Initialisation depot

This commit is contained in:
Serge NOEL
2026-02-10 12:12:11 +01:00
commit c3176e8d79
818 changed files with 52573 additions and 0 deletions

20
arti-api/.dockerignore Normal file
View File

@@ -0,0 +1,20 @@
# .dockerignore
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
build/
*.egg-info/
.pytest_cache/
.coverage
.env
.venv
env/
venv/
.git/
.gitignore
README.md
*.md
.DS_Store
Thumbs.db

637
arti-api/API_EXAMPLES.md Normal file
View File

@@ -0,0 +1,637 @@
# API Examples and Testing
This document provides examples for testing the Arti-API endpoints using curl commands.
## Health Check Examples
### Get API Status
```bash
curl -X GET "http://localhost:8000/" \
-H "accept: application/json"
```
### Health Check
```bash
curl -X GET "http://localhost:8000/health" \
-H "accept: application/json"
```
## Debian Package Management Examples
### Upload a Debian Package
```bash
# Upload for amd64 architecture (default)
curl -X POST "http://localhost:8000/debian/upload?architecture=amd64" \
-H "accept: application/json" \
-H "Content-Type: multipart/form-data" \
-F "file=@my-package_1.0.0_amd64.deb"
# Upload for arm64 architecture
curl -X POST "http://localhost:8000/debian/upload?architecture=arm64" \
-H "accept: application/json" \
-H "Content-Type: multipart/form-data" \
-F "file=@my-package_1.0.0_arm64.deb"
```
### List All Debian Packages
```bash
curl -X GET "http://localhost:8000/debian/packages" \
-H "accept: application/json"
```
### Delete a Debian Package
```bash
curl -X DELETE "http://localhost:8000/debian/package/my-package_1.0.0_amd64.deb" \
-H "accept: application/json"
```
### Refresh Debian Repository
```bash
curl -X POST "http://localhost:8000/refresh/debian" \
-H "accept: application/json"
```
## Helm Chart Management Examples
### Upload a Helm Chart
```bash
curl -X POST "http://localhost:8000/helm/upload" \
-H "accept: application/json" \
-H "Content-Type: multipart/form-data" \
-F "file=@my-chart-0.1.0.tgz"
```
### List All Helm Charts
```bash
curl -X GET "http://localhost:8000/helm/charts" \
-H "accept: application/json"
```
### Delete a Helm Chart
```bash
curl -X DELETE "http://localhost:8000/helm/chart/my-chart-0.1.0.tgz" \
-H "accept: application/json"
```
### Refresh Helm Repository
```bash
curl -X POST "http://localhost:8000/refresh/helm" \
-H "accept: application/json"
```
## Docker Registry Examples
### List Docker Images
```bash
curl -X GET "http://localhost:8000/docker/images" \
-H "accept: application/json"
```
## User Management Examples
### List All Users
```bash
curl -X GET "http://localhost:8000/users" \
-H "accept: application/json"
```
### Get User Information
```bash
curl -X GET "http://localhost:8000/users/admin" \
-H "accept: application/json"
```
### Create/Update User
```bash
# Create a new user
curl -X POST "http://localhost:8000/users" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"username": "developer",
"password": "secure_password123"
}'
# Update existing user password
curl -X POST "http://localhost:8000/users" \
-H "accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"username": "admin",
"password": "new_secure_password456"
}'
```
### Delete User
```bash
curl -X DELETE "http://localhost:8000/users/olduser" \
-H "accept: application/json"
```
## Repository Refresh Examples
### Refresh All Repositories
```bash
curl -X POST "http://localhost:8000/refresh/all" \
-H "accept: application/json"
```
## Python Examples
### Using requests library
```python
import requests
# Health check
response = requests.get("http://localhost:8000/health")
print(response.json())
# Upload a package
with open("my-package_1.0.0_amd64.deb", "rb") as f:
files = {"file": f}
params = {"architecture": "amd64"}
response = requests.post(
"http://localhost:8000/debian/upload",
files=files,
params=params
)
print(response.json())
# List packages
response = requests.get("http://localhost:8000/debian/packages")
print(response.json())
# User management
# Create a user
user_data = {"username": "testuser", "password": "testpass123"}
response = requests.post("http://localhost:8000/users", json=user_data)
print(response.json())
# List users
response = requests.get("http://localhost:8000/users")
print(response.json())
# Get user info
response = requests.get("http://localhost:8000/users/testuser")
print(response.json())
```
## PHP Examples
### Using cURL in PHP
```php
<?php
// Health check
function checkHealth() {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://localhost:8000/health");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json']);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
return json_decode($response, true);
}
return false;
}
// Upload Debian package
function uploadDebianPackage($filePath, $architecture = 'amd64') {
$ch = curl_init();
$postFields = [
'file' => new CURLFile($filePath, 'application/vnd.debian.binary-package'),
];
curl_setopt($ch, CURLOPT_URL, "http://localhost:8000/debian/upload?architecture=" . urlencode($architecture));
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json']);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
'success' => $httpCode === 200,
'data' => json_decode($response, true),
'http_code' => $httpCode
];
}
// List Debian packages
function listDebianPackages() {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://localhost:8000/debian/packages");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json']);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
return json_decode($response, true);
}
return false;
}
// Upload Helm chart
function uploadHelmChart($filePath) {
$ch = curl_init();
$postFields = [
'file' => new CURLFile($filePath, 'application/gzip'),
];
curl_setopt($ch, CURLOPT_URL, "http://localhost:8000/helm/upload");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $postFields);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json']);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
'success' => $httpCode === 200,
'data' => json_decode($response, true),
'http_code' => $httpCode
];
}
// List Helm charts
function listHelmCharts() {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://localhost:8000/helm/charts");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json']);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
return json_decode($response, true);
}
return false;
}
// User management
function createUser($username, $password) {
$ch = curl_init();
$userData = [
'username' => $username,
'password' => $password
];
curl_setopt($ch, CURLOPT_URL, "http://localhost:8000/users");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($userData));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Accept: application/json'
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
'success' => $httpCode === 200,
'data' => json_decode($response, true),
'http_code' => $httpCode
];
}
function listUsers() {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://localhost:8000/users");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json']);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode === 200) {
return json_decode($response, true);
}
return false;
}
function getUserInfo($username) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://localhost:8000/users/" . urlencode($username));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json']);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
'success' => $httpCode === 200,
'data' => json_decode($response, true),
'http_code' => $httpCode
];
}
function deleteUser($username) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://localhost:8000/users/" . urlencode($username));
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json']);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
'success' => $httpCode === 200,
'data' => json_decode($response, true),
'http_code' => $httpCode
];
}
function refreshAllRepositories() {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, "http://localhost:8000/refresh/all");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Accept: application/json']);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [
'success' => $httpCode === 200,
'data' => json_decode($response, true),
'http_code' => $httpCode
];
}
// Example usage
try {
// Check API health
$health = checkHealth();
if ($health) {
echo "API Status: " . $health['status'] . "\n";
}
// List packages
$packages = listDebianPackages();
if ($packages) {
echo "Found " . count($packages['packages']) . " Debian packages\n";
}
// Create a user
$result = createUser('php_user', 'secure_password123');
if ($result['success']) {
echo "User created: " . $result['data']['message'] . "\n";
} else {
echo "Failed to create user: HTTP " . $result['http_code'] . "\n";
}
// List users
$users = listUsers();
if ($users) {
echo "Registry users: " . implode(', ', $users['users']) . "\n";
}
// Refresh repositories
$refresh = refreshAllRepositories();
if ($refresh['success']) {
echo "Repositories refreshed: " . $refresh['data']['message'] . "\n";
}
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}
?>
```
### Using Guzzle HTTP Client (Recommended)
```php
<?php
require_once 'vendor/autoload.php';
use GuzzleHttp\Client;
use GuzzleHttp\Exception\RequestException;
class ArtiApiClient {
private $client;
private $baseUrl;
public function __construct($baseUrl = 'http://localhost:8000') {
$this->baseUrl = $baseUrl;
$this->client = new Client([
'base_uri' => $baseUrl,
'timeout' => 30,
'headers' => [
'Accept' => 'application/json'
]
]);
}
public function checkHealth() {
try {
$response = $this->client->get('/health');
return json_decode($response->getBody(), true);
} catch (RequestException $e) {
return false;
}
}
public function uploadDebianPackage($filePath, $architecture = 'amd64') {
try {
$response = $this->client->post('/debian/upload', [
'multipart' => [
[
'name' => 'file',
'contents' => fopen($filePath, 'r'),
'filename' => basename($filePath)
]
],
'query' => ['architecture' => $architecture]
]);
return [
'success' => true,
'data' => json_decode($response->getBody(), true)
];
} catch (RequestException $e) {
return [
'success' => false,
'error' => $e->getMessage(),
'http_code' => $e->getCode()
];
}
}
public function listDebianPackages() {
try {
$response = $this->client->get('/debian/packages');
return json_decode($response->getBody(), true);
} catch (RequestException $e) {
return false;
}
}
public function createUser($username, $password) {
try {
$response = $this->client->post('/users', [
'json' => [
'username' => $username,
'password' => $password
]
]);
return [
'success' => true,
'data' => json_decode($response->getBody(), true)
];
} catch (RequestException $e) {
return [
'success' => false,
'error' => $e->getMessage(),
'http_code' => $e->getCode()
];
}
}
public function listUsers() {
try {
$response = $this->client->get('/users');
return json_decode($response->getBody(), true);
} catch (RequestException $e) {
return false;
}
}
public function deleteUser($username) {
try {
$response = $this->client->delete("/users/{$username}");
return [
'success' => true,
'data' => json_decode($response->getBody(), true)
];
} catch (RequestException $e) {
return [
'success' => false,
'error' => $e->getMessage(),
'http_code' => $e->getCode()
];
}
}
}
// Example usage with Guzzle
$api = new ArtiApiClient();
// Check health
$health = $api->checkHealth();
if ($health) {
echo "API is healthy: " . $health['status'] . "\n";
}
// List packages
$packages = $api->listDebianPackages();
if ($packages) {
foreach ($packages['packages'] as $package) {
echo "Package: {$package['name']} ({$package['size']} bytes)\n";
}
}
// Create user
$userResult = $api->createUser('guzzle_user', 'test123');
if ($userResult['success']) {
echo "User created successfully\n";
} else {
echo "Failed to create user: " . $userResult['error'] . "\n";
}
```
## Response Examples
### Successful Package Upload Response
```json
{
"message": "Package my-app_1.0.0_amd64.deb uploaded successfully",
"path": "/data/debian/pool/my-app_1.0.0_amd64.deb"
}
```
### Package List Response
```json
{
"packages": [
{
"name": "my-app_1.0.0_amd64.deb",
"size": 1024000,
"modified": "2023-12-01T10:30:00.123456"
},
{
"name": "another-app_2.0.0_arm64.deb",
"size": 2048000,
"modified": "2023-12-01T11:45:00.789012"
}
]
}
```
### Error Response Example
```json
{
"detail": "File must be a .deb package"
}
```
### User Management Response Examples
#### User List Response
```json
{
"users": ["admin", "developer", "readonly", "testuser"]
}
```
#### User Creation Response
```json
{
"message": "User developer created successfully"
}
```
#### User Update Response
```json
{
"message": "User admin updated successfully"
}
```
#### User Info Response
```json
{
"username": "developer",
"created": "2023-12-01T10:30:00.123456"
}
```
#### User Deletion Response
```json
{
"message": "User olduser deleted successfully"
}
```

View File

@@ -0,0 +1,309 @@
# Chart Museum Configuration with htpasswd Authentication
Chart Museum supports htpasswd authentication using the same `/data/htpasswd` file managed by the Arti-API.
## Chart Museum Configuration
### Environment Variables
```bash
# Basic configuration
STORAGE=local
STORAGE_LOCAL_ROOTDIR=/charts
PORT=8080
# Authentication configuration
AUTH_ANONYMOUS_GET=false
BASIC_AUTH_USER=admin
BASIC_AUTH_PASS=password
# OR use htpasswd file (recommended)
HTPASSWD_PATH=/data/htpasswd
AUTH_REALM="Chart Museum"
```
### Docker Compose Configuration
```yaml
version: '3.8'
services:
chartmuseum:
image: chartmuseum/chartmuseum:latest
container_name: chartmuseum
environment:
# Storage configuration
- STORAGE=local
- STORAGE_LOCAL_ROOTDIR=/charts
- PORT=8080
# Authentication with htpasswd
- AUTH_ANONYMOUS_GET=false
- HTPASSWD_PATH=/data/htpasswd
- AUTH_REALM=Chart Museum
# Optional: Allow chart overwrite
- ALLOW_OVERWRITE=true
# Optional: Enable API
- DISABLE_API=false
# Optional: Enable metrics
- DISABLE_METRICS=false
# Optional: Enable logging
- LOG_JSON=true
- DEBUG=false
ports:
- "8080:8080"
volumes:
- /data:/data # Same volume as Arti-API
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
```
### Kubernetes Configuration
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: chartmuseum
labels:
app: chartmuseum
spec:
replicas: 1
selector:
matchLabels:
app: chartmuseum
template:
metadata:
labels:
app: chartmuseum
spec:
containers:
- name: chartmuseum
image: chartmuseum/chartmuseum:latest
ports:
- containerPort: 8080
env:
- name: STORAGE
value: "local"
- name: STORAGE_LOCAL_ROOTDIR
value: "/charts"
- name: PORT
value: "8080"
- name: AUTH_ANONYMOUS_GET
value: "false"
- name: HTPASSWD_PATH
value: "/data/htpasswd"
- name: AUTH_REALM
value: "Chart Museum"
- name: ALLOW_OVERWRITE
value: "true"
- name: DISABLE_API
value: "false"
- name: LOG_JSON
value: "true"
volumeMounts:
- name: artifactory-storage
mountPath: /data
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
volumes:
- name: artifactory-storage
persistentVolumeClaim:
claimName: artifactory-pvc
---
apiVersion: v1
kind: Service
metadata:
name: chartmuseum-service
labels:
app: chartmuseum
spec:
type: ClusterIP
ports:
- port: 8080
targetPort: 8080
protocol: TCP
selector:
app: chartmuseum
```
## Complete Artifactory Setup
### Full Docker Compose with All Services
```yaml
version: '3.8'
services:
# Arti-API for management
arti-api:
build: .
container_name: arti-api
ports:
- "8000:8000"
volumes:
- artifactory_data:/data
environment:
- PYTHONUNBUFFERED=1
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
restart: unless-stopped
# Chart Museum with htpasswd authentication
chartmuseum:
image: chartmuseum/chartmuseum:latest
container_name: chartmuseum
environment:
- STORAGE=local
- STORAGE_LOCAL_ROOTDIR=/data/charts
- PORT=8080
- AUTH_ANONYMOUS_GET=false
- HTPASSWD_PATH=/data/htpasswd
- AUTH_REALM=Chart Museum
- ALLOW_OVERWRITE=true
- DISABLE_API=false
- LOG_JSON=true
ports:
- "8080:8080"
volumes:
- artifactory_data:/data
depends_on:
- arti-api
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
# Docker Registry with htpasswd authentication
registry:
image: registry:2
container_name: docker-registry
environment:
- REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/data/docker
- REGISTRY_AUTH=htpasswd
- REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm
- REGISTRY_AUTH_HTPASSWD_PATH=/data/htpasswd
- REGISTRY_HTTP_ADDR=0.0.0.0:5000
ports:
- "5000:5000"
volumes:
- artifactory_data:/data
depends_on:
- arti-api
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:5000/v2/"]
interval: 30s
timeout: 10s
retries: 3
# Nginx for Debian repository
nginx-debian:
image: nginx:alpine
container_name: nginx-debian
ports:
- "8081:80"
volumes:
- artifactory_data:/data
- ./nginx-debian.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- arti-api
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/"]
interval: 30s
timeout: 10s
retries: 3
volumes:
artifactory_data:
driver: local
```
## Authentication Management
### Using Arti-API to manage users for Chart Museum
```bash
# Create a user that can access Chart Museum
curl -X POST "http://localhost:8000/users" \
-H "Content-Type: application/json" \
-d '{"username": "chartuser", "password": "secure_password123"}'
# List all users
curl -X GET "http://localhost:8000/users"
```
### Test Chart Museum Authentication
```bash
# Without authentication (should fail)
curl -X GET "http://localhost:8080/api/charts"
# With authentication (should work)
curl -u chartuser:secure_password123 -X GET "http://localhost:8080/api/charts"
# Upload a chart with authentication
curl -u chartuser:secure_password123 \
--data-binary "@mychart-0.1.0.tgz" \
"http://localhost:8080/api/charts"
```
### Helm Client Configuration
```bash
# Add the authenticated repository
helm repo add myrepo http://chartuser:secure_password123@localhost:8080
# Or use helm repo add with separate credentials
helm repo add myrepo http://localhost:8080 \
--username chartuser \
--password secure_password123
# Update and search
helm repo update
helm search repo myrepo
```
## Benefits of This Setup
**Unified Authentication**: Same htpasswd file for Docker Registry and Chart Museum
**Centralized User Management**: Use Arti-API to manage all users
**Secure**: bcrypt-hashed passwords
**Standard Compatible**: Works with standard Helm and Docker clients
**Scalable**: Can add more services using the same authentication
**API-Driven**: Programmatic user management through REST API
## Security Notes
- The htpasswd file is shared between all services
- Users created through Arti-API work for both Docker Registry and Chart Museum
- Consider using HTTPS in production
- Regular password rotation is recommended
- Monitor access logs for security auditing

32
arti-api/Dockerfile Normal file
View File

@@ -0,0 +1,32 @@
# Use Python 3.11 slim image as base
FROM python:3.11-slim
# Set working directory
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements first for better Docker layer caching
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY app.py .
# Create volume for shared PVC data
VOLUME ["/data"]
# Expose port 8000
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# Run the application
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@@ -0,0 +1,565 @@
# Kubernetes Network Policies for Artifactory Services
This document provides NetworkPolicy configurations to restrict access to artifactory services, allowing only root path access externally while keeping all other endpoints internal-only.
## Network Policy Strategy
### Access Control Rules:
- **External Access**: Only `/` (root/health check endpoints)
- **Internal Access**: All endpoints from `192.168.100.0/24` network
- **Service Communication**: Allow pod-to-pod communication within namespace
## Network Policies
### 1. Arti-API Network Policy
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: arti-api-network-policy
namespace: default
spec:
podSelector:
matchLabels:
app: arti-api
policyTypes:
- Ingress
- Egress
ingress:
# Allow internal network access to all endpoints
- from:
- ipBlock:
cidr: 192.168.100.0/24
ports:
- protocol: TCP
port: 8000
# Allow external access only to health/status endpoints
# Note: This requires an Ingress controller or service mesh
# to handle path-based routing restrictions
- from: [] # All external sources
ports:
- protocol: TCP
port: 8000
# Allow communication from other services in the same namespace
- from:
- namespaceSelector:
matchLabels:
name: default
- podSelector: {}
ports:
- protocol: TCP
port: 8000
egress:
# Allow outbound traffic for API functionality
- to: []
ports:
- protocol: TCP
port: 53 # DNS
- protocol: UDP
port: 53 # DNS
- to:
- podSelector: {} # Allow communication to other pods
```
### 2. Chart Museum Network Policy
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: chartmuseum-network-policy
namespace: default
spec:
podSelector:
matchLabels:
app: chartmuseum
policyTypes:
- Ingress
- Egress
ingress:
# Allow internal network access to all endpoints
- from:
- ipBlock:
cidr: 192.168.100.0/24
ports:
- protocol: TCP
port: 8080
# Allow external access only to health endpoint
- from: []
ports:
- protocol: TCP
port: 8080
# Allow communication from arti-api and other services
- from:
- podSelector:
matchLabels:
app: arti-api
- namespaceSelector:
matchLabels:
name: default
ports:
- protocol: TCP
port: 8080
egress:
# Allow outbound traffic
- to: []
ports:
- protocol: TCP
port: 53
- protocol: UDP
port: 53
- to:
- podSelector: {}
```
### 3. Docker Registry Network Policy
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: docker-registry-network-policy
namespace: default
spec:
podSelector:
matchLabels:
app: docker-registry
policyTypes:
- Ingress
- Egress
ingress:
# Allow internal network access to all endpoints
- from:
- ipBlock:
cidr: 192.168.100.0/24
ports:
- protocol: TCP
port: 5000
# Allow external access only to health endpoint (/v2/)
- from: []
ports:
- protocol: TCP
port: 5000
# Allow communication from arti-api and other services
- from:
- podSelector:
matchLabels:
app: arti-api
- namespaceSelector:
matchLabels:
name: default
ports:
- protocol: TCP
port: 5000
egress:
# Allow outbound traffic
- to: []
ports:
- protocol: TCP
port: 53
- protocol: UDP
port: 53
- to:
- podSelector: {}
```
## Path-Based Access Control with Traefik v2
Since NetworkPolicy works at the network layer and cannot filter by HTTP paths, you need to combine it with Traefik IngressRoute for path-based restrictions.
### Traefik v2 IngressRoute Configuration
```yaml
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: arti-api-simple
namespace: artifactory
spec:
entryPoints:
- web
routes:
# Internal network gets full access
- match: Host(`api.artifactory.local`) && ClientIP(`192.168.100.0/24`)
kind: Rule
priority: 100
services:
- name: arti-api-service
port: 8000
middlewares:
- name: internal-headers
# External access - only health endpoints
- match: Host(`api.artifactory.local`) && (Path(`/`) || Path(`/health`))
kind: Rule
priority: 90
services:
- name: arti-api-service
port: 8000
middlewares:
- name: external-health-headers
# Block all other external access
- match: Host(`api.artifactory.local`)
kind: Rule
priority: 10
services:
- name: error-service
port: 80
middlewares:
- name: block-external
```
### Complete Traefik Configuration Files
Two versions are provided:
1. **`traefik-simple.yaml`** - Simplified, easy to understand configuration
2. **`traefik-ingressroute.yaml`** - Full-featured with TLS and advanced middlewares
### Key Features:
- **Priority-based routing**: Higher priority rules are evaluated first
- **ClientIP matching**: Uses `ClientIP()` matcher to identify internal network
- **Path-based filtering**: Specific paths allowed for external access
- **Custom error pages**: Friendly 403 pages with helpful information
- **Middleware chaining**: Headers and access control through middlewares
### Deployment:
```bash
# Deploy the simplified version
kubectl apply -f traefik-simple.yaml
# Or deploy the full-featured version
kubectl apply -f traefik-ingressroute.yaml
```
### Istio Service Mesh Configuration
If using Istio, you can implement more granular path-based access control:
```yaml
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: arti-api-access-control
namespace: default
spec:
selector:
matchLabels:
app: arti-api
rules:
# Allow internal network access to all paths
- from:
- source:
ipBlocks: ["192.168.100.0/24"]
# Allow external access only to health endpoints
- to:
- operation:
paths: ["/", "/health"]
# Deny all other external access
- from:
- source:
notIpBlocks: ["192.168.100.0/24"]
to:
- operation:
notPaths: ["/", "/health"]
when:
- key: request.headers[':path']
notValues: ["/", "/health"]
action: DENY
```
## Complete Kubernetes Deployment with Network Policies
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: artifactory
labels:
name: artifactory
---
# Arti-API Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: arti-api
namespace: artifactory
labels:
app: arti-api
spec:
replicas: 1
selector:
matchLabels:
app: arti-api
template:
metadata:
labels:
app: arti-api
spec:
containers:
- name: arti-api
image: hexah/arti-api:1.0.1
ports:
- containerPort: 8000
env:
- name: PYTHONUNBUFFERED
value: "1"
volumeMounts:
- name: artifactory-storage
mountPath: /data
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
volumes:
- name: artifactory-storage
persistentVolumeClaim:
claimName: artifactory-pvc
---
# Arti-API Service
apiVersion: v1
kind: Service
metadata:
name: arti-api-service
namespace: artifactory
labels:
app: arti-api
spec:
type: ClusterIP
ports:
- port: 8000
targetPort: 8000
protocol: TCP
selector:
app: arti-api
---
# Chart Museum Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: chartmuseum
namespace: artifactory
labels:
app: chartmuseum
spec:
replicas: 1
selector:
matchLabels:
app: chartmuseum
template:
metadata:
labels:
app: chartmuseum
spec:
containers:
- name: chartmuseum
image: chartmuseum/chartmuseum:latest
ports:
- containerPort: 8080
env:
- name: STORAGE
value: "local"
- name: STORAGE_LOCAL_ROOTDIR
value: "/data/charts"
- name: PORT
value: "8080"
- name: AUTH_ANONYMOUS_GET
value: "false"
- name: HTPASSWD_PATH
value: "/data/htpasswd"
- name: AUTH_REALM
value: "Chart Museum"
- name: ALLOW_OVERWRITE
value: "true"
- name: DISABLE_API
value: "false"
volumeMounts:
- name: artifactory-storage
mountPath: /data
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
volumes:
- name: artifactory-storage
persistentVolumeClaim:
claimName: artifactory-pvc
---
# Chart Museum Service
apiVersion: v1
kind: Service
metadata:
name: chartmuseum-service
namespace: artifactory
labels:
app: chartmuseum
spec:
type: ClusterIP
ports:
- port: 8080
targetPort: 8080
protocol: TCP
selector:
app: chartmuseum
---
# Network Policies
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: artifactory-network-policy
namespace: artifactory
spec:
podSelector: {} # Apply to all pods in namespace
policyTypes:
- Ingress
- Egress
ingress:
# Allow internal network full access
- from:
- ipBlock:
cidr: 192.168.100.0/24
# Allow limited external access (health checks only)
- from: []
ports:
- protocol: TCP
port: 8000 # Arti-API
- protocol: TCP
port: 8080 # Chart Museum
- protocol: TCP
port: 5000 # Docker Registry
# Allow inter-pod communication
- from:
- podSelector: {}
egress:
# Allow DNS
- to: []
ports:
- protocol: TCP
port: 53
- protocol: UDP
port: 53
# Allow inter-pod communication
- to:
- podSelector: {}
# Allow outbound internet (for package downloads, etc.)
- to: []
ports:
- protocol: TCP
port: 80
- protocol: TCP
port: 443
---
# PersistentVolumeClaim
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: artifactory-pvc
namespace: artifactory
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi
```
## Testing Network Policies
### 1. Test from Internal Network (192.168.100.x)
```bash
# Should work - internal access to all endpoints
curl http://arti-api-service.artifactory.svc.cluster.local:8000/users
curl http://chartmuseum-service.artifactory.svc.cluster.local:8080/api/charts
# Test from a pod in the internal network
kubectl run test-pod --rm -i --tty --image=curlimages/curl -- sh
# Inside the pod:
curl http://arti-api-service.artifactory.svc.cluster.local:8000/debian/packages
```
### 2. Test from External Network
```bash
# Should work - external access to health endpoints
curl http://your-ingress-ip/health
curl http://your-ingress-ip/
# Should be blocked - external access to management endpoints
curl http://your-ingress-ip/users # Should return 403 or timeout
curl http://your-ingress-ip/debian/packages # Should return 403 or timeout
```
### 3. Verify Network Policy
```bash
# Check network policies
kubectl get networkpolicies -n artifactory
# Describe policy
kubectl describe networkpolicy artifactory-network-policy -n artifactory
# Check if pods are selected by policy
kubectl get pods -n artifactory --show-labels
```
## Key Points
1. **NetworkPolicy Limitations**: NetworkPolicy works at Layer 3/4, not HTTP paths
2. **Path-Based Control**: Use Ingress controllers or service mesh for HTTP path filtering
3. **Internal Network**: `192.168.100.0/24` gets full access to all endpoints
4. **External Access**: Limited to health check endpoints only
5. **Service Communication**: Pods can communicate within the namespace
6. **DNS**: Allow DNS traffic for service discovery
This configuration provides defense in depth by combining network-level and application-level access controls.

214
arti-api/README.md Normal file
View File

@@ -0,0 +1,214 @@
# Arti-Api
This is the api part of artifactory server.
Artifactory server consist in backends servers providing services to applications:
- **docker** : a docker registry
- **helm** : chart museum
- **debian** : Nginx serving .deb files for arm64 and amd64 binaries
Each server are in a pod, sharing a pvc volume, with following folder structure :
```console
.
├── docker
├── debian
| ├── dist
| | ├── Release
| | └── main
| | ├── binary-arm64
| | └── binary-amd64
| └── pool
└── charts
```
## Api
The api pod, must be able to update shared pvc volume :
- add / update / delete binary
- refresh what is needed
## Container Application
This repository now contains a complete containerized FastAPI application that provides REST endpoints to manage the artifactory server components.
### Features
- **Debian Package Management**: Upload, delete, and list `.deb` packages
- **Helm Chart Management**: Upload, delete, and list Helm charts (`.tgz` files)
- **Docker Registry Integration**: List Docker images in the registry
- **User Management**: Create, update, delete, and list Docker registry users with htpasswd authentication
- **Repository Refresh**: Refresh package indexes and chart repositories
- **Health Monitoring**: Health check endpoints for container orchestration
### API Endpoints
#### Health & Status
- `GET /` - Root endpoint with API status
- `GET /health` - Health check endpoint
#### Debian Repository
- `POST /debian/upload` - Upload .deb packages
- `GET /debian/packages` - List all Debian packages
- `DELETE /debian/package/{package_name}` - Delete a specific package
- `POST /refresh/debian` - Refresh Debian package indexes
#### Helm Repository
- `POST /helm/upload` - Upload Helm charts (.tgz files)
- `GET /helm/charts` - List all Helm charts
- `DELETE /helm/chart/{chart_name}` - Delete a specific chart
- `POST /refresh/helm` - Refresh Helm chart index
#### Docker Registry
- `GET /docker/images` - List Docker images
#### User Management
- `GET /users` - List all Docker registry users
- `GET /users/{username}` - Get user information
- `POST /users` - Create or update a user
- `DELETE /users/{username}` - Delete a user
#### General Operations
- `POST /refresh/all` - Refresh all repositories
### Quick Start
#### Using Docker Compose (Recommended for development)
```bash
# Build and run the container
./build.sh
docker-compose up -d
# Access the API
curl http://localhost:8000/health
```
#### Using Kubernetes (Recommended for production)
```bash
# Build the container
./build.sh
# Deploy to Kubernetes
kubectl apply -f kubernetes.yaml
# Check deployment status
kubectl get pods -l app=arti-api
```
#### Manual Docker Build
```bash
# Build the image
docker build -t arti-api:latest .
# Run the container
docker run -d \
-p 8000:8000 \
-v $(pwd)/data:/data \
--name arti-api \
arti-api:latest
```
### Configuration
The application expects the shared PVC volume to be mounted at `/data` with the following structure:
- `/data/docker` - Docker registry data
- `/data/debian/dist` - Debian distribution metadata
- `/data/debian/pool` - Debian package pool
- `/data/charts` - Helm charts storage
- `/data/htpasswd` - Docker registry user authentication file
### Environment Variables
- `PYTHONUNBUFFERED=1` - Ensures real-time logging output
### API Documentation
Once the container is running, you can access comprehensive API documentation:
#### Interactive Documentation
- **Swagger UI**: `http://localhost:8000/docs` - Interactive API testing interface
- **ReDoc**: `http://localhost:8000/redoc` - Clean, responsive API documentation
- **OpenAPI Schema**: `http://localhost:8000/openapi.json` - Machine-readable API specification
#### Quick Documentation Server
```bash
# Start documentation server with one command
./serve-docs.sh
# Or manually
docker run -d -p 8000:8000 --name arti-api-docs arti-api:latest
```
#### API Features in Documentation
- 📋 **Comprehensive endpoint documentation** with detailed descriptions
- 🔧 **Interactive testing interface** - test endpoints directly from the browser
- 📝 **Request/response examples** with real data samples
- 🏷️ **Organized by tags** - endpoints grouped by functionality (health, debian, helm, docker, refresh)
- 📊 **Schema definitions** for all data models
- ⚠️ **Error response documentation** with HTTP status codes
- 🚀 **Example curl commands** for all endpoints
#### Testing Examples
See `API_EXAMPLES.md` for comprehensive testing examples including:
- Curl commands for all endpoints
- Python code examples
- Expected response formats
- Error handling examples
### File Structure
```
.
├── app.py # Main FastAPI application with comprehensive Swagger docs
├── requirements.txt # Python dependencies
├── Dockerfile # Container definition
├── docker-compose.yaml # Simple Docker Compose configuration
├── docker-compose-full.yaml # Complete artifactory stack with authentication
├── kubernetes.yaml # Kubernetes deployment manifests
├── build.sh # Build script
├── serve-docs.sh # Documentation server script
├── setup-full-stack.sh # Complete artifactory setup with authentication
├── API_EXAMPLES.md # Comprehensive API testing examples
├── CHARTMUSEUM_AUTH.md # Chart Museum authentication guide
├── .dockerignore # Docker ignore file
└── README.md # This file
```
## Chart Museum Authentication
Yes! Chart Museum can be protected with the same htpasswd file managed by the Arti-API. See `CHARTMUSEUM_AUTH.md` for complete configuration details.
### Quick Setup with Authentication
```bash
# Setup complete authenticated artifactory stack
./setup-full-stack.sh
# This creates:
# - Arti-API (port 8000)
# - Chart Museum with htpasswd auth (port 8080)
# - Docker Registry with htpasswd auth (port 5000)
# - Default users: admin, developer, readonly
```
### Chart Museum Configuration
Chart Museum supports htpasswd authentication using these environment variables:
```bash
HTPASSWD_PATH=/data/htpasswd
AUTH_ANONYMOUS_GET=false
AUTH_REALM="Chart Museum"
```
### Usage Examples
```bash
# Test authenticated access
curl -u admin:admin123 http://localhost:8080/api/charts
# Add authenticated Helm repository
helm repo add myrepo http://admin:admin123@localhost:8080
# Upload chart with authentication
curl -u admin:admin123 --data-binary "@chart.tgz" http://localhost:8080/api/charts
```

1091
arti-api/app.py Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,23 @@
// .drone.jsonnet - Main pipeline configuration entry point
// This file imports the actual configuration from the pipeline folder
local buildSteps = import 'pipeline/build-steps.libsonnet';
local commonConfig = import 'pipeline/common.libsonnet';
{
kind: "pipeline",
type: "kubernetes",
name: "auth-service-build",
service_account: "drone-runner",
clone: { disable: true },
environment: commonConfig.environment,
steps: [
commonConfig.cloneStep,
commonConfig.versionStep,
commonConfig.testStep,
buildSteps.externalBuildahStep,
buildSteps.pushDockerStep,
buildSteps.scaleDownStep
],
trigger: commonConfig.trigger
}

View File

@@ -0,0 +1,168 @@
clone:
disable: true
environment:
GIT_SSL_NO_VERIFY: "true"
kind: pipeline
name: auth-service-build
service_account: drone-runner
steps:
- commands:
- "echo '\U0001F504 Cloning repository...'"
- git config --global http.sslVerify false
- git config --global user.email 'drone@aipice.local'
- git config --global user.name 'Drone CI'
- git clone https://gitea.aipice.local/AIPICE/auth-service.git . || echo 'Clone failed, but continuing...'
- git checkout $DRONE_COMMIT || echo 'Checkout failed, using default'
image: alpine/git
name: clone
when:
event:
- push
- commands:
- "echo '\U0001F4C4 Reading version configuration...'"
- echo 'Sourcing version.conf...'
- . ./version.conf
- 'echo "BASE_VERSION: $BASE_VERSION"'
- 'echo "DOCKER_REPO: $DOCKER_REPO"'
- DOCKER_TAG="$DOCKER_REPO:$BASE_VERSION.$DRONE_BUILD_NUMBER"
- 'echo "DOCKER_TAG: $DOCKER_TAG"'
- echo '✅ Version configuration loaded!'
- 'echo "Will build: $DOCKER_TAG"'
image: alpine:latest
name: read-version
when:
event:
- push
- commands:
- "echo '\U0001F9EA Starting tests...'"
- echo 'Repository ${DRONE_REPO}'
- echo 'Branch ${DRONE_BRANCH}'
- echo 'Owner ${DRONE_REPO_OWNER}'
- echo 'Commit ${DRONE_COMMIT_SHA:0:8}'
- echo 'Build ${DRONE_BUILD_NUMBER}'
- echo 'Reading version info...'
- . ./version.conf
- DOCKER_TAG="$DOCKER_REPO:$BASE_VERSION.$DRONE_BUILD_NUMBER"
- 'echo "Docker tag will be: $DOCKER_TAG"'
- echo 'Checking Dockerfile:'
- cat Dockerfile || echo '❌ Dockerfile not found!'
- echo '✅ Pre-build validation passed!'
image: alpine:latest
name: test
when:
event:
- push
- commands:
- "echo '\U0001F3D7 Building via external Buildah deployment with replica scaling...'"
- echo 'Installing kubectl...'
- apk add --no-cache curl
- curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
- chmod +x kubectl
- mv kubectl /usr/local/bin/
- "echo '\U0001F4E6 Preparing build context...'"
- BUILD_ID="auth-service-${DRONE_BUILD_NUMBER}-$(date +%s)"
- 'echo "Build ID: $BUILD_ID"'
- "echo '\U0001F50D Checking current Buildah deployment replicas...'"
- CURRENT_REPLICAS=$(kubectl get deployment buildah-external -n apps--droneio--prd -o jsonpath='{.spec.replicas}')
- 'echo "Current replicas: $CURRENT_REPLICAS"'
- "echo '\U0001F512 Attempting to scale up Buildah deployment (acts as build lock)...'"
- if [ "$CURRENT_REPLICAS" = "0" ]; then
- ' echo "✅ No build running, scaling up deployment..."'
- ' kubectl scale deployment buildah-external --replicas=1 -n apps--droneio--prd'
- ' echo "⏳ Waiting for pod to be ready..."'
- ' kubectl wait --for=condition=ready pod -l app=buildah-external -n apps--droneio--prd --timeout=120s'
- else
- ' echo "❌ Build already running (replicas=$CURRENT_REPLICAS)! Aborting to prevent conflicts."'
- ' exit 1'
- fi
- echo '<27> Finding ready Buildah pod...'
- BUILDAH_POD=$(kubectl get pods -n apps--droneio--prd -l app=buildah-external --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}')
- if [ -z "$BUILDAH_POD" ]; then
- ' echo "❌ No running Buildah pod found after scaling!"'
- ' kubectl get pods -n apps--droneio--prd -l app=buildah-external'
- ' exit 1'
- fi
- 'echo "✅ Using Buildah pod: $BUILDAH_POD"'
- "echo '\U0001F4C1 Creating build directory in Buildah pod...'"
- kubectl exec $BUILDAH_POD -n apps--droneio--prd -- mkdir -p "/workspace/builds/$BUILD_ID"
- "echo '\U0001F4E4 Copying source files to Buildah pod...'"
- tar czf - . | kubectl exec -i $BUILDAH_POD -n apps--droneio--prd -- tar xzf - -C "/workspace/builds/$BUILD_ID"
- "echo '\U0001F528 Building container image with version from config...'"
- echo 'Reading version configuration...'
- . ./version.conf
- DOCKER_TAG="$DOCKER_REPO:$BASE_VERSION.$DRONE_BUILD_NUMBER"
- 'echo "Building with tag: $DOCKER_TAG"'
- kubectl exec $BUILDAH_POD -n apps--droneio--prd -- sh -c "cd /workspace/builds/$BUILD_ID && buildah build --isolation=chroot --storage-driver=vfs --format=docker --tag $DOCKER_TAG ."
- "echo '\U0001F4CB Listing built images...'"
- kubectl exec $BUILDAH_POD -n apps--droneio--prd -- buildah images | grep auth-service
- 'echo "✅ Image built with tag: $DOCKER_TAG"'
- "echo '\U0001F9F9 Cleaning up build directory...'"
- kubectl exec $BUILDAH_POD -n apps--droneio--prd -- rm -rf "/workspace/builds/$BUILD_ID"
- echo '✅ External Buildah build completed successfully!'
image: alpine:latest
name: build-via-external-buildah
pull: if-not-exists
when:
event:
- push
- commands:
- "echo '\U0001F4E4 Pushing Docker image to registry...'"
- echo 'Installing kubectl...'
- apk add --no-cache curl
- curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
- chmod +x kubectl && mv kubectl /usr/local/bin/
- echo 'Reading version configuration...'
- . ./version.conf
- DOCKER_TAG="$DOCKER_REPO:$BASE_VERSION.$DRONE_BUILD_NUMBER"
- 'echo "Pushing image: $DOCKER_TAG"'
- "echo '\U0001F50D Finding Buildah pod...'"
- BUILDAH_POD=$(kubectl get pods -n apps--droneio--prd -l app=buildah-external --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}')
- 'echo "Using Buildah pod: $BUILDAH_POD"'
- "echo '\U0001F511 Authenticating with Docker registry...'"
- if [ -n "$DOCKER_USERNAME" ] && [ -n "$DOCKER_PASSWORD" ]; then
- ' echo "Logging into Docker registry..."'
- ' kubectl exec $BUILDAH_POD -n apps--droneio--prd -- buildah login -u "$DOCKER_USERNAME" -p "$DOCKER_PASSWORD" "$DOCKER_REGISTRY"'
- else
- ' echo "No Docker credentials provided - attempting unauthenticated push"'
- fi
- "echo '\U0001F680 Pushing image to registry...'"
- kubectl exec $BUILDAH_POD -n apps--droneio--prd -- buildah push "$DOCKER_TAG"
- 'echo "✅ Successfully pushed: $DOCKER_TAG"'
environment:
DOCKER_PASSWORD:
from_secret: docker_password
DOCKER_REGISTRY:
from_secret: docker_registry
DOCKER_USERNAME:
from_secret: docker_username
image: alpine:latest
name: push-docker-image
when:
branch:
- main
- master
event:
- push
- commands:
- "echo '\U0001F53D Scaling down Buildah deployment (release build lock)...'"
- apk add --no-cache curl
- curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
- chmod +x kubectl && mv kubectl /usr/local/bin/
- "echo '\U0001F4CA Current deployment status:'"
- kubectl get deployment buildah-external -n apps--droneio--prd
- "echo '\U0001F53D Scaling down to 0 replicas...'"
- kubectl scale deployment buildah-external --replicas=0 -n apps--droneio--prd
- echo '⏳ Waiting for pods to terminate...'
- kubectl wait --for=delete pod -l app=buildah-external -n apps--droneio--prd --timeout=60s || echo "Pods may still be terminating"
- echo '✅ Buildah deployment scaled down - build lock released!'
image: alpine:latest
name: scale-down-buildah
when:
status:
- success
- failure
trigger:
event:
- push
- pull_request
type: kubernetes

View File

@@ -0,0 +1,48 @@
#!/bin/bash
# Repository Activation Guide for Drone
echo "🚨 REPOSITORY ACTIVATION REQUIRED"
echo "================================="
echo ""
echo "DIAGNOSIS:"
echo "✅ Webhook delivery: WORKING (Drone receives push notifications)"
echo "✅ DNS resolution: WORKING (Gitea can reach Drone)"
echo "✅ .drone.yml file: EXISTS and has proper content"
echo "❌ Repository status: NOT ACTIVATED in Drone"
echo ""
echo "ERROR IN LOGS:"
echo '{"commit":"53f88a1...","error":"configuration: not found","event":"push"}'
echo ""
echo "🎯 TO FIX THIS:"
echo ""
echo "METHOD 1: Web UI (Recommended)"
echo "------------------------------"
echo "1. Open your browser and go to: https://drone.aipice.local"
echo "2. Login with your Gitea credentials"
echo "3. Look for 'AIPICE/auth-service' in the repository list"
echo "4. If not listed, click 'SYNC' button to refresh from Gitea"
echo "5. Find 'AIPICE/auth-service' and click 'ACTIVATE'"
echo "6. Verify the repository shows as 'ACTIVE'"
echo ""
echo "METHOD 2: Check Current Status"
echo "-----------------------------"
echo "Run this command to check if repository is activated:"
echo ""
echo "curl -k -H 'Accept: application/json' https://drone.aipice.local/api/repos"
echo ""
echo "Look for 'AIPICE/auth-service' with 'active: true'"
echo ""
echo "🔧 TROUBLESHOOTING:"
echo ""
echo "If repository doesn't appear:"
echo "- Check Gitea integration settings in Drone"
echo "- Verify DRONE_GITEA_SERVER is correct in ConfigMap"
echo "- Check if user has admin access to the repository in Gitea"
echo ""
echo "Once activated, your sophisticated build system will work:"
echo "✅ Jsonnet configuration with modular imports"
echo "✅ External Buildah with replica-based atomic locking"
echo "✅ Graceful termination (2s vs 30s)"
echo "✅ Full RBAC permissions for deployment scaling"
echo ""
echo "🎉 Everything is ready - just needs activation!"

View File

@@ -0,0 +1,54 @@
# Active Pipeline Configuration
## 🎯 **Currently Active**
**`.drone.jsonnet`** - Jsonnet-based modular configuration
- **Location**: Root directory (required by Drone)
- **Imports from**: `pipeline/common.libsonnet` and `pipeline/build-steps.libsonnet`
- **Status**: ACTIVE - Used for all builds
## 📋 **Reference Files (Not Active)**
### **Starlark Example**
- **File**: `pipeline/.drone.star.example`
- **Purpose**: Reference example of Starlark configuration
- **Status**: INACTIVE - Example only
### **YAML Variants**
- **Files**: `pipeline/.drone.yml.*`
- **Purpose**: Alternative configurations and evolution history
- **Status**: INACTIVE - Reference/backup only
### **YAML Anchors**
- **File**: `pipeline/.drone.yml.anchors`
- **Purpose**: Example of YAML anchor-based factorization
- **Status**: INACTIVE - Example only
## 🔧 **Configuration Hierarchy**
```
1. .drone.jsonnet (ROOT) ← ACTIVE
├── imports pipeline/common.libsonnet
└── imports pipeline/build-steps.libsonnet
2. pipeline/.drone.star.example ← Example
3. pipeline/.drone.yml.* ← Backup/Reference
```
## ⚙️ **How Drone Processes Files**
Drone looks for configuration files in this order:
1. **`.drone.jsonnet`** ← ✅ YOUR ACTIVE CONFIG
2. `.drone.star`
3. `.drone.yml`
4. `.drone.yaml`
Since you have `.drone.jsonnet` in the root, **that's what Drone uses**.
## 🎯 **To Make Changes**
Edit these files:
- `pipeline/common.libsonnet` - Shared steps, environment, triggers
- `pipeline/build-steps.libsonnet` - Build logic, external Buildah
Then commit and push - Drone automatically processes the Jsonnet!

View File

@@ -0,0 +1,83 @@
# Docker Registry Configuration Guide
## Setting up Docker Registry Secrets in Drone
To use a private Docker registry, you need to configure secrets in Drone. Here's how:
### 1. Create Secrets in Drone UI
Go to `https://drone.aipice.local` → Your Repository → Settings → Secrets
Create these secrets:
```bash
# For Docker Hub:
docker_username = your-dockerhub-username
docker_password = your-dockerhub-password
docker_registry = docker.io
# For GitHub Container Registry:
docker_username = your-github-username
docker_password = your-github-token
docker_registry = ghcr.io
# For Harbor/Private Registry:
docker_username = your-harbor-username
docker_password = your-harbor-password
docker_registry = harbor.example.com
```
### 2. Alternative: CLI Method
```bash
# Install drone CLI first
curl -L https://github.com/harness/drone-cli/releases/latest/download/drone_linux_amd64.tar.gz | tar zx
sudo install -t /usr/local/bin drone
# Set server and token
export DRONE_SERVER=https://drone.aipice.local
export DRONE_TOKEN=your-drone-token
# Create secrets
drone secret add --repository AIPICE/auth-service --name docker_username --data "your-username"
drone secret add --repository AIPICE/auth-service --name docker_password --data "your-password"
drone secret add --repository AIPICE/auth-service --name docker_registry --data "docker.io"
```
### 3. Update version.conf for Different Registries
```bash
# For Docker Hub:
DOCKER_REPO=yourusername/auth-service
# For GitHub Container Registry:
DOCKER_REPO=ghcr.io/yourusername/auth-service
# For Harbor:
DOCKER_REPO=harbor.example.com/project/auth-service
# For Local Registry:
DOCKER_REPO=registry.aipice.local/auth-service
```
### 4. Generated Docker Tags
With `BASE_VERSION=1.0` in version.conf, your images will be tagged as:
- `yourusername/auth-service:1.0.123` (where 123 is the build number)
- `ghcr.io/yourusername/auth-service:1.0.456`
- etc.
### 5. Troubleshooting
If push fails:
1. Check secrets are properly set in Drone UI
2. Verify registry URL format
3. Ensure credentials have push permissions
4. Check registry accepts the image format
### 6. Test Authentication
You can test manually:
```bash
kubectl exec buildah-pod -- buildah login -u username -p password registry.example.com
```

View File

@@ -0,0 +1,32 @@
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies for LDAP
RUN apt-get update && apt-get install -y \
libldap2-dev \
libsasl2-dev \
libssl-dev \
gcc \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements first for better caching
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create non-root user
RUN adduser --disabled-password --gecos 'Non Root' appuser && chown -R appuser:appuser /app
USER appuser
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
EXPOSE 8080
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8080"]

View File

@@ -0,0 +1,210 @@
---
# Auth Service Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-service
namespace: {{ .Values.global.Category }}--{{ .Values.global.Name }}--{{ .Values.global.Type }}
spec:
replicas: 2
selector:
matchLabels:
app: auth-service
template:
metadata:
labels:
app: auth-service
spec:
containers:
- name: auth-service
image: {{ .Values.authService.image }}:{{ .Values.authService.tag }}
ports:
- containerPort: 8080
env:
- name: JWT_SECRET
valueFrom:
secretKeyRef:
name: auth-secrets
key: jwt-secret
- name: TOKEN_EXPIRE_HOURS
value: "8"
- name: ALLOWED_DOMAINS
value: "{{ .Values.authService.allowedDomains }}"
- name: AUTH_DOMAIN
value: "{{ .Values.authService.domain }}"
- name: CORS_ORIGINS
value: "{{ .Values.authService.corsOrigins }}"
- name: AD_SERVER
value: "{{ .Values.authService.activeDirectory.server }}"
- name: AD_BASE_DN
value: "{{ .Values.authService.activeDirectory.baseDN }}"
- name: AD_USER_SEARCH_BASE
value: "{{ .Values.authService.activeDirectory.userSearchBase }}"
- name: AD_BIND_USER
valueFrom:
secretKeyRef:
name: auth-secrets
key: ad-bind-user
- name: AD_BIND_PASSWORD
valueFrom:
secretKeyRef:
name: auth-secrets
key: ad-bind-password
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
---
# Auth Service
apiVersion: v1
kind: Service
metadata:
name: auth-service
namespace: {{ .Values.global.Category }}--{{ .Values.global.Name }}--{{ .Values.global.Type }}
spec:
selector:
app: auth-service
ports:
- port: 8080
targetPort: 8080
type: ClusterIP
---
# Auth Secrets
apiVersion: v1
kind: Secret
metadata:
name: auth-secrets
namespace: {{ .Values.global.Category }}--{{ .Values.global.Name }}--{{ .Values.global.Type }}
type: Opaque
data:
# Base64 encoded values - update these with your actual values
jwt-secret: {{ .Values.authService.jwtSecret | b64enc }}
ad-bind-user: {{ .Values.authService.activeDirectory.bindUser | b64enc }}
ad-bind-password: {{ .Values.authService.activeDirectory.bindPassword | b64enc }}
---
# Traefik ForwardAuth Middleware
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: auth-forward
namespace: {{ .Values.global.Category }}--{{ .Values.global.Name }}--{{ .Values.global.Type }}
spec:
forwardAuth:
address: http://auth-service.{{ .Values.global.Category }}--{{ .Values.global.Name }}--{{ .Values.global.Type }}.svc.cluster.local:8080/auth/verify
authResponseHeaders:
- "X-Auth-User"
- "X-Auth-Email"
- "X-Auth-Groups"
- "X-Auth-Display-Name"
authRequestHeaders:
- "X-Forwarded-Proto"
- "X-Forwarded-Host"
- "X-Forwarded-Uri"
- "X-Original-URL"
---
# Traefik IngressRoute for Auth Service
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: auth-service-route
namespace: {{ .Values.global.Category }}--{{ .Values.global.Name }}--{{ .Values.global.Type }}
spec:
entryPoints:
- websecure
routes:
- match: Host(`{{ .Values.authService.domain }}`)
kind: Rule
services:
- name: auth-service
port: 8080
tls:
certResolver: letsencrypt
---
# Protected API with ForwardAuth
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: arti-api-protected
namespace: {{ .Values.global.Category }}--{{ .Values.global.Name }}--{{ .Values.global.Type }}
spec:
entryPoints:
- websecure
routes:
# Public endpoints (no auth required)
- match: Host(`{{ .Values.global.Api.Url }}`) && (Path(`/`) || Path(`/health`))
kind: Rule
priority: 1000
services:
- name: api
port: 8000
# Protected endpoints (require authentication)
- match: Host(`{{ .Values.global.Api.Url }}`)
kind: Rule
priority: 500
services:
- name: api
port: 8000
middlewares:
- name: auth-forward
tls:
certResolver: letsencrypt
---
# Multi-domain Auth Configuration
# This creates ForwardAuth protection for any subdomain under your domain
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: multi-domain-auth
namespace: {{ .Values.global.Category }}--{{ .Values.global.Name }}--{{ .Values.global.Type }}
spec:
entryPoints:
- websecure
routes:
# Protect all subdomains except the auth service itself
- match: HostRegexp(`{subdomain:[a-zA-Z0-9-]+}.{{ .Values.authService.baseDomain }}`) && !Host(`{{ .Values.authService.domain }}`)
kind: Rule
priority: 100
middlewares:
- name: auth-forward
services:
- name: upstream-service-selector
port: 80
tls:
certResolver: letsencrypt
domains:
- main: "{{ .Values.authService.baseDomain }}"
sans:
- "*.{{ .Values.authService.baseDomain }}"
---
# Wildcard certificate for all subdomains
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-cert
namespace: {{ .Values.global.Category }}--{{ .Values.global.Name }}--{{ .Values.global.Type }}
spec:
secretName: wildcard-tls
issuerRef:
name: letsencrypt
kind: ClusterIssuer
commonName: "*.{{ .Values.authService.baseDomain }}"
dnsNames:
- "{{ .Values.authService.baseDomain }}"
- "*.{{ .Values.authService.baseDomain }}"

View File

@@ -0,0 +1,30 @@
# Helm values for Authentication Service
authService:
image: "your-registry/auth-service"
tag: "1.0.0"
domain: "auth.aipice.lan"
baseDomain: "aipice.lan" # Base domain for wildcard certificates
# Allowed domains for cross-domain authentication
allowedDomains: "aipice.lan,yourdomain.com"
corsOrigins: "https://*.aipice.lan,https://*.yourdomain.com"
# JWT Configuration
jwtSecret: "your-super-secret-jwt-key-change-this-in-production"
# Active Directory Configuration
activeDirectory:
server: "ldap://your-ad-server.yourdomain.com"
baseDN: "DC=yourdomain,DC=com"
userSearchBase: "CN=Users,DC=yourdomain,DC=com"
bindUser: "CN=ServiceAccount,CN=Users,DC=yourdomain,DC=com"
bindPassword: "your-service-account-password"
# Existing global configuration
global:
Category: "infrastructure"
Name: "artifactory"
Type: "service"
Api:
Url: "api.yourdomain.com"

View File

@@ -0,0 +1,78 @@
# ✅ Project organization
## **📁 Clean Project Structure**
```
auth-service/
├── 📄 .drone.jsonnet # Pipeline entry point
├── 📄 app.py # Application code
├── 📄 Dockerfile # Container definition
├── 📄 requirements.txt # Dependencies
├── 📄 PROJECT-STRUCTURE.md # Structure overview
├── 📄 manage-secrets.sh # Application utility
├── 🗂️ templates/ # Application templates
└── 🗂️ pipeline/ # 🎯 ALL pipeline files
```
### **🧩 Modular Pipeline Configuration**
- **✅ Jsonnet-based**: `.drone.jsonnet` imports from `pipeline/`
- **✅ Shared components**: `common.libsonnet` for reusable steps
- **✅ Build logic**: `build-steps.libsonnet` for external Buildah - docker image compilation
- **✅ Import system**: Root entry point loads modular components
### **🛠️ Complete Pipeline Ecosystem**
- **📋 Management scripts**: Build, deploy, update operations
- **⚙️ Kubernetes resources**: RBAC, deployments, configurations
- **📚 Documentation**: Guides, analysis, troubleshooting
- **🔄 Alternative configs**: Multiple pipeline variants for reference
## 🚀 **Benefits Achieved**
### **🎯 Organization**
- **Separation of concerns**: Application vs pipeline code
- **Single location**: All CI/CD files in one folder
- **Clear structure**: Easy to navigate and maintain
### **🔄 Modularity**
- **Reusable components**: Common steps shared across configurations
- **Easy customization**: Modify specific parts without affecting others
- **Version control**: Track changes to pipeline components independently
### **📝 Maintainability**
- **Reduced complexity**: Clean root directory
- **Better documentation**: Organized guides and references
- **Operational scripts**: Complete management toolset
## 📋 **Usage Patterns**
### **Development Workflow**
```bash
# Edit pipeline configuration
vim pipeline/common.libsonnet pipeline/build-steps.libsonnet
# Test locally
jsonnet .drone.jsonnet
# Deploy changes
git add . && git commit -m "Update pipeline" && git push
```
### **Operations Workflow**
```bash
# Check system status
pipeline/manage-external-buildah.sh status
# Deploy/redeploy system
pipeline/deploy-external-buildah.sh
# Update after infrastructure changes
pipeline/update-buildah-pod.sh
```
## 🎉 **Next Steps**
1. **✅ Ready to use**: Pipeline triggers automatically on push
2. **🔧 Customize**: Modify `pipeline/*.libsonnet` for specific needs
3. **📈 Scale**: Create environment-specific configurations
4. **🚀 Extend**: Add new build targets or deployment stages
The project is now **clean, organized, and ready for production use** with a **modular, maintainable pipeline system**! 🎯

View File

@@ -0,0 +1,115 @@
# 📁 Project Structure - Auth Service
```
auth-service/
├── 📄 .drone.jsonnet # Main pipeline entry point (imports from pipeline/)
├── 📄 app.py # Main application code
├── 📄 Dockerfile # Container build definition
├── 📄 requirements.txt # Python dependencies
├── 📄 README.md # Application documentation
└── 📁 pipeline/ # 🎯 CI/CD Pipeline Configuration
├── 🔧 Jsonnet Configuration
│ ├── common.libsonnet # Shared pipeline components
│ ├── build-steps.libsonnet # Build-specific logic
│ └── .drone.jsonnet # Original pipeline config (moved here)
├── 🚀 Management Scripts
│ ├── manage-external-buildah.sh # Buildah service management
│ ├── update-buildah-pod.sh # Auto-update pod references
│ ├── deploy-external-buildah.sh # Complete system deployment
│ └── convert-to-jsonnet.sh # YAML to Jsonnet migration
├── ⚙️ Kubernetes Resources
│ ├── buildah-external-deployment.yaml # External Buildah service
│ ├── buildah-rbac.yaml # Buildah RBAC permissions
│ ├── drone-build-rbac.yaml # Drone build permissions
│ ├── default-sa-binding.yaml # Service account bindings
│ ├── drone-configmap-updated.yaml # Drone server config
│ └── kubernetes-auth.yaml # Auth service deployment
├── 📋 Alternative Configs
│ ├── .drone.yml.backup # Original YAML backup
│ ├── .drone.yml.external-buildah # Basic external build
│ ├── .drone.yml.external-buildah-advanced # Advanced build
│ ├── .drone.yml.external-buildah-production # Production build
│ ├── .drone.yml.buildah-privileged # Privileged container attempts
│ ├── .drone.yml.img-alternative # img builder variant
│ ├── .drone.yml.nerdctl-alternative # nerdctl builder variant
│ └── values-example.yaml # Kubernetes deployment values
└── 📚 Documentation
├── PIPELINE-README.md # Pipeline folder overview
├── EXTERNAL-BUILDAH-SYSTEM.md # External build system guide
├── JSONNET-GUIDE.md # Jsonnet usage documentation
├── DRONE-SETUP.md # Drone setup instructions
├── GIT-WEBHOOK-CONFIG.md # Webhook configuration
└── MULTI-DOMAIN-GUIDE.md # Multi-domain setup
```
## 🎯 **Key Benefits of This Structure**
### **🧩 Organized Layout**
- **Clear separation** of application code vs pipeline configuration
- **Dedicated folder** for all CI/CD related files
- **Easy navigation** and maintenance
### **🔄 Modular Pipeline**
- **Jsonnet-based** configuration with imports
- **Reusable components** in libsonnet files
- **Alternative configurations** for different scenarios
### **🛠️ Complete Toolset**
- **Management scripts** for operational tasks
- **Kubernetes resources** for deployment
- **Documentation** for guidance and troubleshooting
### **📝 Maintainability**
- **Single location** for all pipeline changes
- **Version controlled** configurations and scripts
- **Clear dependencies** and relationships
## 🚀 **Usage Workflows**
### **Development Workflow**
```bash
# 1. Edit pipeline configuration
vim pipeline/common.libsonnet pipeline/build-steps.libsonnet
# 2. Test configuration locally
jsonnet .drone.jsonnet
# 3. Commit and push
git add . && git commit -m "Update pipeline" && git push
```
### **Operations Workflow**
```bash
# Check system status
pipeline/manage-external-buildah.sh status
# Deploy/redeploy system
pipeline/deploy-external-buildah.sh
# Update after pod restarts
pipeline/update-buildah-pod.sh
```
### **Migration Workflow**
```bash
# Convert YAML to Jsonnet (if needed)
pipeline/convert-to-jsonnet.sh
# Use alternative configurations
cp pipeline/.drone.yml.external-buildah-production .drone.yml
```
## 🔗 **Integration Points**
- **Root `.drone.jsonnet`** imports from `pipeline/` folder
- **Scripts reference** local files within pipeline folder
- **Documentation cross-references** between files
- **Kubernetes resources** work together as complete system
This structure provides a **clean, maintainable, and scalable** approach to managing your CI/CD pipeline while keeping application code separate from infrastructure concerns.

View File

@@ -0,0 +1,44 @@
# RBAC Fix Summary
## Problem
```
Error from server (Forbidden): deployments.apps "buildah-external" is forbidden:
User "system:serviceaccount:apps--droneio--prd:default" cannot patch resource "deployments/scale"
in API group "apps" in the namespace "apps--droneio--prd"
```
## Root Cause
The `default` service account in the `apps--droneio--prd` namespace was bound to the `drone-build-role`,
but that role didn't have permissions to scale deployments.
## Solution Applied
Updated the `drone-build-role` to include:
### NEW Permissions Added:
- `deployments.apps` with verbs: `[get, list, watch]`
- `deployments.apps/scale` with verbs: `[get, update, patch]`
- Enhanced `pods` permissions with verbs: `[get, list, watch, create, delete]`
### Verification:
```bash
kubectl auth can-i patch deployments/scale --as=system:serviceaccount:apps--droneio--prd:default -n apps--droneio--prd
# Result: yes ✅
kubectl auth can-i get deployments --as=system:serviceaccount:apps--droneio--prd:default -n apps--droneio--prd
# Result: yes ✅
```
## Status
**RBAC PERMISSIONS FIXED**
The Drone builds can now:
- Scale the `buildah-external` deployment up from 0→1 (acquire build lock)
- Scale the `buildah-external` deployment down from 1→0 (release build lock)
- Monitor pod status and wait for readiness
- Execute build commands in the Buildah pod
## Next Steps
1. Repository needs to be **activated in Drone UI** at https://drone.aipice.local
2. Once activated, the sophisticated Jsonnet pipeline with replica-based locking will work perfectly
The atomic build locking system is now ready to prevent concurrent builds! 🚀

View File

@@ -0,0 +1,285 @@
# Authentication Service with Active Directory Integration
This authentication service provides JWT-based authentication with Active Directory integration and Traefik ForwardAuth support for Kubernetes environments.
## Features
- 🔐 **Active Directory Authentication**: Validates credentials against your AD server
- 🎫 **JWT Tokens**: Secure token-based authentication with configurable expiration
- 🍪 **Cookie & Local Storage**: Tokens stored securely in HTTP-only cookies and locally
- 🚀 **Traefik Integration**: ForwardAuth middleware for seamless Kubernetes access control
- 📱 **Responsive UI**: Clean, modern login interface
- 🔒 **Security Headers**: Proper CORS, security headers, and token validation
## Architecture
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ User Browser │───▶│ Auth Service │───▶│ Active Directory│
│ │ │ │ │ │
│ 1. Login Form │ │ 2. Validate AD │ │ 3. LDAP Auth │
│ 4. Store Token │◀───│ Create JWT │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ Protected API │◀───│ Traefik Forward │
│ │ │ Auth Middleware │
│ 5. Access with │ │ 6. Validate JWT │
│ JWT Token │ │ │
└─────────────────┘ └──────────────────┘
```
## Quick Start
To build image, let's drone io work! As we use drone.io simply push git repository
### 1. Configure Active Directory
Update the `values.yaml` file with your AD configuration:
```yaml
authService:
activeDirectory:
server: "ldap://your-ad-server.yourdomain.com"
baseDN: "DC=yourdomain,DC=com"
userSearchBase: "CN=Users,DC=yourdomain,DC=com"
bindUser: "CN=ServiceAccount,CN=Users,DC=yourdomain,DC=com"
bindPassword: "your-service-account-password"
```
### 3. Configure Traefik ForwardAuth
The service automatically creates a ForwardAuth middleware that:
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: auth-forward
spec:
forwardAuth:
address: http://auth-service:8080/auth/verify
authResponseHeaders:
- "X-Auth-User"
- "X-Auth-Email"
- "X-Auth-Groups"
- "X-Auth-Display-Name"
```
### 4. Protect Your Services
Add the ForwardAuth middleware to any IngressRoute:
```yaml
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: protected-service
spec:
routes:
- match: Host(`api.yourdomain.com`)
kind: Rule
services:
- name: your-api-service
port: 8000
middlewares:
- name: auth-forward # This protects the entire service
```
## How It Works
### Authentication Flow
1. **User visits protected resource** → Traefik ForwardAuth redirects to login
2. **User enters AD credentials** → Service validates against Active Directory
3. **JWT token created** → Stored in HTTP-only cookie + localStorage
4. **Subsequent requests** → Traefik validates JWT via ForwardAuth
5. **Access granted** → User headers passed to backend service
### Token Storage
The system uses a dual-storage approach:
- **HTTP-only Cookie**: Secure, automatic transmission, protected from XSS
- **localStorage**: Available to JavaScript for SPA applications
### Security Features
-**LDAP over TLS** support for secure AD communication
-**JWT token expiration** with configurable timeouts
-**HTTP-only cookies** prevent XSS token theft
-**Secure headers** for production deployment
-**CORS protection** with configurable origins
## API Endpoints
### Authentication Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/` | GET | Login page (HTML) |
| `/dashboard` | GET | Dashboard page (HTML) |
| `/auth/login` | POST | Authenticate user |
| `/auth/verify` | POST | Verify JWT token (ForwardAuth) |
| `/auth/logout` | GET | Logout user |
| `/auth/user` | GET | Get current user info |
| `/health` | GET | Health check |
### ForwardAuth Integration
When Traefik calls `/auth/verify`, the service:
1. **Checks for token** in Authorization header or cookies
2. **Validates JWT** signature and expiration
3. **Returns user headers** for backend services:
- `X-Auth-User`: Username
- `X-Auth-Email`: User email
- `X-Auth-Groups`: AD group memberships
- `X-Auth-Display-Name`: User's display name
## Configuration
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `JWT_SECRET` | Secret key for JWT signing | (required) |
| `TOKEN_EXPIRE_HOURS` | Token expiration in hours | 8 |
| `AD_SERVER` | LDAP server URL | (required) |
| `AD_BASE_DN` | Base DN for AD | (required) |
| `AD_USER_SEARCH_BASE` | User search base | (required) |
| `AD_BIND_USER` | Service account for LDAP | (optional) |
| `AD_BIND_PASSWORD` | Service account password | (optional) |
### Kubernetes Secrets
Create the required secrets:
```bash
kubectl create secret generic auth-secrets \
--from-literal=jwt-secret="your-super-secret-key" \
--from-literal=ad-bind-user="CN=ServiceAccount,CN=Users,DC=yourdomain,DC=com" \
--from-literal=ad-bind-password="your-service-password"
```
## Advanced Usage
### Custom Group-Based Access
The service passes AD group memberships in the `X-Auth-Groups` header. You can use this in your backend services:
```python
# In your FastAPI backend
from fastapi import Header
def check_admin_access(x_auth_groups: str = Header(None)):
groups = x_auth_groups.split(',') if x_auth_groups else []
if 'CN=Admins,CN=Groups,DC=yourdomain,DC=com' not in groups:
raise HTTPException(status_code=403, detail="Admin access required")
```
### Multiple Protection Levels
You can create different ForwardAuth middlewares for different access levels:
```yaml
# Admin-only middleware
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: admin-auth
spec:
forwardAuth:
address: http://auth-service:8080/auth/verify-admin
authResponseHeaders:
- "X-Auth-User"
- "X-Auth-Groups"
```
### Token Refresh
The service automatically handles token refresh. Configure shorter expiration times and implement refresh logic in your frontend:
```javascript
// Check token expiration
const token = localStorage.getItem('auth_token');
const payload = JSON.parse(atob(token.split('.')[1]));
const expiry = new Date(payload.exp * 1000);
if (expiry < new Date()) {
// Redirect to login for refresh
window.location.href = '/auth/login';
}
```
## Monitoring
### Health Checks
The service includes health check endpoints:
```bash
curl http://auth-service:8080/health
```
### Logging
The service logs authentication attempts and failures:
```
INFO: Successfully authenticated user: john.doe
ERROR: Authentication failed for user: invalid.user
ERROR: LDAP connection failed: timeout
```
## Security Considerations
1. **Use HTTPS**: Always deploy with TLS certificates
2. **Secure JWT Secret**: Use a strong, unique JWT secret
3. **Network Security**: Restrict access to AD servers
4. **Token Expiration**: Use reasonable token expiration times
5. **Service Account**: Use a dedicated AD service account with minimal permissions
6. **Audit Logs**: Monitor authentication logs for suspicious activity
## Troubleshooting
### Common Issues
1. **LDAP Connection Failed**
- Check AD server connectivity
- Verify LDAP URL format
- Check firewall rules
2. **Authentication Failed**
- Verify AD credentials
- Check user search base DN
- Confirm user exists in specified OU
3. **ForwardAuth Not Working**
- Verify Traefik middleware configuration
- Check service connectivity
- Review Traefik logs
4. **Token Issues**
- Verify JWT secret consistency
- Check token expiration
- Validate cookie settings
### Debug Mode
Enable debug logging by setting log level:
```yaml
env:
- name: LOG_LEVEL
value: "DEBUG"
```
This will provide detailed authentication flow logs.
# Trigger build with Jsonnet
# Webhook test mer. 01 oct. 2025 17:11:01 CEST
# DNS fix test mer. 01 oct. 2025 17:25:00 CEST
# RBAC fix test jeu. 02 oct. 2025 09:06:56 CEST

View File

@@ -0,0 +1,86 @@
# 🚀 Versioning & Docker Registry Setup Complete!
## ✅ What's Implemented
### 1. Dynamic Versioning from `version.conf`
Your pipeline now reads versioning from `version.conf`:
```bash
BASE_VERSION=1.0
DOCKER_REPO=hexah/auth-service
```
### 2. Generated Docker Tags
Images are now tagged as: `<DOCKER_REPO>:<BASE_VERSION>.<DRONE_BUILD_NUMBER>`
**Examples:**
- `hexah/auth-service:1.0.123` (build #123)
- `ghcr.io/username/auth-service:1.0.456` (build #456)
### 3. Docker Registry Authentication
Added support for private registries with these secrets:
- `docker_username` - Registry username
- `docker_password` - Registry password/token
- `docker_registry` - Registry URL (docker.io, ghcr.io, etc.)
### 4. Pipeline Flow
1. **Clone** - Get source code
2. **Read Version** - Parse `version.conf`
3. **Test** - Validate and show planned Docker tag
4. **Build** - External Buildah with replica locking + versioned tag
5. **Push** - Authenticated push to registry (main/master only)
6. **Scale Down** - Release build lock
## 🔧 Setup Instructions
### For Docker Hub:
```bash
# In Drone UI → Repository → Settings → Secrets:
docker_username = your-dockerhub-username
docker_password = your-dockerhub-password
docker_registry = docker.io
# In version.conf:
DOCKER_REPO=yourusername/auth-service
```
### For GitHub Container Registry:
```bash
# Secrets:
docker_username = your-github-username
docker_password = ghp_your-github-token
docker_registry = ghcr.io
# In version.conf:
DOCKER_REPO=ghcr.io/yourusername/auth-service
```
### For Private Harbor/Registry:
```bash
# Secrets:
docker_username = harbor-username
docker_password = harbor-password
docker_registry = harbor.example.com
# In version.conf:
DOCKER_REPO=harbor.example.com/project/auth-service
```
## 🎯 Next Steps
1. **Activate Repository** in Drone UI at https://drone.aipice.local
2. **Set Docker Secrets** in Repository → Settings → Secrets
3. **Update version.conf** with your registry details
4. **Push to main/master** to trigger build + push
## 💫 Advanced Features Ready
-**Atomic Build Locking** (replica scaling 0→1→0)
-**Modular Jsonnet Configuration** with imports
-**External Buildah** with privileged builds
-**Graceful Termination** (2s vs 30s)
-**RBAC Permissions** for deployment scaling
-**Dynamic Versioning** from config file
-**Multi-Registry Support** with authentication
-**Branch-based Pushing** (only main/master)
**Your sophisticated CI/CD pipeline is now complete!** 🎉

View File

@@ -0,0 +1,40 @@
#!/bin/bash
# Webhook troubleshooting guide
echo "🔍 WEBHOOK TROUBLESHOOTING GUIDE"
echo "================================="
echo ""
echo "Your Jsonnet configuration is PERFECT! The issue is webhook delivery."
echo "Drone server is not receiving push notifications from Gitea."
echo ""
echo "TO FIX THIS:"
echo ""
echo "1. ACCESS DRONE WEB UI:"
echo " - Open https://drone.aipice.local in your browser"
echo " - Login with your Gitea credentials"
echo " - Check if AIPICE/auth-service repository is listed and ACTIVE"
echo ""
echo "2. IF REPOSITORY NOT LISTED:"
echo " - Click 'SYNC' to refresh repository list from Gitea"
echo " - Find AIPICE/auth-service and click 'ACTIVATE'"
echo ""
echo "3. IF REPOSITORY LISTED BUT INACTIVE:"
echo " - Click on the repository"
echo " - Click 'ACTIVATE' button"
echo " - This will create/update the webhook in Gitea"
echo ""
echo "4. VERIFY WEBHOOK IN GITEA:"
echo " - Go to https://gitea.aipice.local/AIPICE/auth-service/settings/hooks"
echo " - You should see a webhook pointing to https://drone.aipice.local/hook"
echo " - Test the webhook by clicking 'Test Delivery'"
echo ""
echo "5. CHECK NETWORK CONNECTIVITY:"
echo " - Ensure Gitea can reach Drone webhook endpoint"
echo " - Check firewall rules between Gitea and Drone"
echo ""
echo "ALTERNATIVE: Manual trigger for testing:"
echo " - In Drone web UI, go to repository"
echo " - Click 'NEW BUILD' button"
echo " - This will test your Jsonnet config without webhook"
echo ""
echo "Your current .drone.jsonnet will work perfectly once webhook is fixed!"

View File

@@ -0,0 +1,298 @@
from fastapi import FastAPI, HTTPException, Request, Response, Depends, Form
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from fastapi.middleware.cors import CORSMiddleware
import jwt
import bcrypt
import ldap3
from datetime import datetime, timedelta
import os
import logging
from typing import Optional
from pydantic import BaseModel
"""
This is an authentication service using FastAPI that verifies user credentials against Active Directory (AD)
and issues JWT tokens for authenticated users. It supports cross-domain authentication and is designed to work
with Traefik as a reverse proxy.
This will be front end for apps
"""
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Configuration
JWT_SECRET = os.getenv("JWT_SECRET", "your-super-secret-key-change-this")
JWT_ALGORITHM = "HS256"
TOKEN_EXPIRE_HOURS = int(os.getenv("TOKEN_EXPIRE_HOURS", "8"))
# Domain configuration for cross-domain auth
ALLOWED_DOMAINS = os.getenv("ALLOWED_DOMAINS", "domain.tld").split(",")
AUTH_DOMAIN = os.getenv("AUTH_DOMAIN", "auth.domain.tld")
CORS_ORIGINS = os.getenv("CORS_ORIGINS", "https://*.domain.tld").split(",")
app = FastAPI(title="Authentication Service", description="AD Authentication with JWT tokens")
# Add CORS middleware for cross-domain authentication
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # In production, specify exact domains
allow_credentials=True,
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["*"],
)
# Domain configuration for cross-domain auth
ALLOWED_DOMAINS = os.getenv("ALLOWED_DOMAINS", "domain.tld").split(",")
AUTH_DOMAIN = os.getenv("AUTH_DOMAIN", "auth.domain.tld")
CORS_ORIGINS = os.getenv("CORS_ORIGINS", "https://*.domain.tld").split(",")
# Active Directory Configuration
AD_SERVER = os.getenv("AD_SERVER", "ldap://your-ad-server.com")
AD_BASE_DN = os.getenv("AD_BASE_DN", "DC=yourdomain,DC=com")
AD_USER_SEARCH_BASE = os.getenv("AD_USER_SEARCH_BASE", "CN=Users,DC=yourdomain,DC=com")
AD_BIND_USER = os.getenv("AD_BIND_USER", "ReadUser") # Service account for LDAP bind
AD_BIND_PASSWORD = os.getenv("AD_BIND_PASSWORD", "")
# Setup templates and static files
templates = Jinja2Templates(directory="templates")
app.mount("/static", StaticFiles(directory="static"), name="static")
security = HTTPBearer(auto_error=False)
class LoginRequest(BaseModel):
username: str
password: str
class TokenData(BaseModel):
username: str
email: Optional[str] = None
groups: list = []
exp: datetime
def verify_ad_credentials(username: str, password: str) -> dict:
"""
Verify credentials against Active Directory
Returns user info if valid, raises exception if invalid
"""
try:
# Connect to AD server
server = ldap3.Server(AD_SERVER, get_info=ldap3.ALL)
# If we have a service account, use it for initial bind
if AD_BIND_USER and AD_BIND_PASSWORD:
conn = ldap3.Connection(server, AD_BIND_USER, AD_BIND_PASSWORD, auto_bind=True)
else:
conn = ldap3.Connection(server)
# Search for the user
search_filter = f"(sAMAccountName={username})"
conn.search(AD_USER_SEARCH_BASE, search_filter, attributes=['mail', 'memberOf', 'displayName'])
if not conn.entries:
raise HTTPException(status_code=401, detail="Invalid credentials")
user_dn = conn.entries[0].entry_dn
user_info = {
'username': username,
'email': str(conn.entries[0].mail) if conn.entries[0].mail else '',
'display_name': str(conn.entries[0].displayName) if conn.entries[0].displayName else username,
'groups': [str(group) for group in conn.entries[0].memberOf] if conn.entries[0].memberOf else []
}
# Now try to bind with user credentials to verify password
user_conn = ldap3.Connection(server, user_dn, password)
if not user_conn.bind():
raise HTTPException(status_code=401, detail="Invalid credentials")
user_conn.unbind()
conn.unbind()
logger.info(f"Successfully authenticated user: {username}")
return user_info
except ldap3.core.exceptions.LDAPException as e:
logger.error(f"LDAP error: {str(e)}")
raise HTTPException(status_code=500, detail="Authentication service error")
except Exception as e:
logger.error(f"Authentication error: {str(e)}")
raise HTTPException(status_code=401, detail="Invalid credentials")
def create_jwt_token(user_info: dict) -> str:
"""Create JWT token with user information"""
expire = datetime.utcnow() + timedelta(hours=TOKEN_EXPIRE_HOURS)
payload = {
"username": user_info["username"],
"email": user_info["email"],
"display_name": user_info["display_name"],
"groups": user_info["groups"],
"exp": expire,
"iat": datetime.utcnow()
}
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
def verify_jwt_token(token: str) -> dict:
"""Verify and decode JWT token"""
try:
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(status_code=401, detail="Token expired")
except jwt.JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
@app.get("/", response_class=HTMLResponse)
async def login_page(request: Request):
"""Serve the login page"""
return templates.TemplateResponse("login.html", {"request": request})
@app.get("/dashboard", response_class=HTMLResponse)
async def dashboard(request: Request):
"""Serve the dashboard page"""
return templates.TemplateResponse("dashboard.html", {"request": request})
@app.post("/auth/login")
async def login(username: str = Form(...), password: str = Form(...)):
"""Authenticate user and return JWT token"""
try:
# Verify credentials against AD
user_info = verify_ad_credentials(username, password)
# Create JWT token
token = create_jwt_token(user_info)
# Create response with token in cookie
response = JSONResponse({
"success": True,
"message": "Login successful",
"user": {
"username": user_info["username"],
"email": user_info["email"],
"display_name": user_info["display_name"]
}
})
# Set HTTP-only cookie with token (works across subdomains)
response.set_cookie(
key="auth_token",
value=token,
domain=f".{ALLOWED_DOMAINS[0]}", # Set for all subdomains
httponly=True,
secure=True, # Use HTTPS in production
samesite="lax",
max_age=TOKEN_EXPIRE_HOURS * 3600
)
# Also return token for local storage (optional)
response.headers["X-Auth-Token"] = token
return response
except HTTPException as e:
return JSONResponse(
status_code=e.status_code,
content={"success": False, "message": e.detail}
)
@app.post("/auth/verify")
async def verify_token(request: Request, credentials: HTTPAuthorizationCredentials = Depends(security)):
"""Verify token endpoint for Traefik ForwardAuth"""
token = None
# Get original request information from Traefik headers
original_host = request.headers.get("X-Forwarded-Host", request.headers.get("Host", ""))
original_proto = request.headers.get("X-Forwarded-Proto", "https")
original_uri = request.headers.get("X-Forwarded-Uri", "/")
original_url = request.headers.get("X-Original-URL", f"{original_proto}://{original_host}{original_uri}")
# Check Authorization header first
if credentials:
token = credentials.credentials
else:
# Check cookie
token = request.cookies.get("auth_token")
if not token:
# Redirect to auth service with return URL
auth_url = f"https://{AUTH_DOMAIN}/?return_url={original_url}"
logger.info(f"No token found, redirecting to: {auth_url}")
# Return 401 with redirect location for Traefik
response = JSONResponse(
status_code=401,
content={"error": "Authentication required", "auth_url": auth_url}
)
response.headers["Location"] = auth_url
return response
try:
payload = verify_jwt_token(token)
# Check if the request is from an allowed domain
is_allowed_domain = any(domain in original_host for domain in ALLOWED_DOMAINS)
if not is_allowed_domain:
logger.warning(f"Access denied for domain: {original_host}")
raise HTTPException(status_code=403, detail="Domain not allowed")
# Return user info in headers for Traefik
headers = {
"X-Auth-User": payload["username"],
"X-Auth-Email": payload["email"],
"X-Auth-Groups": ",".join(payload["groups"]),
"X-Auth-Display-Name": payload["display_name"],
"X-Auth-Domain": original_host
}
logger.info(f"Authentication successful for {payload['username']} accessing {original_host}")
return JSONResponse(
content={"valid": True, "user": payload["username"], "domain": original_host},
headers=headers
)
except HTTPException as e:
# On token validation failure, redirect to auth service
auth_url = f"https://{AUTH_DOMAIN}/?return_url={original_url}"
logger.warning(f"Token validation failed: {e.detail}, redirecting to: {auth_url}")
response = JSONResponse(
status_code=401,
content={"error": e.detail, "auth_url": auth_url}
)
response.headers["Location"] = auth_url
return response
@app.get("/auth/logout")
async def logout():
"""Logout endpoint"""
response = JSONResponse({"message": "Logged out successfully"})
response.delete_cookie("auth_token")
return response
@app.get("/auth/user")
async def get_current_user(request: Request):
"""Get current user info from token"""
token = request.cookies.get("auth_token")
if not token:
raise HTTPException(status_code=401, detail="Not authenticated")
payload = verify_jwt_token(token)
return {
"username": payload["username"],
"email": payload["email"],
"display_name": payload["display_name"],
"groups": payload["groups"]
}
# liveness probe entry point
@app.get("/health")
async def health_check():
"""Health check endpoint"""
return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()}
# Main entry point
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8080)

View File

@@ -0,0 +1,42 @@
apiVersion: v1
data:
Corefile: |
.:53 {
errors
health
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
hosts /etc/coredns/NodeHosts {
ttl 60
reload 15s
fallthrough
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
import /etc/coredns/custom/*.override
}
import /etc/coredns/custom/*.server
NodeHosts: |
192.168.100.214 srv-001
kind: ConfigMap
metadata:
annotations:
objectset.rio.cattle.io/applied: H4sIAAAAAAAA/4yQwWrzMBCEX0Xs2fEf20nsX9BDybH02lMva2kdq1Z2g6SkBJN3L8IUCiVtbyNGOzvfzoAn90IhOmHQcKmgAIsJQc+wl0CD8wQaSr1t1PzKSilFIUiIix4JfRoXHQjtdZHTuafAlCgq488xUSi9wK2AybEFDXvhwR2e8QQFHCnh50ZkloTJCcf8lP6NTIqUyuCkNJiSp9LJP5czoLjryztTWB0uE2iYmvjFuVSFenJsHx6tFf41gvGY6Y0Eshz/9D2e0OSZfIJVvMZExwzusSf/I9SIcQQNvaG6a+r/XVdV7abBddPtsN9W66Eedi0N7aberM22zaHf6t0tcPsIAAD//8Ix+PfoAQAA
objectset.rio.cattle.io/id: ""
objectset.rio.cattle.io/owner-gvk: k3s.cattle.io/v1, Kind=Addon
objectset.rio.cattle.io/owner-name: coredns
objectset.rio.cattle.io/owner-namespace: kube-system
creationTimestamp: "2024-10-09T16:17:37Z"
labels:
objectset.rio.cattle.io/hash: bce283298811743a0386ab510f2f67ef74240c57
name: coredns
namespace: kube-system
resourceVersion: "11107894"
uid: d6c19736-636f-481c-8155-efe5adbd465a

View File

@@ -0,0 +1,36 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: coredns
namespace: kube-system
data:
Corefile: |
.:53 {
errors
health
ready
kubernetes cluster.local in-addr.arpa ip6.arpa {
pods insecure
fallthrough in-addr.arpa ip6.arpa
}
hosts /etc/coredns/NodeHosts {
ttl 60
reload 15s
fallthrough
}
prometheus :9153
forward . /etc/resolv.conf
cache 30
loop
reload
loadbalance
import /etc/coredns/custom/*.override
}
aipice.local:53 {
errors
cache 30
forward . 192.168.100.241
}
import /etc/coredns/custom/*.server
NodeHosts: |
192.168.100.214 srv-001

View File

@@ -0,0 +1,45 @@
#!/bin/bash
# Debug script to check Drone configuration files
echo "🔍 DRONE CONFIGURATION DEBUG"
echo "============================"
echo ""
echo "1. Current commit hash:"
git rev-parse HEAD
echo ""
echo "2. Files in repository root:"
ls -la .drone*
echo ""
echo "3. Testing .drone.jsonnet compilation:"
if jsonnet .drone.jsonnet >/dev/null 2>&1; then
echo "✅ .drone.jsonnet compiles successfully"
echo "Generated output (first 10 lines):"
jsonnet .drone.jsonnet | head -10
else
echo "❌ .drone.jsonnet compilation failed"
jsonnet .drone.jsonnet
fi
echo ""
echo "4. Checking Drone server configuration:"
echo "DRONE_JSONNET_ENABLED: $(kubectl get configmap drone -n apps--droneio--prd -o jsonpath='{.data.DRONE_JSONNET_ENABLED}')"
echo "DRONE_JSONNET_IMPORT_PATHS: $(kubectl get configmap drone -n apps--droneio--prd -o jsonpath='{.data.DRONE_JSONNET_IMPORT_PATHS}')"
echo ""
echo "5. Recent Drone logs (last 5):"
kubectl logs droneio-7686bf675f-scdvh -n apps--droneio--prd --tail=5
echo ""
echo "6. Testing webhook connectivity from Gitea to Drone:"
kubectl exec gitea-app-dep-6db56f9d88-g7qlb -n cluster-infra--gitea--prd -c gitea -- curl -k -s -o /dev/null -w "HTTP Status: %{http_code}" https://drone.aipice.local/hook --connect-timeout 5
echo ""
echo ""
echo "🎯 NEXT STEPS:"
echo "If webhook connectivity works but Drone can't find YAML:"
echo "- Check if repository is ACTIVATED in Drone UI"
echo "- Verify .drone.jsonnet is committed to git"
echo "- Try manual build trigger in Drone UI"

View File

@@ -0,0 +1,127 @@
#!/bin/bash
# Drone Secrets Management Script
# Usage: ./manage-secrets.sh
set -e
echo "🔐 Drone Secrets Management"
echo "=========================="
echo
read -p "Enter your TOKEN: " DRONE_TOKEN
# Configuration
DRONE_SERVER="https://drone.aipice.local"
REPO_OWNER="AIPICE"
REPO_NAME="auth-service"
REPO="${REPO_OWNER}/${REPO_NAME}"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# Check if drone CLI is available
if ! command -v drone &> /dev/null; then
echo -e "${YELLOW}📥 Installing Drone CLI...${NC}"
curl -L https://github.com/harness/drone-cli/releases/latest/download/drone_linux_amd64.tar.gz | tar zx
sudo install -t /usr/local/bin drone
echo -e "${GREEN}✅ Drone CLI installed${NC}"
fi
# Check if DRONE_TOKEN is set
if [ -z "$DRONE_TOKEN" ]; then
echo -e "${RED}❌ DRONE_TOKEN environment variable not set${NC}"
echo "Please get your Drone API token from: ${DRONE_SERVER}/account"
echo "Then run: export DRONE_TOKEN=your-token-here"
echo
exit 1
fi
# Configure drone CLI
export DRONE_SERVER
export DRONE_TOKEN
echo "📋 Configuration:"
echo " Server: $DRONE_SERVER"
echo " Repository: $REPO"
echo
# Function to create a secret
create_secret() {
local secret_name=$1
local secret_value=$2
local description=$3
echo -e "${YELLOW}Creating secret: $secret_name${NC}"
if drone secret add --repository "$REPO" --name "$secret_name" --data "$secret_value"; then
echo -e "${GREEN}✅ Secret '$secret_name' created successfully${NC}"
else
echo -e "${RED}❌ Failed to create secret '$secret_name'${NC}"
fi
echo
}
# Function to list secrets
list_secrets() {
echo -e "${YELLOW}📋 Current secrets for $REPO:${NC}"
drone secret ls --repository "$REPO" || echo -e "${RED}❌ Failed to list secrets${NC}"
echo
}
# Main menu
while true; do
echo "🎯 What would you like to do?"
echo "1) List existing secrets"
echo "2) Add Gitea credentials"
echo "3) Add Docker Hub credentials"
echo "4) Add custom secret"
echo "5) Test connection"
echo "6) Exit"
echo
read -p "Choose an option (1-6): " choice
echo
case $choice in
1)
list_secrets
;;
2)
echo "🔑 Adding Gitea credentials..."
read -p "Gitea Username: " gitea_username
read -s -p "Gitea Password/Token: " gitea_password
echo
create_secret "gitea_username" "$gitea_username" "Gitea username for cloning"
create_secret "gitea_password" "$gitea_password" "Gitea password/token for cloning"
;;
3)
echo "🐳 Adding Docker Hub credentials..."
read -p "Docker Hub Username: " docker_username
read -s -p "Docker Hub Password/Token: " docker_password
echo
create_secret "docker_username" "$docker_username" "Docker Hub username"
create_secret "docker_password" "$docker_password" "Docker Hub password/token"
;;
4)
echo "🔐 Adding custom secret..."
read -p "Secret name: " secret_name
read -s -p "Secret value: " secret_value
echo
create_secret "$secret_name" "$secret_value" "Custom secret"
;;
5)
echo "🔍 Testing connection..."
drone info || echo -e "${RED}❌ Connection failed${NC}"
echo
;;
6)
echo "👋 Goodbye!"
break
;;
*)
echo -e "${RED}❌ Invalid option${NC}"
;;
esac
done

View File

@@ -0,0 +1,164 @@
# Drone CI Secrets Configuration
This document explains how to configure secrets in Drone CI for the auth-service pipeline.
## Required Secrets
Configure these secrets in your Drone CI interface at `https://drone.aipice.local`:
### Docker Registry Secrets
```bash
# Docker Hub credentials for pushing images
docker_username: your-docker-username
docker_password: your-docker-password-or-token
```
### Git Secrets (Optional)
```bash
# For creating git tags (if using private repos)
git_username: your-git-username
git_token: your-git-personal-access-token
```
### Notification Secrets (Optional)
```bash
# Webhook URL for build notifications (Slack, Discord, etc.)
webhook_url: https://hooks.slack.com/services/YOUR/WEBHOOK/URL
# Drone API token for deployment notifications
drone_token: your-drone-api-token
```
## Setting Up Secrets in Drone
### Via Drone UI
1. Navigate to `https://drone.aipice.local`
2. Go to your repository settings
3. Click on "Secrets" tab
4. Add each secret with the name and value
### Via Drone CLI
```bash
# Install Drone CLI
curl -L https://github.com/harness/drone-cli/releases/latest/download/drone_linux_amd64.tar.gz | tar zx
sudo install -t /usr/local/bin drone
# Configure Drone CLI
export DRONE_SERVER=https://drone.aipice.local
export DRONE_TOKEN=your-drone-token
# Add secrets
drone secret add --repository your-org/auth-service --name docker_username --data your-docker-username
drone secret add --repository your-org/auth-service --name docker_password --data your-docker-password
```
### Via API
```bash
# Add secret via REST API
curl -X POST https://drone.aipice.local/api/repos/your-org/auth-service/secrets \
-H "Authorization: Bearer your-drone-token" \
-H "Content-Type: application/json" \
-d '{
"name": "docker_username",
"data": "your-docker-username"
}' --insecure
```
## Verifying Configuration
### Test Docker Credentials
```bash
# Test Docker login with your credentials
echo "your-docker-password" | docker login -u your-docker-username --password-stdin
```
### Test Drone Connection
```bash
# Test Drone API access
curl -H "Authorization: Bearer your-drone-token" \
https://drone.aipice.local/api/user \
--insecure
```
## Build Trigger
Once secrets are configured, the pipeline will automatically:
1. **On push to main/master:**
- Build Docker image: `hexah/auth-service:1.0.X` (where X is build number)
- Push to Docker registry
- Create Git tag: `v1.0.X`
- Send notifications (if configured)
2. **On push to other branches:**
- Run tests and validation
- Test Docker build (without pushing)
## Version Pattern
The pipeline uses this versioning scheme:
```
Base Version: 1.0 (defined in version.conf)
Build Number: Drone's automatic build counter
Final Version: 1.0.{BUILD_NUMBER}
Examples:
- First build: 1.0.1
- Second build: 1.0.2
- etc.
```
## Customizing Versions
To change the base version (e.g., for major releases):
1. Edit `version.conf`:
```
BASE_VERSION=2.0
```
2. Next build will create: `2.0.1`, `2.0.2`, etc.
## Troubleshooting
### Build Fails on Docker Push
Check that:
- Docker credentials are correct
- Repository `hexah/auth-service` exists
- Account has push permissions
### SSL Certificate Issues
The pipeline includes `skip_verify: true` for self-signed certificates, but you can also:
```bash
# Add Drone server certificate to trusted store
openssl s_client -connect drone.aipice.local:443 -servername drone.aipice.local < /dev/null 2>/dev/null | openssl x509 -outform PEM > drone.crt
sudo cp drone.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
```
### Git Tag Creation Fails
Ensure the Drone service account has push permissions to the repository.
## Example Build Output
Successful build will show:
```
✓ version: Building version 1.0.15
✓ docker-build: Successfully built hexah/auth-service:1.0.15
✓ git-tag: Created tag v1.0.15
✓ deploy-notification: Notified deployment system
```

View File

@@ -0,0 +1,171 @@
# External Buildah Build System - Updated Documentation
## 🎯 Overview
Updated external build system with dynamic pod discovery and concurrent build protection.
## ✨ **New Features**
### 🔍 **Dynamic Pod Discovery**
- Automatically finds running Buildah pods using labels
- No more hardcoded pod names
- Resilient to pod restarts and recreations
### 🔒 **Concurrent Build Protection**
- Lock file mechanism prevents simultaneous builds
- Automatic cleanup of stale locks (older than 10 minutes)
- Timeout protection (5-minute maximum wait)
- Guaranteed lock release even on build failure
### 🛠️ **Enhanced Management**
- Updated management script with dynamic pod discovery
- Lock management commands
- Better error handling and status reporting
## 📋 **How It Works**
### **Dynamic Pod Discovery**
```bash
BUILDAH_POD=$(kubectl get pods -n apps--droneio--prd -l app=buildah-external --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}')
```
### **Locking Mechanism**
1. **Acquire Lock**: Creates `/workspace/locks/build-${DRONE_BUILD_NUMBER}.lock`
2. **Wait for Lock**: Up to 5 minutes timeout with 5-second intervals
3. **Auto-cleanup**: Removes locks older than 10 minutes
4. **Guaranteed Release**: Cleanup step runs on success OR failure
### **Build Process**
1. Find current Buildah pod dynamically
2. Acquire build lock with timeout
3. Transfer source code
4. Execute build in isolated workspace
5. Retrieve results
6. Clean up workspace and release lock
## 🚀 **Usage**
### **Deploy the System**
```bash
./deploy-external-buildah.sh
```
### **Use Production Configuration**
```bash
cp .drone.yml.external-buildah-production .drone.yml
# OR use the current updated version
git add .drone.yml
git commit -m "Implement dynamic external Buildah build"
git push
```
### **Management Commands**
```bash
# Complete status overview
./manage-external-buildah.sh status
# Lock management
./manage-external-buildah.sh locks list # List current locks
./manage-external-buildah.sh locks clean # Remove old locks
./manage-external-buildah.sh locks clear # Remove ALL locks
# Test functionality
./manage-external-buildah.sh test
# Clean old builds
./manage-external-buildah.sh clean
```
## 🔧 **Configuration Files**
### **Updated Files**
-`.drone.yml` - Updated with dynamic discovery and locking
-`manage-external-buildah.sh` - Enhanced management script
-`buildah-external-deployment.yaml` - External Buildah service
-`buildah-rbac.yaml` - RBAC configuration
### **Key Configuration Elements**
#### **Pod Discovery**
```yaml
- BUILDAH_POD=$(kubectl get pods -n apps--droneio--prd -l app=buildah-external --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}')
```
#### **Lock Management**
```yaml
- LOCK_FILE="/workspace/locks/build-${DRONE_BUILD_NUMBER}.lock"
- timeout=300 # 5 minutes maximum wait
```
#### **Cleanup Step**
```yaml
- name: cleanup-build-lock
when:
status:
- success
- failure
```
## 📊 **Benefits**
### **Reliability**
- ✅ No hardcoded pod names
- ✅ Automatic pod discovery
- ✅ Resilient to restarts
### **Concurrency**
- ✅ Prevents build conflicts
- ✅ Automatic lock cleanup
- ✅ Timeout protection
### **Maintenance**
- ✅ Self-managing system
- ✅ Comprehensive status reporting
- ✅ Easy troubleshooting
## 🎯 **Next Steps**
1. **Test the Updated System**:
```bash
./manage-external-buildah.sh status
```
2. **Commit the Configuration**:
```bash
git add .drone.yml
git commit -m "Add dynamic pod discovery and build locking"
git push
```
3. **Monitor First Build**:
- Watch Drone CI interface for build progress
- Check locks: `./manage-external-buildah.sh locks list`
- Verify cleanup: `./manage-external-buildah.sh status`
## 🔍 **Troubleshooting**
### **No Buildah Pod Found**
```bash
kubectl get pods -n apps--droneio--prd -l app=buildah-external
kubectl apply -f buildah-external-deployment.yaml
```
### **Lock Issues**
```bash
# Clean old locks
./manage-external-buildah.sh locks clean
# Clear all locks (emergency)
./manage-external-buildah.sh locks clear
```
### **Build Failures**
```bash
# Check pod logs
./manage-external-buildah.sh logs
# Check pod details
./manage-external-buildah.sh details
```
The system is now production-ready with robust error handling, dynamic discovery, and concurrent build protection!

View File

@@ -0,0 +1,132 @@
# Git Hosting Service Configuration for Drone CI Webhooks
## For Gitea
Add to your Gitea configuration (`app.ini`):
```ini
[webhook]
# Allow webhooks to internal/private networks
ALLOWED_HOST_LIST = private
# Or specifically allow your Drone server
ALLOWED_HOST_LIST = 192.168.100.214,drone.aipice.local,*.aipice.local
# Skip TLS verification for internal services
SKIP_TLS_VERIFY = true
```
Restart Gitea after configuration changes:
```bash
sudo systemctl restart gitea
# or if using Docker:
docker restart gitea
```
## For GitLab
Add to your GitLab configuration (`gitlab.rb`):
```ruby
# Allow outbound requests to private networks
gitlab_rails['outbound_requests_whitelist'] = [
'192.168.100.0/24',
'10.0.0.0/8',
'172.16.0.0/12'
]
# Or specifically allow your Drone server
gitlab_rails['outbound_requests_whitelist'] = ['192.168.100.214']
# Webhook timeout settings
gitlab_rails['webhook_timeout'] = 30
```
Apply configuration:
```bash
sudo gitlab-ctl reconfigure
```
## For GitHub Enterprise
In the GitHub Enterprise admin settings:
1. Go to **Management Console****Privacy**
2. Under **Private Mode**, configure:
- Allow webhook delivery to private networks: ✅
- Exempt domains: `*.aipice.local`
## Alternative: Use Public Domain
If you can't modify the Git hosting service configuration, make your Drone CI accessible via a public domain:
1. **Setup external access** to Drone CI
2. **Use public domain** like `drone-public.yourdomain.com`
3. **Update webhook URL** in Git repository settings
## Testing Webhook Connectivity
Test if your Git service can reach Drone:
```bash
# From your Git hosting server, test connection:
curl -I https://drone.aipice.local/healthz --insecure
# Expected response:
HTTP/1.1 200 OK
```
## Manual Webhook Configuration
If automatic webhook setup fails, configure manually:
1. **Go to repository settings** in your Git service
2. **Add webhook** with:
- URL: `https://drone.aipice.local/hook?secret=YOUR_SECRET`
- Content Type: `application/json`
- Events: `Push`, `Tag push`, `Pull requests`
- SSL verification: Disabled (for self-signed certs)
## Firewall Configuration
Ensure firewall allows Git service to reach Drone:
```bash
# Allow Git server to reach Drone CI
sudo ufw allow from GIT_SERVER_IP to any port 443
sudo ufw allow from 192.168.100.0/24 to any port 443
```
## Troubleshooting
### Check Git Service Logs
**Gitea:**
```bash
sudo journalctl -u gitea -f
# Look for webhook delivery attempts
```
**GitLab:**
```bash
sudo gitlab-ctl tail gitlab-rails
# Look for outbound request blocks
```
### Check Drone Logs
```bash
# Check if Drone receives webhook calls
kubectl logs -n drone deployment/drone-server | grep webhook
```
### Test Manual Webhook
```bash
# Simulate webhook call from Git service
curl -X POST https://drone.aipice.local/hook?secret=YOUR_SECRET \
-H "Content-Type: application/json" \
-H "X-GitHub-Event: push" \
-d '{"ref":"refs/heads/main"}' \
--insecure
```

View File

@@ -0,0 +1,144 @@
# Graceful Termination Solutions for Buildah Container
## 🎯 **Problem**
`sleep infinity` ignores SIGTERM signals, forcing Kubernetes to wait for SIGKILL timeout (default 30 seconds). This causes:
- ⏳ Slow pod termination
- 💸 Unnecessary resource usage during termination
- 🐌 Slower scaling operations
## ✅ **Solutions Implemented**
### **🥇 Recommended: Signal-Aware Bash Loop**
```bash
command: ["/bin/bash"]
args: ["-c", "trap 'exit 0' TERM; while true; do sleep 30 & wait $!; done"]
```
**Benefits:**
-**Immediate response** to SIGTERM (tested: 2 seconds)
-**Simple implementation** - no external dependencies
-**Compatible** with existing infrastructure
-**Resource efficient** - responsive sleep loops
### **⚙️ Configuration Parameters**
```yaml
terminationGracePeriodSeconds: 5 # Reduced from default 30s
readinessProbe:
exec:
command: ["/bin/bash", "-c", "buildah --version"]
initialDelaySeconds: 5
periodSeconds: 10
```
## 📊 **Performance Comparison**
| Method | Termination Time | Complexity | Resource Usage |
|--------|------------------|------------|----------------|
| `sleep infinity` | 30s (SIGKILL) | Low | High during termination |
| **Signal-aware loop** | **2s** | Low | **Low** |
| Custom entrypoint | 3-5s | Medium | Low |
| Chart override | Variable | High | Low |
## 🔧 **Implementation Options**
### **Option 1: Direct Deployment Update** ⭐
```yaml
command: ["/bin/bash"]
args: ["-c", "trap 'exit 0' TERM; while true; do sleep 30 & wait $!; done"]
terminationGracePeriodSeconds: 5
```
**Use when:** Direct control over deployment YAML
### **Option 2: Chart Override Values**
```yaml
# For Helm chart deployments
buildah-external:
command: ["/bin/bash"]
args: ["-c", "trap 'exit 0' TERM; while true; do sleep 30 & wait $!; done"]
terminationGracePeriodSeconds: 5
```
**Use when:** Deployment managed by Helm charts
### **Option 3: ConfigMap Entrypoint**
```yaml
# More sophisticated signal handling with cleanup
volumeMounts:
- name: entrypoint-script
mountPath: /scripts
volumes:
- name: entrypoint-script
configMap:
name: buildah-entrypoint
```
**Use when:** Need complex termination logic or cleanup
## 🧪 **Validation**
### **Test Graceful Termination**
```bash
pipeline/test-graceful-termination.sh
```
**Validates:**
- ✅ Pod responsiveness during operation
- ✅ Signal handling speed (target: <10s)
- ✅ Clean termination without SIGKILL
- ✅ Proper deployment scaling
### **Test Results**
```
✅ Pod terminated in 2 seconds
🎉 Excellent! Graceful termination completed quickly (≤10s)
📝 Method: Signal-aware bash loop with trap
```
## 🔄 **Integration with Replica Locking**
The signal-aware termination works perfectly with the replica-based locking system:
```bash
# Scale up (acquire lock) - fast startup
kubectl scale deployment buildah-external --replicas=1
kubectl wait --for=condition=ready pod -l app=buildah-external --timeout=60s
# Scale down (release lock) - fast termination
kubectl scale deployment buildah-external --replicas=0
kubectl wait --for=delete pod -l app=buildah-external --timeout=10s # Much faster!
```
## 📋 **Migration Steps**
1. **Update deployment** with signal-aware command
2. **Reduce termination grace period** to 5-10 seconds
3. **Add readiness probe** for build verification
4. **Test termination speed** with validation script
5. **Monitor** build pipeline performance
## 🎯 **Benefits Achieved**
- **🚀 15x faster termination** (30s → 2s)
- **💰 Resource savings** during scaling operations
- **🔧 Better UX** for developers (faster builds)
- **⚡ Responsive scaling** for replica-based locking
- **🛡️ Robust** - handles signals properly
## 🔍 **Monitoring Commands**
```bash
# Check termination grace period
kubectl get pod <pod-name> -o jsonpath='{.spec.terminationGracePeriodSeconds}'
# Monitor termination events
kubectl get events --field-selector involvedObject.name=<pod-name>
# Test signal responsiveness
kubectl exec <pod-name> -- kill -TERM 1
```
This solution provides **optimal performance** while maintaining **simplicity** and **compatibility** with existing infrastructure! 🎉

View File

@@ -0,0 +1,169 @@
# Drone CI Jsonnet Configuration Guide
## ✅ **Jsonnet is Now Enabled!**
Your Drone CI server now supports Jsonnet configurations with the following setup:
### 🔧 **Server Configuration**
The following environment variables have been added to enable Jsonnet:
```yaml
DRONE_JSONNET_ENABLED: "true"
DRONE_STARLARK_ENABLED: "true" # Bonus: Starlark support too
```
### 📁 **File Structure**
```
├── .drone.jsonnet # Main pipeline configuration
├── common.libsonnet # Shared steps and environment
├── build-steps.libsonnet # Build-specific logic
├── .drone.yml.backup # Original YAML (backup)
└── drone-configmap-updated.yaml # Updated server config
```
## 🚀 **How to Use Jsonnet**
### **1. Main Configuration (`.drone.jsonnet`)**
- Entry point for your pipeline
- Imports and combines modules
- Generates final pipeline configuration
### **2. Common Module (`common.libsonnet`)**
- Shared environment variables
- Common steps (clone, test, cleanup)
- Reusable triggers and conditions
### **3. Build Module (`build-steps.libsonnet`)**
- Build-specific logic
- External Buildah integration
- Container build steps
## 🔄 **Workflow**
1. **Edit Jsonnet files** (`.drone.jsonnet`, `*.libsonnet`)
2. **Test locally** (optional): `jsonnet .drone.jsonnet`
3. **Commit and push** - Drone automatically processes Jsonnet
4. **Pipeline runs** using generated configuration
## 🛠️ **Local Development**
### **Generate YAML for testing:**
```bash
# Generate YAML from Jsonnet
jsonnet .drone.jsonnet > .drone.yml.test
# Validate generated YAML
python3 -c "import yaml; yaml.safe_load(open('.drone.yml.test'))"
# Compare with original
diff .drone.yml.backup .drone.yml.test
```
### **Jsonnet Utilities:**
```bash
# Format Jsonnet files
jsonnetfmt -i .drone.jsonnet common.libsonnet build-steps.libsonnet
# Validate syntax
jsonnet .drone.jsonnet > /dev/null && echo "✅ Valid Jsonnet"
```
## 🎯 **Benefits Achieved**
### **Modularity**
- ✅ Separate concerns (common vs build-specific)
- ✅ Reusable components
- ✅ Easier maintenance
### **Flexibility**
- ✅ Variables and functions
- ✅ Conditional logic
- ✅ Dynamic configuration
### **DRY Principle**
- ✅ No code duplication
- ✅ Single source of truth
- ✅ Consistent patterns
## 📋 **Configuration Examples**
### **Creating Environment-Specific Builds:**
```jsonnet
// .drone.jsonnet
local buildSteps = import 'build-steps.libsonnet';
local commonConfig = import 'common.libsonnet';
local environment = std.extVar('environment');
{
kind: "pipeline",
type: "kubernetes",
name: "auth-service-" + environment,
service_account: "drone-runner",
environment: commonConfig.environment + {
BUILD_ENV: environment
},
steps: [
commonConfig.cloneStep,
commonConfig.testStep,
buildSteps.externalBuildahStep + {
commands: [
// Add environment-specific commands
"echo 'Building for: " + environment + "'",
] + buildSteps.externalBuildahStep.commands
}
]
}
```
### **Adding Conditional Steps:**
```jsonnet
// build-steps.libsonnet
{
externalBuildahStep: {
// ... existing configuration
commands: [
// ... existing commands
] + (
if std.extVar('push_to_registry') == 'true' then [
"echo '📤 Pushing to registry...'",
"kubectl exec $BUILDAH_POD -- buildah push auth-service:1.0.${DRONE_BUILD_NUMBER} docker://registry.aipice.local/auth-service:1.0.${DRONE_BUILD_NUMBER}"
] else []
)
}
}
```
## 🔍 **Troubleshooting**
### **Check if Jsonnet is enabled:**
```bash
kubectl get configmap drone -n apps--droneio--prd -o yaml | grep JSONNET
```
### **Verify Drone server restart:**
```bash
kubectl get pods -n apps--droneio--prd | grep droneio
```
### **Test Jsonnet syntax:**
```bash
jsonnet .drone.jsonnet | python3 -c "import sys,yaml; yaml.safe_load(sys.stdin)"
```
### **View generated pipeline in Drone UI:**
- Go to your repository in Drone UI
- The generated YAML will be shown in the build view
## 🎉 **Next Steps**
1. **Create variants**: Development, staging, production configurations
2. **Add functions**: Custom build logic, notification steps
3. **Share modules**: Reuse across multiple repositories
4. **Optimize**: Use Jsonnet's advanced features for complex scenarios
Your Drone CI is now supercharged with Jsonnet! 🚀

View File

@@ -0,0 +1,306 @@
# Multi-Domain Authentication for *.aipice.fr
This guide shows how to use a single authentication service to protect multiple subdomains under `aipice.fr`.
## 🎯 Architecture Overview
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ auth.aipice.fr │ │ arti-api.aipice │ │ *.aipice.fr │
│ │ │ .fr │ │ │
│ Auth Service │───▶│ Protected API │ │ Other Services │
│ (Login Page) │ │ (with auth) │ │ (with auth) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└────────────────────────┼────────────────────────┘
┌─────────────────┐
│ Active Directory│
│ Validation │
└─────────────────┘
```
## 🚀 Quick Setup for aipice.fr
### 1. Deploy the Authentication Service
```bash
# Run the deployment script
./deploy-aipice.sh
# This will:
# - Create wildcard certificate for *.aipice.fr
# - Deploy auth service at auth.aipice.fr
# - Protect arti-api.aipice.fr with authentication
# - Create reusable ForwardAuth middleware
```
### 2. Access Your Services
- **Authentication**: https://auth.aipice.fr
- **Protected API**: https://arti-api.aipice.fr (requires login)
- **Public endpoints**: https://arti-api.aipice.fr/ and /health (no auth)
## 🔒 How Multi-Domain Protection Works
### Authentication Flow
1. **User visits** `https://arti-api.aipice.fr/users`
2. **Traefik checks** auth via ForwardAuth middleware
3. **No token?** → Redirect to `https://auth.aipice.fr/?return_url=https://arti-api.aipice.fr/users`
4. **User logs in** → JWT token stored in cookie for `.aipice.fr` domain
5. **User redirected** back to `https://arti-api.aipice.fr/users`
6. **Traefik validates** token → Access granted
### Cross-Domain Cookie Sharing
The auth service sets cookies with `domain=.aipice.fr`, making them available to all subdomains:
```python
response.set_cookie(
key="auth_token",
value=token,
domain=".aipice.fr", # Works for all *.aipice.fr
httponly=True,
secure=True,
samesite="lax"
)
```
## 🛡️ Protecting Additional Services
To protect any new subdomain (e.g., `grafana.aipice.fr`), simply add the ForwardAuth middleware:
```yaml
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: grafana-protected
spec:
entryPoints:
- websecure
routes:
- match: Host(`grafana.aipice.fr`)
kind: Rule
services:
- name: grafana
port: 3000
middlewares:
- name: auth-forward
namespace: infrastructure--artifactory--service # Where auth service is deployed
tls:
secretName: wildcard-aipice-fr
```
## 📋 Configuration Examples
### Multiple Protection Levels
You can create different auth requirements for different services:
```yaml
# Public service (no auth)
- match: Host(`public.aipice.fr`)
services:
- name: public-service
port: 80
# Basic auth required
- match: Host(`internal.aipice.fr`)
services:
- name: internal-service
port: 80
middlewares:
- name: auth-forward
# Admin-only access (custom verification)
- match: Host(`admin.aipice.fr`)
services:
- name: admin-service
port: 80
middlewares:
- name: admin-auth-forward # Custom middleware with admin check
```
### Group-Based Access Control
Use Active Directory groups for fine-grained access:
```python
# In your backend service
def require_admin_group(x_auth_groups: str = Header(None)):
groups = x_auth_groups.split(',') if x_auth_groups else []
admin_groups = [
'CN=Domain Admins,CN=Users,DC=aipice,DC=fr',
'CN=IT Team,CN=Groups,DC=aipice,DC=fr'
]
if not any(group in groups for group in admin_groups):
raise HTTPException(status_code=403, detail="Admin access required")
```
## 🔧 Advanced Configuration
### Environment Variables
The auth service supports these domain-specific variables:
```yaml
env:
- name: ALLOWED_DOMAINS
value: "aipice.fr,yourdomain.com" # Multiple domains supported
- name: AUTH_DOMAIN
value: "auth.aipice.fr"
- name: CORS_ORIGINS
value: "https://*.aipice.fr,https://*.yourdomain.com"
```
### Wildcard Certificate
For automatic SSL across all subdomains:
```yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-aipice-fr
spec:
secretName: wildcard-aipice-fr
issuerRef:
name: letsencrypt
kind: ClusterIssuer
commonName: "*.aipice.fr"
dnsNames:
- "aipice.fr"
- "*.aipice.fr"
```
## 🎨 Customizing the Login Page
Update the login page branding for your domain:
```html
<!-- In templates/login.html -->
<div class="login-header">
<h1>🔐 Aipice Access</h1>
<p>Sign in to access Aipice services</p>
</div>
```
## 📊 Monitoring and Logs
### Check Authentication Status
```bash
# View auth service logs
kubectl logs -n infrastructure--artifactory--service deployment/auth-service
# Check ForwardAuth requests
kubectl logs -n traefik deployment/traefik | grep "auth-forward"
# Test authentication
curl -H "Authorization: Bearer $TOKEN" https://auth.aipice.fr/auth/verify
```
### Common Log Entries
```
INFO: Successfully authenticated user: john.doe accessing arti-api.aipice.fr
INFO: No token found, redirecting to: https://auth.aipice.fr/?return_url=https://grafana.aipice.fr/
WARNING: Access denied for domain: suspicious.domain.com
```
## 🔍 Troubleshooting
### Issue: "Page isn't redirecting properly"
**Cause**: Redirect loop between auth service and protected service.
**Solution**: Ensure auth service domain is excluded from ForwardAuth:
```yaml
routes:
- match: Host(`auth.aipice.fr`)
# No auth-forward middleware here!
services:
- name: auth-service
```
### Issue: "Authentication required" but user is logged in
**Cause**: Cookie domain mismatch or token expiration.
**Solution**: Check cookie domain and token validity:
```bash
# Check token in browser console
localStorage.getItem('auth_token')
# Verify token server-side
curl -H "Authorization: Bearer $TOKEN" https://auth.aipice.fr/auth/verify
```
### Issue: Cross-domain cookie not working
**Cause**: Cookie domain not set correctly.
**Solution**: Ensure cookie domain starts with dot:
```python
domain=".aipice.fr" # ✅ Works for all subdomains
domain="aipice.fr" # ❌ Only works for exact domain
```
## 📈 Scaling Considerations
### High Availability
```yaml
spec:
replicas: 3 # Multiple auth service instances
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 1
```
### Performance Tuning
```yaml
resources:
requests:
memory: "256Mi"
cpu: "200m"
limits:
memory: "512Mi"
cpu: "500m"
```
### Caching
Consider adding Redis for token caching in high-traffic scenarios:
```yaml
- name: REDIS_URL
value: "redis://redis-service:6379"
```
## 🎯 Real-World Example
Here's a complete setup for a company using multiple services:
```yaml
# Services protected by auth.aipice.fr:
# - arti-api.aipice.fr (Artifactory API)
# - grafana.aipice.fr (Monitoring)
# - jenkins.aipice.fr (CI/CD)
# - nextcloud.aipice.fr (File sharing)
# - wiki.aipice.fr (Documentation)
# All use the same ForwardAuth middleware
# All share the same authentication cookie
# All redirect to auth.aipice.fr when needed
# All respect Active Directory group memberships
```
This gives you enterprise-grade authentication across your entire `*.aipice.fr` infrastructure with a single, centralized auth service!

View File

@@ -0,0 +1,67 @@
# Pipeline Configuration Files
This folder contains all Drone CI pipeline-related configuration files and scripts.
## 📁 **File Organization**
### **🔧 Jsonnet Configuration**
- `common.libsonnet` - Shared pipeline components (steps, environment, triggers)
- `build-steps.libsonnet` - Build-specific logic (external Buildah integration)
### **🚀 Management Scripts**
- `manage-external-buildah.sh` - External Buildah service management
- `update-buildah-pod.sh` - Auto-update pod references in configurations
- `deploy-external-buildah.sh` - Complete deployment automation
- `convert-to-jsonnet.sh` - Migration helper from YAML to Jsonnet
### **⚙️ Kubernetes Resources**
- `buildah-external-deployment.yaml` - External Buildah service deployment
- `buildah-rbac.yaml` - RBAC for Buildah operations
- `drone-build-rbac.yaml` - RBAC for Drone build steps
- `default-sa-binding.yaml` - Service account permissions
- `drone-configmap-updated.yaml` - Drone server configuration with Jsonnet support
### **📋 Alternative Configurations**
- Various `.drone.yml.*` files - Alternative pipeline configurations for reference
- `.drone.star.example` - Starlark configuration example (not used)
### **📚 Documentation**
- `EXTERNAL-BUILDAH-SYSTEM.md` - External build system documentation
- `JSONNET-GUIDE.md` - Jsonnet usage guide
- Other analysis and guide files
## 🔄 **Usage Workflow**
1. **Edit Configuration**: Modify `common.libsonnet` or `build-steps.libsonnet`
2. **Test Locally**: `cd .. && jsonnet .drone.jsonnet`
3. **Deploy**: Commit and push changes
4. **Manage**: Use scripts in this folder for maintenance
## 🎯 **Key Benefits**
- **🧩 Organized Structure**: All pipeline files in one place
- **🔄 Modular Design**: Separate concerns and reusable components
- **📝 Easy Maintenance**: Clear file organization and documentation
- **🛠️ Management Tools**: Complete set of automation scripts
## 📖 **Quick Start**
```bash
# Test current configuration
cd .. && jsonnet .drone.jsonnet
# Check external Buildah status
./manage-external-buildah.sh status
# Deploy complete system
./deploy-external-buildah.sh
# Update pod references after restarts
./update-buildah-pod.sh
```
## 🔗 **Related Files**
- `../.drone.jsonnet` - Root-level entry point (imports from this folder)
- `../Dockerfile` - Application container definition
- `../requirements.txt` - Application dependencies

View File

@@ -0,0 +1,429 @@
# Authentication Service with Active Directory Integration
This authentication service provides JWT-based authentication with Active Directory integration and Traefik ForwardAuth support for Kubernetes environments.
## Features
- 🔐 **Active Directory Authentication**: Validates credentials against your AD server
- 🎫 **JWT Tokens**: Secure token-based authentication with configurable expiration
- 🍪 **Cookie & Local Storage**: Tokens stored securely in HTTP-only cookies and locally
- 🚀 **Traefik Integration**: ForwardAuth middleware for seamless Kubernetes access control
- 📱 **Responsive UI**: Clean, modern login interface
- 🔒 **Security Headers**: Proper CORS, security headers, and token validation
## Architecture
```
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ User Browser │───▶│ Auth Service │───▶│ Active Directory│
│ │ │ │ │ │
│ 1. Login Form │ │ 2. Validate AD │ │ 3. LDAP Auth │
│ 4. Store Token │◀───│ Create JWT │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │
│ │
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ Protected API │◀───│ Traefik Forward │
│ │ │ Auth Middleware │
│ 5. Access with │ │ 6. Validate JWT │
│ JWT Token │ │ │
└─────────────────┘ └──────────────────┘
```
## Quick Start
### 1. Build and Deploy
```bash
# Build the authentication service
cd auth-service
docker build -t your-registry/auth-service:1.0.0 .
docker push your-registry/auth-service:1.0.0
# Update values in values-example.yaml
cp values-example.yaml values.yaml
# Edit values.yaml with your AD configuration
# Deploy to Kubernetes
kubectl apply -f kubernetes-auth.yaml
```
### 2. Configure Active Directory
Update the `values.yaml` file with your AD configuration:
```yaml
authService:
activeDirectory:
server: "ldap://your-ad-server.yourdomain.com"
baseDN: "DC=yourdomain,DC=com"
userSearchBase: "CN=Users,DC=yourdomain,DC=com"
bindUser: "CN=ServiceAccount,CN=Users,DC=yourdomain,DC=com"
bindPassword: "your-service-account-password"
```
### 3. Configure Traefik ForwardAuth
The service automatically creates a ForwardAuth middleware that:
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: auth-forward
spec:
forwardAuth:
address: http://auth-service:8080/auth/verify
authResponseHeaders:
- "X-Auth-User"
- "X-Auth-Email"
- "X-Auth-Groups"
- "X-Auth-Display-Name"
```
### 4. Protect Your Services
Add the ForwardAuth middleware to any IngressRoute:
```yaml
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: protected-service
spec:
routes:
- match: Host(`api.yourdomain.com`)
kind: Rule
services:
- name: your-api-service
port: 8000
middlewares:
- name: auth-forward # This protects the entire service
```
## How It Works
### Authentication Flow
1. **User visits protected resource** → Traefik ForwardAuth redirects to login
2. **User enters AD credentials** → Service validates against Active Directory
3. **JWT token created** → Stored in HTTP-only cookie + localStorage
4. **Subsequent requests** → Traefik validates JWT via ForwardAuth
5. **Access granted** → User headers passed to backend service
### Token Storage
The system uses a dual-storage approach:
- **HTTP-only Cookie**: Secure, automatic transmission, protected from XSS
- **localStorage**: Available to JavaScript for SPA applications
### Security Features
-**LDAP over TLS** support for secure AD communication
-**JWT token expiration** with configurable timeouts
-**HTTP-only cookies** prevent XSS token theft
-**Secure headers** for production deployment
-**CORS protection** with configurable origins
## API Endpoints
### Authentication Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/` | GET | Login page (HTML) |
| `/dashboard` | GET | Dashboard page (HTML) |
| `/auth/login` | POST | Authenticate user |
| `/auth/verify` | POST | Verify JWT token (ForwardAuth) |
| `/auth/logout` | GET | Logout user |
| `/auth/user` | GET | Get current user info |
| `/health` | GET | Health check |
### ForwardAuth Integration
When Traefik calls `/auth/verify`, the service:
1. **Checks for token** in Authorization header or cookies
2. **Validates JWT** signature and expiration
3. **Returns user headers** for backend services:
- `X-Auth-User`: Username
- `X-Auth-Email`: User email
- `X-Auth-Groups`: AD group memberships
- `X-Auth-Display-Name`: User's display name
## Configuration
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `JWT_SECRET` | Secret key for JWT signing | (required) |
| `TOKEN_EXPIRE_HOURS` | Token expiration in hours | 8 |
| `AD_SERVER` | LDAP server URL | (required) |
| `AD_BASE_DN` | Base DN for AD | (required) |
| `AD_USER_SEARCH_BASE` | User search base | (required) |
| `AD_BIND_USER` | Service account for LDAP | (optional) |
| `AD_BIND_PASSWORD` | Service account password | (optional) |
### Kubernetes Secrets
Create the required secrets:
```bash
kubectl create secret generic auth-secrets \
--from-literal=jwt-secret="your-super-secret-key" \
--from-literal=ad-bind-user="CN=ServiceAccount,CN=Users,DC=yourdomain,DC=com" \
--from-literal=ad-bind-password="your-service-password"
```
## Advanced Usage
### Custom Group-Based Access
The service passes AD group memberships in the `X-Auth-Groups` header. You can use this in your backend services:
```python
# In your FastAPI backend
from fastapi import Header
def check_admin_access(x_auth_groups: str = Header(None)):
groups = x_auth_groups.split(',') if x_auth_groups else []
if 'CN=Admins,CN=Groups,DC=yourdomain,DC=com' not in groups:
raise HTTPException(status_code=403, detail="Admin access required")
```
### Multiple Protection Levels
You can create different ForwardAuth middlewares for different access levels:
```yaml
# Admin-only middleware
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: admin-auth
spec:
forwardAuth:
address: http://auth-service:8080/auth/verify-admin
authResponseHeaders:
- "X-Auth-User"
- "X-Auth-Groups"
```
### Token Refresh
The service automatically handles token refresh. Configure shorter expiration times and implement refresh logic in your frontend:
```javascript
// Check token expiration
const token = localStorage.getItem('auth_token');
const payload = JSON.parse(atob(token.split('.')[1]));
const expiry = new Date(payload.exp * 1000);
if (expiry < new Date()) {
// Redirect to login for refresh
window.location.href = '/auth/login';
}
```
## Monitoring
### Health Checks
The service includes health check endpoints:
```bash
curl http://auth-service:8080/health
```
### Logging
The service logs authentication attempts and failures:
```
INFO: Successfully authenticated user: john.doe
ERROR: Authentication failed for user: invalid.user
ERROR: LDAP connection failed: timeout
```
## Security Considerations
1. **Use HTTPS**: Always deploy with TLS certificates
2. **Secure JWT Secret**: Use a strong, unique JWT secret
3. **Network Security**: Restrict access to AD servers
4. **Token Expiration**: Use reasonable token expiration times
5. **Service Account**: Use a dedicated AD service account with minimal permissions
6. **Audit Logs**: Monitor authentication logs for suspicious activity
## Troubleshooting
### Common Issues
1. **LDAP Connection Failed**
- Check AD server connectivity
- Verify LDAP URL format
- Check firewall rules
2. **Authentication Failed**
- Verify AD credentials
- Check user search base DN
- Confirm user exists in specified OU
3. **ForwardAuth Not Working**
- Verify Traefik middleware configuration
- Check service connectivity
- Review Traefik logs
4. **Token Issues**
- Verify JWT secret consistency
- Check token expiration
- Validate cookie settings
### Debug Mode
Enable debug logging by setting log level:
```yaml
env:
- name: LOG_LEVEL
value: "DEBUG"
```
This will provide detailed authentication flow logs.
# 🔧 Drone Configuration Factorization Options
## 1. Jsonnet Configuration (Recommended)
Drone supports Jsonnet for dynamic configuration generation, which allows you to include files and create reusable components.
## 2. YAML Anchors & References (Limited)
YAML supports anchors and references for basic factorization:
## 3. External Configuration Files (Starlark)
Drone also supports Starlark (Python-like) for advanced configuration:
🎯 Recommended: Convert to Jsonnet
Let me create a script to help you convert your current .drone.yml to the modular Jsonnet approach:
convert-to-jsonnet.sh
```bash
#!/bin/bash
# Convert existing .drone.yml to modular Jsonnet structure
# This script helps migrate from YAML to factorized Jsonnet configuration
echo "🔄 Converting Drone configuration to modular Jsonnet..."
# Generate the final configuration from Jsonnet
echo "📝 Generating .drone.yml from Jsonnet..."
if command -v jsonnet >/dev/null 2>&1; then
jsonnet .drone.jsonnet > .drone.yml.generated
echo "✅ Generated .drone.yml.generated from Jsonnet"
echo ""
echo "📋 To use the new configuration:"
echo "1. Review: cat .drone.yml.generated"
echo "2. Test: python3 -c \"import yaml; yaml.safe_load(open('.drone.yml.generated'))\""
echo "3. Replace: mv .drone.yml.generated .drone.yml"
echo "4. Commit: git add .drone.jsonnet common.libsonnet build-steps.libsonnet .drone.yml"
else
echo "⚠️ jsonnet not installed. Installing..."
# Try to install jsonnet
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update && sudo apt-get install -y jsonnet
elif command -v apk >/dev/null 2>&1; then
apk add --no-cache jsonnet
elif command -v yum >/dev/null 2>&1; then
sudo yum install -y jsonnet
else
echo "❌ Cannot install jsonnet automatically"
echo "📖 Please install jsonnet manually:"
echo " - Ubuntu/Debian: sudo apt-get install jsonnet"
echo " - Alpine: apk add jsonnet"
echo " - CentOS/RHEL: sudo yum install jsonnet"
echo " - Or download from: https://github.com/google/jsonnet"
exit 1
fi
# Try generating again
if command -v jsonnet >/dev/null 2>&1; then
jsonnet .drone.jsonnet > .drone.yml.generated
echo "✅ Generated .drone.yml.generated from Jsonnet"
else
echo "❌ Failed to install jsonnet"
exit 1
fi
fi
echo ""
echo "🎯 Benefits of Jsonnet configuration:"
echo " ✅ Reusable components (common.libsonnet, build-steps.libsonnet)"
echo " ✅ Variables and functions"
echo " ✅ Conditional logic"
echo " ✅ Better maintainability"
echo " ✅ DRY principle"
echo ""
echo "📚 Files created:"
echo " - .drone.jsonnet (main configuration)"
echo " - common.libsonnet (shared steps and config)"
echo " - build-steps.libsonnet (build-specific logic)"
echo " - .drone.yml.generated (generated YAML)"
# Validate the generated YAML
if [ -f ".drone.yml.generated" ]; then
echo ""
echo "🔍 Validating generated YAML..."
if python3 -c "import yaml; yaml.safe_load(open('.drone.yml.generated'))" 2>/dev/null; then
echo "✅ Generated YAML is valid"
else
echo "❌ Generated YAML has syntax errors"
exit 1
fi
fi
```
# 📋 Summary of Factorization Options
| Method | Pros | Cons | Best For |
| ------ | ---- | ---- | -------- |
| Jsonnet | ✅ Full programming language<br>✅ Variables, functions, imports<br>✅ Conditional logic | ❌ Learning curve<br>❌ Requires jsonnet tool | Complex configurations |
| YAML Anchors | ✅ Native YAML<br>✅ Simple syntax<br>✅ No extra tools | ❌ Limited functionality<br>❌ No conditionals | Simple repetition |
| Starlark | ✅ Python-like syntax<br>✅ Advanced logic<br>✅ Built into Drone | ❌ Drone-specific<br>❌ Less common | Advanced pipelines |
# 🎯 Recommendation for Your Project
For your external Buildah build system, I recommend Jsonnet because:
- **Modularity**: Separate build logic from common steps
- **Reusability**: Common kubectl setup, environment config
- **Maintainability**: Easier to update build steps independently
- **Flexibility**: Can create variants (dev, staging, prod) easily
## 🚀 Quick Start with Jsonnet
```console
# Install jsonnet (if needed)
sudo apt-get install jsonnet
# Convert to modular structure
./convert-to-jsonnet.sh
# Review generated configuration
cat .drone.yml.generated
# Test and deploy
mv .drone.yml.generated .drone.yml
git add .drone.jsonnet common.libsonnet build-steps.libsonnet .drone.yml
git commit -m "Convert to modular Jsonnet configuration"
git push
```
The modular approach will make it much easier to:
- 🔧 Update build steps without touching common logic
- 🎯 Create environment-specific configurations
- 🧪 Test individual components
- 📦 Share configuration across projects

View File

@@ -0,0 +1,152 @@
# Replica-Based Build Locking System
## 🎯 **Concept**
Instead of using lock files, we use Kubernetes deployment **replica scaling** as an atomic locking mechanism:
- **Replicas = 0**: No build running (lock available)
- **Replicas = 1**: Build in progress (lock acquired)
## 🔧 **How It Works**
### **Build Start (Lock Acquisition)**
```bash
# Check if lock is available
CURRENT_REPLICAS=$(kubectl get deployment buildah-external -o jsonpath='{.spec.replicas}')
if [ "$CURRENT_REPLICAS" = "0" ]; then
# Acquire lock by scaling up
kubectl scale deployment buildah-external --replicas=1
kubectl wait --for=condition=ready pod -l app=buildah-external --timeout=120s
else
# Lock unavailable - build already running
exit 1
fi
```
### **Build End (Lock Release)**
```bash
# Always release lock (runs on success OR failure)
kubectl scale deployment buildah-external --replicas=0
kubectl wait --for=delete pod -l app=buildah-external --timeout=60s
```
## ✅ **Benefits**
### **🔒 Atomic Operations**
- **Kubernetes guarantees** atomic scaling operations
- **No race conditions** possible between concurrent builds
- **Built-in conflict resolution** via Kubernetes API
### **🚀 Resource Efficiency**
- **Zero resource usage** when no builds are running
- **Pod only exists** during active builds
- **Automatic cleanup** of compute resources
### **🛡️ Robust Error Handling**
- **Scale-down always runs** (success or failure)
- **No stale locks** - Kubernetes manages lifecycle
- **Self-healing** if pods crash during build
### **📊 Observable State**
- **Easy monitoring**: `kubectl get deployment buildah-external`
- **Clear status**: Replica count = build status
- **No hidden state** in lock files
## 🔄 **Build Pipeline Flow**
```mermaid
graph TD
A[Build Triggered] --> B{Check Replicas}
B -->|replicas=0| C[Scale to 1]
B -->|replicas≠0| D[❌ Build Already Running]
C --> E[Wait for Pod Ready]
E --> F[Execute Build]
F --> G[Scale to 0]
G --> H[✅ Build Complete]
D --> I[❌ Exit with Error]
```
## 📋 **Pipeline Implementation**
### **Build Step**
```jsonnet
{
name: "build-via-external-buildah",
commands: [
// Check current replicas
"CURRENT_REPLICAS=$(kubectl get deployment buildah-external -o jsonpath='{.spec.replicas}')",
// Acquire lock or fail
"if [ \"$CURRENT_REPLICAS\" = \"0\" ]; then",
" kubectl scale deployment buildah-external --replicas=1",
" kubectl wait --for=condition=ready pod -l app=buildah-external --timeout=120s",
"else",
" echo \"Build already running!\"; exit 1",
"fi",
// ... build commands ...
]
}
```
### **Cleanup Step**
```jsonnet
{
name: "scale-down-buildah",
commands: [
"kubectl scale deployment buildah-external --replicas=0",
"kubectl wait --for=delete pod -l app=buildah-external --timeout=60s"
],
when: {
status: ["success", "failure"] // Always runs
}
}
```
## 🧪 **Testing**
Use the test script to verify the locking mechanism:
```bash
pipeline/test-replica-locking.sh
```
This tests:
- ✅ Lock acquisition when available
- ✅ Lock blocking when unavailable
- ✅ Proper lock release
- ✅ System reset for next build
## 🔍 **Monitoring**
### **Check Build Status**
```bash
# Quick status check
kubectl get deployment buildah-external -n apps--droneio--prd
# Detailed status
kubectl describe deployment buildah-external -n apps--droneio--prd
```
### **Build Status Meanings**
- **READY 0/0**: No build running, system idle
- **READY 0/1**: Build starting, pod creating
- **READY 1/1**: Build active, pod running
- **READY 1/0**: Build ending, pod terminating
## 🎯 **Migration Notes**
This approach **replaces**:
- ❌ Lock file creation/deletion
- ❌ Lock timeout mechanisms
- ❌ Lock cleanup scripts
- ❌ Manual pod discovery
With **Kubernetes-native**:
- ✅ Atomic scaling operations
- ✅ Built-in conflict resolution
- ✅ Automatic resource management
- ✅ Observable state
The system is now **simpler, more reliable, and more efficient**! 🚀

View File

@@ -0,0 +1,250 @@
# Traefik Certificate Fix for drone.aipice.local
The error indicates that Traefik is serving a default certificate instead of a proper certificate for `drone.aipice.local`.
## 🔍 Root Cause
```
x509: certificate is valid for a7b8f3b8fd415b0fbd62e803b96eec90.d8282a75d7bf97aa2eb0bd7c2d927f85.traefik.default, not drone.aipice.local
```
This means:
- Traefik is using a default/fallback certificate
- No proper certificate configured for `drone.aipice.local`
- The domain doesn't match the certificate
## 🚀 Solutions
### Solution 1: Create Proper IngressRoute for Drone
Create a proper Traefik IngressRoute for your Drone CI:
```yaml
---
# drone-ingressroute.yaml
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: drone-ci
namespace: drone # Adjust to your Drone namespace
spec:
entryPoints:
- websecure
routes:
- match: Host(`drone.aipice.local`)
kind: Rule
services:
- name: drone-server # Your Drone service name
port: 80
tls:
certResolver: letsencrypt
domains:
- main: drone.aipice.local
---
# If you need a wildcard certificate for *.aipice.local
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: wildcard-aipice-local
namespace: drone
spec:
secretName: wildcard-aipice-local-tls
issuerRef:
name: letsencrypt
kind: ClusterIssuer
commonName: "*.aipice.local"
dnsNames:
- "aipice.local"
- "*.aipice.local"
```
### Solution 2: Update Drone Helm Values (if using Helm)
If you're using Helm to deploy Drone:
```yaml
# drone-values.yaml
ingress:
enabled: true
className: traefik
annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
cert-manager.io/cluster-issuer: letsencrypt
hosts:
- host: drone.aipice.local
paths:
- path: /
pathType: Prefix
tls:
- secretName: drone-aipice-local-tls
hosts:
- drone.aipice.local
```
### Solution 3: Manual Certificate Creation
Create a certificate manually for `drone.aipice.local`:
```yaml
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: drone-aipice-local-cert
namespace: drone
spec:
secretName: drone-tls-secret
issuerRef:
name: letsencrypt
kind: ClusterIssuer
commonName: drone.aipice.local
dnsNames:
- drone.aipice.local
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: drone-secure
namespace: drone
spec:
entryPoints:
- websecure
routes:
- match: Host(`drone.aipice.local`)
kind: Rule
services:
- name: drone-server
port: 80
tls:
secretName: drone-tls-secret
```
## 🔧 Quick Fix Commands
```bash
# 1. Check current Drone IngressRoute
kubectl get ingressroute -A | grep drone
# 2. Check current certificates
kubectl get certificates -A | grep drone
# 3. Check Traefik logs for certificate issues
kubectl logs -n traefik deployment/traefik | grep drone
# 4. Apply the fixed IngressRoute
kubectl apply -f drone-ingressroute.yaml
# 5. Wait for certificate to be issued
kubectl get certificate -n drone -w
```
## 🕵️ Debugging Steps
### Check Current Drone Service
```bash
# Find your Drone service
kubectl get svc -A | grep drone
# Check the service details
kubectl describe svc drone-server -n drone
```
### Check Traefik Configuration
```bash
# Check Traefik dashboard for routing
kubectl port-forward -n traefik svc/traefik 8080:8080
# Visit http://localhost:8080 to see routes
# Check IngressRoutes
kubectl get ingressroute -A -o yaml | grep -A 20 drone
```
### Verify Certificate Status
```bash
# Check certificate status
kubectl describe certificate -n drone
# Check certificate secret
kubectl get secret -n drone | grep tls
# Test certificate with openssl
openssl s_client -connect drone.aipice.local:443 -servername drone.aipice.local
```
## 🛠️ Alternative: Disable Certificate Verification
If you can't fix the certificate immediately, you can configure your Git service to skip certificate verification:
### For Gitea
```ini
# In Gitea app.ini
[webhook]
SKIP_TLS_VERIFY = true
ALLOWED_HOST_LIST = private
```
### For GitLab
```ruby
# In gitlab.rb
gitlab_rails['webhook_timeout'] = 30
gitlab_rails['outbound_requests_whitelist'] = ['192.168.100.0/24']
gitlab_rails['webhook_ssl_verification'] = false
```
### For GitHub (if self-hosted)
In webhook configuration:
- ☐ Enable SSL verification (uncheck this)
## 🎯 Complete Working Example
Here's a complete working configuration:
```yaml
---
# Complete Drone CI IngressRoute with proper TLS
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: drone-aipice-local
namespace: drone
labels:
app: drone-server
spec:
entryPoints:
- websecure
routes:
- match: Host(`drone.aipice.local`)
kind: Rule
services:
- name: drone-server
port: 80
middlewares:
- name: drone-headers
tls:
certResolver: letsencrypt
domains:
- main: drone.aipice.local
---
# Optional: Add security headers
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: drone-headers
namespace: drone
spec:
headers:
customRequestHeaders:
X-Forwarded-Proto: https
customResponseHeaders:
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
```
Apply this configuration and your webhooks should work properly with valid TLS certificates!

View File

@@ -0,0 +1,132 @@
// build-steps.libsonnet - Build-specific steps with replica-based scaling and locking
{
externalBuildahStep: {
name: "build-via-external-buildah",
image: "alpine:latest",
pull: "if-not-exists",
commands: [
"echo '🏗 Building via external Buildah deployment with replica scaling...'",
"echo 'Installing kubectl...'",
"apk add --no-cache curl",
"curl -LO \"https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl\"",
"chmod +x kubectl",
"mv kubectl /usr/local/bin/",
"echo '📦 Preparing build context...'",
"BUILD_ID=\"auth-service-${DRONE_BUILD_NUMBER}-$(date +%s)\"",
"echo \"Build ID: $BUILD_ID\"",
"echo '🔍 Checking current Buildah deployment replicas...'",
"CURRENT_REPLICAS=$(kubectl get deployment buildah-external -n apps--droneio--prd -o jsonpath='{.spec.replicas}')",
"echo \"Current replicas: $CURRENT_REPLICAS\"",
"echo '🔒 Attempting to scale up Buildah deployment (acts as build lock)...'",
"if [ \"$CURRENT_REPLICAS\" = \"0\" ]; then",
" echo \" No build running, scaling up deployment...\"",
" kubectl scale deployment buildah-external --replicas=1 -n apps--droneio--prd",
" echo \" Waiting for pod to be ready...\"",
" kubectl wait --for=condition=ready pod -l app=buildah-external -n apps--droneio--prd --timeout=120s",
"else",
" echo \" Build already running (replicas=$CURRENT_REPLICAS)! Aborting to prevent conflicts.\"",
" exit 1",
"fi",
"echo '<EFBFBD> Finding ready Buildah pod...'",
"BUILDAH_POD=$(kubectl get pods -n apps--droneio--prd -l app=buildah-external --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}')",
"if [ -z \"$BUILDAH_POD\" ]; then",
" echo \" No running Buildah pod found after scaling!\"",
" kubectl get pods -n apps--droneio--prd -l app=buildah-external",
" exit 1",
"fi",
"echo \" Using Buildah pod: $BUILDAH_POD\"",
"echo '📁 Creating build directory in Buildah pod...'",
"kubectl exec $BUILDAH_POD -n apps--droneio--prd -- mkdir -p \"/workspace/builds/$BUILD_ID\"",
"echo '📤 Copying source files to Buildah pod...'",
"tar czf - . | kubectl exec -i $BUILDAH_POD -n apps--droneio--prd -- tar xzf - -C \"/workspace/builds/$BUILD_ID\"",
"echo '🔨 Building container image with version from config...'",
"echo 'Reading version configuration...'",
". ./version.conf",
"DOCKER_TAG=\"$DOCKER_REPO:$BASE_VERSION.$DRONE_BUILD_NUMBER\"",
"echo \"Building with tag: $DOCKER_TAG\"",
"kubectl exec $BUILDAH_POD -n apps--droneio--prd -- sh -c \"cd /workspace/builds/$BUILD_ID && buildah build --isolation=chroot --storage-driver=vfs --format=docker --tag $DOCKER_TAG .\"",
"echo '📋 Listing built images...'",
"kubectl exec $BUILDAH_POD -n apps--droneio--prd -- buildah images | grep auth-service",
"echo \" Image built with tag: $DOCKER_TAG\"",
"echo '🧹 Cleaning up build directory...'",
"kubectl exec $BUILDAH_POD -n apps--droneio--prd -- rm -rf \"/workspace/builds/$BUILD_ID\"",
"echo ' External Buildah build completed successfully!'"
],
when: {
event: ["push"]
}
},
pushDockerStep: {
name: "push-docker-image",
image: "alpine:latest",
environment: {
DOCKER_USERNAME: { from_secret: "docker_username" },
DOCKER_PASSWORD: { from_secret: "docker_password" },
DOCKER_REGISTRY: { from_secret: "docker_registry" }
},
commands: [
"echo '📤 Pushing Docker image to registry...'",
"echo 'Installing kubectl...'",
"apk add --no-cache curl",
"curl -LO \"https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl\"",
"chmod +x kubectl && mv kubectl /usr/local/bin/",
"echo 'Reading version configuration...'",
". ./version.conf",
"DOCKER_TAG=\"$DOCKER_REPO:$BASE_VERSION.$DRONE_BUILD_NUMBER\"",
"echo \"Pushing image: $DOCKER_TAG\"",
"echo '🔍 Finding Buildah pod...'",
"BUILDAH_POD=$(kubectl get pods -n apps--droneio--prd -l app=buildah-external --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}')",
"echo \"Using Buildah pod: $BUILDAH_POD\"",
"echo '🔑 Authenticating with Docker registry...'",
"if [ -n \"$DOCKER_USERNAME\" ] && [ -n \"$DOCKER_PASSWORD\" ]; then",
" echo \"Logging into Docker registry...\"",
" kubectl exec $BUILDAH_POD -n apps--droneio--prd -- buildah login -u \"$DOCKER_USERNAME\" -p \"$DOCKER_PASSWORD\" \"$DOCKER_REGISTRY\"",
"else",
" echo \"No Docker credentials provided - attempting unauthenticated push\"",
"fi",
"echo '🚀 Pushing image to registry...'",
"kubectl exec $BUILDAH_POD -n apps--droneio--prd -- buildah push \"$DOCKER_TAG\"",
"echo \" Successfully pushed: $DOCKER_TAG\""
],
when: {
event: ["push"],
branch: ["main", "master"]
}
},
scaleDownStep: {
name: "scale-down-buildah",
image: "alpine:latest",
commands: [
"echo '🔽 Scaling down Buildah deployment (release build lock)...'",
"apk add --no-cache curl",
"curl -LO \"https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl\"",
"chmod +x kubectl && mv kubectl /usr/local/bin/",
"echo '📊 Current deployment status:'",
"kubectl get deployment buildah-external -n apps--droneio--prd",
"echo '🔽 Scaling down to 0 replicas...'",
"kubectl scale deployment buildah-external --replicas=0 -n apps--droneio--prd",
"echo ' Waiting for pods to terminate...'",
"kubectl wait --for=delete pod -l app=buildah-external -n apps--droneio--prd --timeout=60s || echo \"Pods may still be terminating\"",
"echo ' Buildah deployment scaled down - build lock released!'"
],
when: {
status: ["success", "failure"]
}
}
}

View File

@@ -0,0 +1,69 @@
# buildah-chart-override.yaml
# Override values for Drone chart to include signal-aware Buildah deployment
# If using Helm charts, these values override the default deployment
buildah-external:
enabled: true
replicaCount: 0 # Start with 0 replicas
image:
repository: quay.io/buildah/stable
tag: latest
pullPolicy: IfNotPresent
# Signal-aware command override
command: ["/bin/bash"]
args: ["-c", "trap 'echo Received SIGTERM, shutting down gracefully; exit 0' TERM; while true; do sleep 5 & wait $!; done"]
# Security context
securityContext:
privileged: true
runAsUser: 0
capabilities:
add:
- SYS_ADMIN
- MKNOD
- SYS_CHROOT
# Resource limits
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "2Gi"
cpu: "1000m"
# Pod security and termination
podSecurityContext:
runAsUser: 0
fsGroup: 0
# Graceful termination period
terminationGracePeriodSeconds: 10 # Reduced from default 30s
# Service account
serviceAccount:
name: "drone-buildah-sa"
# Environment variables
env:
- name: STORAGE_DRIVER
value: "vfs"
- name: BUILDAH_ISOLATION
value: "chroot"
# Volumes
volumes:
- name: workspace
emptyDir:
sizeLimit: 2Gi
- name: buildah-storage
emptyDir:
sizeLimit: 2Gi
volumeMounts:
- name: workspace
mountPath: /workspace
- name: buildah-storage
mountPath: /var/lib/containers

View File

@@ -0,0 +1,35 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: buildah-entrypoint
namespace: apps--droneio--prd
data:
entrypoint.sh: |
#!/bin/bash
# Signal-aware entrypoint for graceful shutdown
echo "🚀 Starting Buildah container with graceful shutdown support"
# Graceful shutdown handler
shutdown_handler() {
echo "📡 Received termination signal, shutting down gracefully..."
# Kill any running buildah processes
pkill -TERM buildah 2>/dev/null || true
# Give processes time to cleanup
sleep 2
echo "✅ Graceful shutdown complete"
exit 0
}
# Set up signal handlers
trap shutdown_handler SIGTERM SIGINT
# Keep container alive while handling signals
echo "⏳ Container ready, waiting for build requests..."
while true; do
sleep 10 &
wait $! # This wait will be interrupted by signals
done

View File

@@ -0,0 +1,68 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: buildah-external
namespace: apps--droneio--prd
labels:
app: buildah-external
purpose: on-demand-builds
spec:
replicas: 0 # Default to 0 - scaled up only during builds for atomic locking
selector:
matchLabels:
app: buildah-external
template:
metadata:
labels:
app: buildah-external
spec:
serviceAccountName: drone-buildah-sa
terminationGracePeriodSeconds: 5 # Faster termination
containers:
- name: buildah
image: quay.io/buildah/stable:latest
# Signal-aware command that responds to SIGTERM immediately
command: ["/bin/bash"]
args: ["-c", "trap 'exit 0' TERM; while true; do sleep 30 & wait $!; done"]
securityContext:
privileged: true
runAsUser: 0
capabilities:
add:
- SYS_ADMIN
- MKNOD
- SYS_CHROOT
volumeMounts:
- name: workspace
mountPath: /workspace
- name: buildah-storage
mountPath: /var/lib/containers
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "2Gi"
cpu: "1000m"
env:
- name: STORAGE_DRIVER
value: "vfs"
- name: BUILDAH_ISOLATION
value: "chroot"
# Readiness probe to ensure container is ready for builds
readinessProbe:
exec:
command:
- /bin/bash
- -c
- "buildah --version"
initialDelaySeconds: 5
periodSeconds: 10
volumes:
- name: workspace
emptyDir:
sizeLimit: 2Gi
- name: buildah-storage
emptyDir:
sizeLimit: 2Gi
restartPolicy: Always

View File

@@ -0,0 +1,65 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: buildah-external
namespace: apps--droneio--prd
labels:
app: buildah-external
purpose: on-demand-builds
spec:
# Default to 0 - scaled up only during builds for atomic locking
replicas: 0
selector:
matchLabels:
app: buildah-external
template:
metadata:
labels:
app: buildah-external
spec:
serviceAccountName: drone-buildah-sa
containers:
- name: buildah
image: quay.io/buildah/stable:latest
command: ["/bin/bash"]
args: ["/scripts/entrypoint.sh"]
securityContext:
privileged: true
runAsUser: 0
capabilities:
add:
- SYS_ADMIN
- MKNOD
- SYS_CHROOT
volumeMounts:
- name: entrypoint-script
mountPath: /scripts
readOnly: true
- name: workspace
mountPath: /workspace
- name: buildah-storage
mountPath: /var/lib/containers
resources:
requests:
memory: "256Mi"
cpu: "100m"
limits:
memory: "2Gi"
cpu: "1200m"
env:
- name: STORAGE_DRIVER
value: "vfs"
- name: BUILDAH_ISOLATION
value: "chroot"
volumes:
- name: entrypoint-script
configMap:
name: buildah-entrypoint
defaultMode: 0755
- name: workspace
emptyDir:
sizeLimit: 2Gi
- name: buildah-storage
emptyDir:
sizeLimit: 2Gi
restartPolicy: Always

View File

@@ -0,0 +1,84 @@
// common.libsonnet - Shared configuration
{
environment: {
GIT_SSL_NO_VERIFY: "true"
},
cloneStep: {
name: "clone",
image: "alpine/git",
commands: [
"echo '🔄 Cloning repository...'",
"git config --global http.sslVerify false",
"git config --global user.email 'drone@aipice.local'",
"git config --global user.name 'Drone CI'",
"git clone https://gitea.aipice.local/AIPICE/auth-service.git . || echo 'Clone failed, but continuing...'",
"git checkout $DRONE_COMMIT || echo 'Checkout failed, using default'"
],
when: {
event: ["push"]
}
},
versionStep: {
name: "read-version",
image: "alpine:latest",
commands: [
"echo '📄 Reading version configuration...'",
"echo 'Sourcing version.conf...'",
". ./version.conf",
"echo \"BASE_VERSION: $BASE_VERSION\"",
"echo \"DOCKER_REPO: $DOCKER_REPO\"",
"DOCKER_TAG=\"$DOCKER_REPO:$BASE_VERSION.$DRONE_BUILD_NUMBER\"",
"echo \"DOCKER_TAG: $DOCKER_TAG\"",
"echo ' Version configuration loaded!'",
"echo \"Will build: $DOCKER_TAG\""
],
when: {
event: ["push"]
}
},
testStep: {
name: "test",
image: "alpine:latest",
commands: [
"echo '🧪 Starting tests...'",
"echo 'Repository ${DRONE_REPO}'",
"echo 'Branch ${DRONE_BRANCH}'",
"echo 'Owner ${DRONE_REPO_OWNER}'",
"echo 'Commit ${DRONE_COMMIT_SHA:0:8}'",
"echo 'Build ${DRONE_BUILD_NUMBER}'",
"echo 'Reading version info...'",
". ./version.conf",
"DOCKER_TAG=\"$DOCKER_REPO:$BASE_VERSION.$DRONE_BUILD_NUMBER\"",
"echo \"Docker tag will be: $DOCKER_TAG\"",
"echo 'Checking Dockerfile:'",
"cat Dockerfile || echo ' Dockerfile not found!'",
"echo ' Pre-build validation passed!'"
],
when: {
event: ["push"]
}
},
cleanupStep: {
name: "cleanup-build-lock",
image: "alpine:latest",
commands: [
"echo '🧹 Ensuring build lock cleanup...'",
"apk add --no-cache curl",
"curl -LO \"https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl\"",
"chmod +x kubectl && mv kubectl /usr/local/bin/",
"BUILDAH_POD=$(kubectl get pods -n apps--droneio--prd -l app=buildah-external --field-selector=status.phase=Running -o jsonpath='{.items[0].metadata.name}')",
"if [ -n \"$BUILDAH_POD\" ]; then kubectl exec $BUILDAH_POD -n apps--droneio--prd -- rm -f \"/workspace/locks/build-${DRONE_BUILD_NUMBER}.lock\" || echo \"Lock cleanup completed\"; echo \" Lock cleanup verified\"; else echo \" Buildah pod not available for cleanup\"; fi"
],
when: {
status: ["success", "failure"]
}
},
trigger: {
event: ["push", "pull_request"]
}
}

View File

@@ -0,0 +1,73 @@
#!/bin/bash
# Convert existing .drone.yml to modular Jsonnet structure
# This script helps migrate from YAML to factorized Jsonnet configuration
echo "🔄 Converting Drone configuration to modular Jsonnet..."
# Generate the final configuration from Jsonnet
echo "📝 Generating .drone.yml from Jsonnet..."
if command -v jsonnet >/dev/null 2>&1; then
jsonnet .drone.jsonnet > .drone.yml.generated
echo "✅ Generated .drone.yml.generated from Jsonnet"
echo ""
echo "📋 To use the new configuration:"
echo "1. Review: cat .drone.yml.generated"
echo "2. Test: python3 -c \"import yaml; yaml.safe_load(open('.drone.yml.generated'))\""
echo "3. Replace: mv .drone.yml.generated .drone.yml"
echo "4. Commit: git add .drone.jsonnet common.libsonnet build-steps.libsonnet .drone.yml"
else
echo "⚠️ jsonnet not installed. Installing..."
# Try to install jsonnet
if command -v apt-get >/dev/null 2>&1; then
sudo apt-get update && sudo apt-get install -y jsonnet
elif command -v apk >/dev/null 2>&1; then
apk add --no-cache jsonnet
elif command -v yum >/dev/null 2>&1; then
sudo yum install -y jsonnet
else
echo "❌ Cannot install jsonnet automatically"
echo "📖 Please install jsonnet manually:"
echo " - Ubuntu/Debian: sudo apt-get install jsonnet"
echo " - Alpine: apk add jsonnet"
echo " - CentOS/RHEL: sudo yum install jsonnet"
echo " - Or download from: https://github.com/google/jsonnet"
exit 1
fi
# Try generating again
if command -v jsonnet >/dev/null 2>&1; then
jsonnet .drone.jsonnet > .drone.yml.generated
echo "✅ Generated .drone.yml.generated from Jsonnet"
else
echo "❌ Failed to install jsonnet"
exit 1
fi
fi
echo ""
echo "🎯 Benefits of Jsonnet configuration:"
echo " ✅ Reusable components (common.libsonnet, build-steps.libsonnet)"
echo " ✅ Variables and functions"
echo " ✅ Conditional logic"
echo " ✅ Better maintainability"
echo " ✅ DRY principle"
echo ""
echo "📚 Files created:"
echo " - .drone.jsonnet (main configuration)"
echo " - common.libsonnet (shared steps and config)"
echo " - build-steps.libsonnet (build-specific logic)"
echo " - .drone.yml.generated (generated YAML)"
# Validate the generated YAML
if [ -f ".drone.yml.generated" ]; then
echo ""
echo "🔍 Validating generated YAML..."
if python3 -c "import yaml; yaml.safe_load(open('.drone.yml.generated'))" 2>/dev/null; then
echo "✅ Generated YAML is valid"
else
echo "❌ Generated YAML has syntax errors"
exit 1
fi
fi

View File

@@ -0,0 +1,62 @@
#!/bin/bash
# Deploy External Buildah Build System
# Sets up complete external build environment for Drone CI
set -e
echo "🚀 Deploying External Buildah Build System"
echo "============================================="
NAMESPACE="apps--droneio--prd"
# Check if namespace exists
if ! kubectl get namespace $NAMESPACE >/dev/null 2>&1; then
echo "❌ Namespace $NAMESPACE not found!"
exit 1
fi
echo "✅ Namespace $NAMESPACE verified"
# Deploy RBAC if not exists
echo "🔐 Setting up RBAC..."
if kubectl get serviceaccount drone-buildah-sa -n $NAMESPACE >/dev/null 2>&1; then
echo "✅ ServiceAccount already exists"
else
kubectl apply -f buildah-rbac.yaml
echo "✅ RBAC deployed"
fi
# Deploy external Buildah service
echo "🏗️ Deploying external Buildah service..."
kubectl apply -f buildah-external-deployment.yaml
echo "⏳ Waiting for Buildah pod to be ready..."
kubectl wait --for=condition=ready pod -l app=buildah-external -n $NAMESPACE --timeout=60s
# Update pod references
echo "🔄 Updating configuration files..."
./update-buildah-pod.sh
# Test the setup
echo "🧪 Testing build system..."
./manage-external-buildah.sh test
# Show status
echo ""
echo "📊 Deployment Status"
echo "===================="
kubectl get pods -n $NAMESPACE | grep -E "(NAME|buildah|drone)"
echo ""
echo "✅ External Buildah Build System deployed successfully!"
echo ""
echo "🎯 Next Steps:"
echo "1. Test with: ./manage-external-buildah.sh status"
echo "2. Use config: cp .drone.yml.external-buildah-production .drone.yml"
echo "3. Commit and push to trigger build"
echo ""
echo "📋 Available configurations:"
echo " .drone.yml.external-buildah - Basic external build"
echo " .drone.yml.external-buildah-advanced - Advanced with error handling"
echo " .drone.yml.external-buildah-production - Production-ready version"

View File

@@ -0,0 +1,29 @@
#!/bin/bash
echo "🚀 FORCE DRONE BUILD"
echo "==================="
echo "📅 $(date)"
echo
echo "Method: Empty commit (most reliable)"
echo "Repository: AIPICE/auth-service"
echo
read -p "Force build now? (y/N): " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]
then
echo "Creating empty commit..."
git commit --allow-empty -m "Force Drone build - $(date +'%Y-%m-%d %H:%M:%S')"
echo "Pushing to trigger build..."
git push
echo "✅ Build trigger sent!"
echo "Monitor build at: https://drone.aipice.local/AIPICE/auth-service"
echo "Watch build logs:"
echo " kubectl logs -f \$(kubectl get pods -n apps--droneio--prd | grep drone-runner | cut -d' ' -f1) -n apps--droneio--prd"
else
echo "Build not triggered."
fi

View File

@@ -0,0 +1,64 @@
#!/bin/bash
# Test graceful termination of signal-aware Buildah container
set -e
NAMESPACE="apps--droneio--prd"
DEPLOYMENT="buildah-external"
echo "🧪 Testing Graceful Termination"
echo "==============================="
# Scale up to create a pod
echo "🔼 Scaling up deployment..."
kubectl scale deployment $DEPLOYMENT --replicas=1 -n $NAMESPACE
echo "⏳ Waiting for pod to be ready..."
kubectl wait --for=condition=ready pod -l app=buildah-external -n $NAMESPACE --timeout=60s
POD_NAME=$(kubectl get pods -l app=buildah-external -n $NAMESPACE -o jsonpath='{.items[0].metadata.name}')
echo "📦 Testing pod: $POD_NAME"
# Test that the container is responsive
echo "🔍 Testing container responsiveness..."
kubectl exec $POD_NAME -n $NAMESPACE -- buildah --version
# Test graceful termination timing
echo "⏱️ Testing termination speed..."
START_TIME=$(date +%s)
echo "📤 Sending termination signal (scaling down)..."
kubectl scale deployment $DEPLOYMENT --replicas=0 -n $NAMESPACE
echo "⏳ Waiting for pod to terminate..."
kubectl wait --for=delete pod -l app=buildah-external -n $NAMESPACE --timeout=30s
END_TIME=$(date +%s)
TERMINATION_TIME=$((END_TIME - START_TIME))
echo "✅ Pod terminated in ${TERMINATION_TIME} seconds"
if [ $TERMINATION_TIME -le 10 ]; then
echo "🎉 Excellent! Graceful termination completed quickly (≤10s)"
elif [ $TERMINATION_TIME -le 30 ]; then
echo "✅ Good! Termination within acceptable time (≤30s)"
else
echo "⚠️ Slow termination (>30s) - may need optimization"
fi
echo ""
echo "🔍 Final deployment status:"
kubectl get deployment $DEPLOYMENT -n $NAMESPACE
echo ""
echo "📊 Termination Analysis:"
echo " ⏱️ Time: ${TERMINATION_TIME}s"
echo " 🎯 Target: <10s (excellent), <30s (good)"
echo " 📝 Method: Signal-aware bash loop with trap"
echo ""
if [ $TERMINATION_TIME -le 10 ]; then
echo "✅ Signal handling is working optimally!"
else
echo "💡 Consider further optimization if needed"
fi

View File

@@ -0,0 +1,104 @@
#!/bin/bash
# Test replica-based build locking mechanism
# This script demonstrates how the build system uses replicas for atomic locking
set -e
NAMESPACE="apps--droneio--prd"
DEPLOYMENT="buildah-external"
echo "🧪 Testing Replica-Based Build Locking"
echo "======================================"
# Function to get current replicas
get_replicas() {
kubectl get deployment $DEPLOYMENT -n $NAMESPACE -o jsonpath='{.spec.replicas}'
}
# Function to check if build can start
can_start_build() {
local replicas=$(get_replicas)
if [ "$replicas" = "0" ]; then
echo "✅ Build can start (replicas=0)"
return 0
else
echo "❌ Build already running (replicas=$replicas)"
return 1
fi
}
# Function to start build (scale up)
start_build() {
echo "🔒 Acquiring build lock (scaling up)..."
kubectl scale deployment $DEPLOYMENT --replicas=1 -n $NAMESPACE
echo "⏳ Waiting for pod to be ready..."
kubectl wait --for=condition=ready pod -l app=buildah-external -n $NAMESPACE --timeout=120s
echo "✅ Build lock acquired!"
}
# Function to end build (scale down)
end_build() {
echo "🔽 Releasing build lock (scaling down)..."
kubectl scale deployment $DEPLOYMENT --replicas=0 -n $NAMESPACE
echo "⏳ Waiting for pods to terminate..."
kubectl wait --for=delete pod -l app=buildah-external -n $NAMESPACE --timeout=60s || echo "Pods may still be terminating"
echo "✅ Build lock released!"
}
# Test sequence
echo "📊 Current deployment status:"
kubectl get deployment $DEPLOYMENT -n $NAMESPACE
echo ""
echo "🔍 Checking if build can start..."
if can_start_build; then
echo ""
echo "🚀 Starting test build..."
start_build
echo ""
echo "📊 Deployment during build:"
kubectl get deployment $DEPLOYMENT -n $NAMESPACE
kubectl get pods -l app=buildah-external -n $NAMESPACE
echo ""
echo "🔍 Testing concurrent build attempt..."
if can_start_build; then
echo "🚨 ERROR: Concurrent build should be blocked!"
exit 1
else
echo "✅ Concurrent build correctly blocked!"
fi
echo ""
echo "🛑 Ending test build..."
end_build
echo ""
echo "📊 Final deployment status:"
kubectl get deployment $DEPLOYMENT -n $NAMESPACE
echo ""
echo "🔍 Verifying build can start again..."
if can_start_build; then
echo "✅ Build system ready for next build!"
else
echo "🚨 ERROR: Build system not properly reset!"
exit 1
fi
else
echo ""
echo "⚠️ Cannot test - build already running"
echo "Use: kubectl scale deployment $DEPLOYMENT --replicas=0 -n $NAMESPACE"
fi
echo ""
echo "🎉 Replica-based locking test completed successfully!"
echo ""
echo "💡 Benefits:"
echo " ✅ Atomic operations (no race conditions)"
echo " ✅ No lock files to manage"
echo " ✅ Kubernetes-native approach"
echo " ✅ Resource efficient (only runs when needed)"
echo " ✅ Automatic cleanup on failure"

View File

@@ -0,0 +1,8 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
python-multipart==0.0.6
jinja2==3.1.2
ldap3==2.9.1
PyJWT==2.8.0
bcrypt==4.0.1
python-jose[cryptography]==3.3.0

View File

@@ -0,0 +1,210 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Dashboard - Artifactory</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f5f5f5;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.user-info {
display: flex;
align-items: center;
gap: 1rem;
}
.logout-btn {
background: rgba(255, 255, 255, 0.2);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
transition: background 0.3s;
}
.logout-btn:hover {
background: rgba(255, 255, 255, 0.3);
}
.container {
max-width: 1200px;
margin: 2rem auto;
padding: 0 2rem;
}
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
margin-top: 2rem;
}
.card {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
transition: transform 0.2s;
}
.card:hover {
transform: translateY(-5px);
}
.card h3 {
color: #333;
margin-bottom: 1rem;
}
.card p {
color: #666;
line-height: 1.6;
}
.api-link {
display: inline-block;
margin-top: 1rem;
padding: 0.5rem 1rem;
background: #667eea;
color: white;
text-decoration: none;
border-radius: 4px;
transition: background 0.3s;
}
.api-link:hover {
background: #5a6fd8;
}
.alert {
padding: 1rem;
margin-bottom: 1rem;
border-radius: 6px;
display: none;
}
.alert-info {
background-color: #d1ecf1;
border: 1px solid #bee5eb;
color: #0c5460;
}
</style>
</head>
<body>
<div class="header">
<h1>🏗️ Artifactory Dashboard</h1>
<div class="user-info">
<span id="userInfo">Loading...</span>
<button class="logout-btn" onclick="logout()">Logout</button>
</div>
</div>
<div class="container">
<div id="alert" class="alert alert-info">
<strong>Welcome!</strong> You have successfully authenticated with Active Directory.
</div>
<div class="cards">
<div class="card">
<h3>📦 Debian Repository</h3>
<p>Manage and distribute Debian packages for arm64 and amd64 architectures.</p>
<a href="/debian" class="api-link">Browse Packages</a>
</div>
<div class="card">
<h3>⛵ Helm Charts</h3>
<p>Store and manage Helm charts for Kubernetes deployments.</p>
<a href="/charts" class="api-link">Browse Charts</a>
</div>
<div class="card">
<h3>🐳 Docker Registry</h3>
<p>Manage Docker images and container registries.</p>
<a href="/docker" class="api-link">Browse Images</a>
</div>
<div class="card">
<h3>🔧 API Documentation</h3>
<p>Explore the REST API endpoints and interactive documentation.</p>
<a href="/docs" class="api-link">View API Docs</a>
</div>
<div class="card">
<h3>📊 Health Status</h3>
<p>Monitor the health and status of all artifactory services.</p>
<a href="/health" class="api-link">View Health</a>
</div>
<div class="card">
<h3>👥 User Management</h3>
<p>Manage users and permissions for the artifactory services.</p>
<a href="/users" class="api-link">Manage Users</a>
</div>
</div>
</div>
<script>
async function loadUserInfo() {
try {
const response = await fetch('/auth/user', {
credentials: 'include'
});
if (response.ok) {
const user = await response.json();
document.getElementById('userInfo').textContent =
`${user.display_name || user.username} (${user.email})`;
document.getElementById('alert').style.display = 'block';
} else {
// Redirect to login if not authenticated
window.location.href = '/';
}
} catch (error) {
console.error('Failed to load user info:', error);
window.location.href = '/';
}
}
async function logout() {
try {
await fetch('/auth/logout', {
credentials: 'include'
});
// Clear local storage
localStorage.removeItem('auth_token');
localStorage.removeItem('auth_timestamp');
// Redirect to login
window.location.href = '/';
} catch (error) {
console.error('Logout error:', error);
// Still redirect to login
window.location.href = '/';
}
}
// Load user info on page load
document.addEventListener('DOMContentLoaded', loadUserInfo);
</script>
</body>
</html>

View File

@@ -0,0 +1,283 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Authentication - Artifactory</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.login-container {
background: white;
padding: 2rem;
border-radius: 10px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
}
.login-header {
text-align: center;
margin-bottom: 2rem;
}
.login-header h1 {
color: #333;
margin-bottom: 0.5rem;
font-size: 2rem;
}
.login-header p {
color: #666;
font-size: 0.9rem;
}
.form-group {
margin-bottom: 1.5rem;
}
label {
display: block;
margin-bottom: 0.5rem;
color: #333;
font-weight: 500;
}
input[type="text"],
input[type="password"] {
width: 100%;
padding: 12px;
border: 2px solid #e1e1e1;
border-radius: 6px;
font-size: 1rem;
transition: border-color 0.3s ease;
}
input[type="text"]:focus,
input[type="password"]:focus {
outline: none;
border-color: #667eea;
}
.login-button {
width: 100%;
padding: 12px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: transform 0.2s ease;
}
.login-button:hover {
transform: translateY(-2px);
}
.login-button:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
.alert {
padding: 12px;
margin-bottom: 1rem;
border-radius: 6px;
display: none;
}
.alert-error {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
}
.alert-success {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
}
.loading {
display: none;
text-align: center;
margin-top: 1rem;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #667eea;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.footer {
text-align: center;
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid #e1e1e1;
color: #666;
font-size: 0.8rem;
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-header">
<h1>🔐 Sign In</h1>
<p>Access Artifactory Services</p>
</div>
<div id="alert" class="alert"></div>
<form id="loginForm">
<div class="form-group">
<label for="username">Username</label>
<input type="text" id="username" name="username" required placeholder="Enter your domain username">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" id="password" name="password" required placeholder="Enter your password">
</div>
<button type="submit" class="login-button" id="loginButton">
Sign In
</button>
</form>
<div class="loading" id="loading">
<div class="spinner"></div>
<p>Authenticating...</p>
</div>
<div class="footer">
<p>Use your Active Directory credentials</p>
</div>
</div>
<script>
const loginForm = document.getElementById('loginForm');
const loginButton = document.getElementById('loginButton');
const loading = document.getElementById('loading');
const alert = document.getElementById('alert');
function showAlert(message, type = 'error') {
alert.className = `alert alert-${type}`;
alert.textContent = message;
alert.style.display = 'block';
}
function hideAlert() {
alert.style.display = 'none';
}
function setLoading(isLoading) {
if (isLoading) {
loginButton.disabled = true;
loginButton.textContent = 'Signing In...';
loading.style.display = 'block';
} else {
loginButton.disabled = false;
loginButton.textContent = 'Sign In';
loading.style.display = 'none';
}
}
function storeToken(token) {
// Store token in localStorage for client-side access
localStorage.setItem('auth_token', token);
localStorage.setItem('auth_timestamp', new Date().getTime());
}
function redirectToApp() {
// Redirect to the main application or dashboard
const returnUrl = new URLSearchParams(window.location.search).get('return_url');
window.location.href = returnUrl || '/dashboard';
}
loginForm.addEventListener('submit', async (e) => {
e.preventDefault();
hideAlert();
setLoading(true);
const formData = new FormData(loginForm);
try {
const response = await fetch('/auth/login', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.success) {
// Store token if provided in header
const token = response.headers.get('X-Auth-Token');
if (token) {
storeToken(token);
}
showAlert(`Welcome back, ${data.user.display_name || data.user.username}!`, 'success');
// Redirect after a short delay
setTimeout(() => {
redirectToApp();
}, 1500);
} else {
showAlert(data.message || 'Login failed');
}
} catch (error) {
console.error('Login error:', error);
showAlert('Network error. Please try again.');
} finally {
setLoading(false);
}
});
// Check if user is already authenticated
document.addEventListener('DOMContentLoaded', async () => {
try {
const response = await fetch('/auth/user', {
credentials: 'include'
});
if (response.ok) {
const user = await response.json();
showAlert(`Already signed in as ${user.display_name || user.username}`, 'success');
setTimeout(() => {
redirectToApp();
}, 1500);
}
} catch (error) {
// User not authenticated, show login form
console.log('User not authenticated');
}
});
</script>
</body>
</html>

View File

@@ -0,0 +1,46 @@
#!/bin/bash
# Test script to verify Drone Jsonnet configuration
echo "🔍 Testing Drone Jsonnet Configuration..."
echo "========================================="
echo "1. Checking .drone.jsonnet syntax..."
if jsonnet .drone.jsonnet > /dev/null 2>&1; then
echo "✅ .drone.jsonnet syntax is valid"
else
echo "❌ .drone.jsonnet syntax error"
jsonnet .drone.jsonnet
exit 1
fi
echo ""
echo "2. Checking Drone server status..."
kubectl get pods -n apps--droneio--prd -l app=droneio
echo ""
echo "3. Checking Drone configuration..."
echo "DRONE_JSONNET_ENABLED: $(kubectl get configmap drone -n apps--droneio--prd -o jsonpath='{.data.DRONE_JSONNET_ENABLED}')"
echo "DRONE_JSONNET_IMPORT_PATHS: $(kubectl get configmap drone -n apps--droneio--prd -o jsonpath='{.data.DRONE_JSONNET_IMPORT_PATHS}')"
echo ""
echo "4. Recent Drone server logs (looking for errors)..."
kubectl logs deployment/droneio -n apps--droneio--prd --tail=10 | grep -i "error\|warning\|jsonnet" || echo "No error/warning/jsonnet logs found"
echo ""
echo "5. Checking repository files..."
echo "Files in repository root:"
ls -la .drone*
echo ""
echo "Files in pipeline directory:"
ls -la pipeline/*.libsonnet 2>/dev/null || echo "No .libsonnet files found"
echo ""
echo "6. Testing jsonnet compilation with output..."
echo "Generated YAML pipeline:"
echo "------------------------"
jsonnet .drone.jsonnet | head -20
echo "... (output truncated)"
echo ""
echo "🏁 Test completed. If syntax is valid but builds aren't triggering,"
echo " the issue is likely with webhook configuration between Gitea and Drone."

View File

@@ -0,0 +1,33 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: apps--droneio--prd
name: drone-build-role
rules:
# Existing permissions
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"]
# NEW: Add deployment scaling permissions
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments/scale"]
verbs: ["get", "update", "patch"]
# NEW: Add permissions to wait for pods to be ready
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch", "create", "delete"]

View File

@@ -0,0 +1,17 @@
# Version configuration for auth-service
# This file defines the base version for automated builds
BASE_VERSION=1.0
DOCKER_REPO=docker.aipice.fr/hexah/auth-service
# Build configuration
BUILD_ON_BRANCHES=main,master
TEST_ON_BRANCHES=main,master,develop,feature/*
# Docker registry configuration
REGISTRY_URL=docker.io
REGISTRY_NAMESPACE=hexah
# Deployment configuration
DEPLOY_NAMESPACE=infrastructure--artifactory--service
DEPLOY_SERVICE=auth-service

22
arti-api/build.sh Executable file
View File

@@ -0,0 +1,22 @@
#!/bin/bash
# Build script for Arti-API
set -e
echo "Building Arti-API container..."
# Build the Docker image
docker build -t hexah/arti-api:1.0.0 .
echo "Build completed successfully!"
echo ""
echo "To run the container:"
echo " docker-compose up -d"
echo ""
echo "To deploy to Kubernetes:"
echo " kubectl apply -f kubernetes.yaml"
echo ""
echo "API will be available at:"
echo " http://localhost:8000"
echo " API docs: http://localhost:8000/docs"

161
arti-api/deploy-traefik.sh Executable file
View File

@@ -0,0 +1,161 @@
#!/bin/bash
# Traefik v2 IngressRoute Deployment Script for Artifactory
# Deploys the complete artifactory stack with Traefik-based access control
set -e
echo "🚀 Deploying Artifactory with Traefik v2 IngressRoute..."
echo "=================================================="
echo ""
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Configuration
NAMESPACE="artifactory"
TRAEFIK_VERSION="simple" # Change to "full" for full-featured version
echo "📋 Configuration:"
echo " Namespace: $NAMESPACE"
echo " Internal Network: 192.168.100.0/24"
echo " Traefik Version: $TRAEFIK_VERSION"
echo ""
# Check prerequisites
echo "🔍 Checking prerequisites..."
if ! command -v kubectl &> /dev/null; then
echo -e "${RED}kubectl not found. Please install kubectl first.${NC}"
exit 1
fi
# Check if Traefik is running
TRAEFIK_PODS=$(kubectl get pods -A -l app.kubernetes.io/name=traefik --no-headers 2>/dev/null | wc -l)
if [ "$TRAEFIK_PODS" -eq 0 ]; then
echo -e "⚠️ ${YELLOW}Warning: No Traefik pods found. Make sure Traefik v2 is installed.${NC}"
echo " You can install Traefik with:"
echo " helm repo add traefik https://helm.traefik.io/traefik"
echo " helm install traefik traefik/traefik"
echo ""
fi
# Create namespace if it doesn't exist
echo "📦 Creating namespace..."
kubectl create namespace $NAMESPACE --dry-run=client -o yaml | kubectl apply -f -
# Deploy the base services (without ingress)
echo "🏗️ Deploying base services..."
kubectl apply -f kubernetes.yaml -n $NAMESPACE 2>/dev/null || echo " Note: Base services might already exist"
# Wait for services to be ready
echo "⏳ Waiting for services to be ready..."
kubectl wait --for=condition=available --timeout=120s deployment/arti-api -n $NAMESPACE 2>/dev/null || echo " Arti-API deployment not found, continuing..."
# Deploy Traefik IngressRoute
echo "🌐 Deploying Traefik IngressRoute..."
if [ "$TRAEFIK_VERSION" = "full" ]; then
kubectl apply -f traefik-ingressroute.yaml
echo -e "${GREEN}Full-featured Traefik configuration deployed${NC}"
else
kubectl apply -f traefik-simple.yaml
echo -e "${GREEN}Simplified Traefik configuration deployed${NC}"
fi
# Check deployment status
echo ""
echo "📊 Deployment Status:"
echo " Deployments:"
kubectl get deployments -n $NAMESPACE 2>/dev/null | grep -E "(NAME|arti-api|chartmuseum|docker-registry|error-service)" || echo " No deployments found"
echo " Services:"
kubectl get services -n $NAMESPACE 2>/dev/null | grep -E "(NAME|arti-api|chartmuseum|docker-registry|error-service)" || echo " No services found"
echo " IngressRoutes:"
kubectl get ingressroute -n $NAMESPACE 2>/dev/null | grep -E "(NAME|arti|chart|registry)" || echo " No IngressRoutes found"
echo " Middlewares:"
kubectl get middleware -n $NAMESPACE 2>/dev/null | grep -E "(NAME|internal|external|block)" || echo " No middlewares found"
echo ""
# Get Traefik external IP/URL
TRAEFIK_SERVICE=$(kubectl get svc -A -l app.kubernetes.io/name=traefik --no-headers 2>/dev/null | head -1)
if [ -n "$TRAEFIK_SERVICE" ]; then
TRAEFIK_IP=$(echo $TRAEFIK_SERVICE | awk '{print $5}')
echo -e "🌐 ${BLUE}Traefik Service Info:${NC}"
echo " $TRAEFIK_SERVICE"
echo ""
fi
# Display access information
echo "🎯 Access Information:"
echo ""
echo -e "📱 ${GREEN}Service URLs:${NC}"
echo " 🔧 Arti-API: http://api.artifactory.local"
echo " 📚 API Docs: http://api.artifactory.local/docs"
echo " ⛵ Chart Museum: http://charts.artifactory.local"
echo " 🐳 Docker Registry: http://registry.artifactory.local"
echo ""
echo -e "🔐 ${YELLOW}Access Control:${NC}"
echo " 🏠 Internal Network (192.168.100.0/24): Full access to all endpoints"
echo " 🌐 External Network: Limited to health endpoints only"
echo ""
echo -e "${GREEN}Health Endpoints (External Access):${NC}"
echo " curl http://api.artifactory.local/health"
echo " curl http://charts.artifactory.local/health"
echo " curl http://registry.artifactory.local/v2/"
echo ""
echo -e "🚫 ${RED}Blocked Endpoints (External Access):${NC}"
echo " curl http://api.artifactory.local/users # Returns 403"
echo " curl http://charts.artifactory.local/api/charts # Returns 403"
echo " curl http://registry.artifactory.local/v2/myapp/ # Returns 403"
echo ""
echo -e "🏠 ${GREEN}Internal Network Examples (192.168.100.x):${NC}"
echo " curl http://api.artifactory.local/users # Full access"
echo " curl http://charts.artifactory.local/api/charts # Full access"
echo " docker login registry.artifactory.local # Full access"
echo ""
echo -e "🔧 ${BLUE}DNS Configuration:${NC}"
echo " Add these entries to your /etc/hosts or DNS server:"
echo " $TRAEFIK_IP api.artifactory.local"
echo " $TRAEFIK_IP charts.artifactory.local"
echo " $TRAEFIK_IP registry.artifactory.local"
echo ""
echo -e "📋 ${BLUE}Management Commands:${NC}"
echo " # View IngressRoute details:"
echo " kubectl describe ingressroute -n $NAMESPACE"
echo ""
echo " # Check middleware configuration:"
echo " kubectl get middleware -n $NAMESPACE -o yaml"
echo ""
echo " # View Traefik dashboard (if enabled):"
echo " kubectl port-forward -n traefik service/traefik 9000:9000"
echo " # Then access: http://localhost:9000/dashboard/"
echo ""
echo " # Test from internal network:"
echo " kubectl run test-internal --rm -i --tty --image=curlimages/curl -- sh"
echo ""
echo " # Clean up:"
echo " kubectl delete ingressroute,middleware,configmap,deployment,service -n $NAMESPACE -l app=error-service"
echo " kubectl delete -f traefik-${TRAEFIK_VERSION}.yaml"
echo ""
echo -e "🎉 ${GREEN}Traefik IngressRoute deployment completed!${NC}"
echo ""
echo -e "📖 ${BLUE}Next Steps:${NC}"
echo " 1. Configure DNS entries for the artifactory domains"
echo " 2. Test access from internal network (192.168.100.x)"
echo " 3. Verify external access is properly restricted"
echo " 4. Set up TLS certificates for production use"
echo " 5. Configure Traefik dashboard access if needed"

View File

@@ -0,0 +1,99 @@
version: '3.8'
services:
# Arti-API for management
arti-api:
build: .
container_name: arti-api
ports:
- "8000:8000"
volumes:
- artifactory_data:/data
environment:
- PYTHONUNBUFFERED=1
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
restart: unless-stopped
# Chart Museum with htpasswd authentication
chartmuseum:
image: chartmuseum/chartmuseum:latest
container_name: chartmuseum
environment:
# Storage configuration
- STORAGE=local
- STORAGE_LOCAL_ROOTDIR=/data/charts
- PORT=8080
# Authentication with htpasswd
- AUTH_ANONYMOUS_GET=false
- HTPASSWD_PATH=/data/htpasswd
- AUTH_REALM=Chart Museum
# Features
- ALLOW_OVERWRITE=true
- DISABLE_API=false
- DISABLE_METRICS=false
- LOG_JSON=true
- DEBUG=false
# CORS settings (optional)
- CORS_ALLOW_ORIGIN=*
ports:
- "8080:8080"
volumes:
- artifactory_data:/data
depends_on:
arti-api:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
# Docker Registry with htpasswd authentication
registry:
image: registry:2
container_name: docker-registry
environment:
# Storage
- REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/data/docker
# Authentication
- REGISTRY_AUTH=htpasswd
- REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm
- REGISTRY_AUTH_HTPASSWD_PATH=/data/htpasswd
# Network
- REGISTRY_HTTP_ADDR=0.0.0.0:5000
- REGISTRY_HTTP_HEADERS_Access-Control-Allow-Origin=['*']
- REGISTRY_HTTP_HEADERS_Access-Control-Allow-Methods=['HEAD','GET','OPTIONS','DELETE']
- REGISTRY_HTTP_HEADERS_Access-Control-Allow-Headers=['Authorization','Accept','Cache-Control']
- REGISTRY_HTTP_HEADERS_Access-Control-Max-Age=[1728000]
- REGISTRY_HTTP_HEADERS_Access-Control-Allow-Credentials=[true]
- REGISTRY_HTTP_HEADERS_Access-Control-Expose-Headers=['Docker-Content-Digest']
ports:
- "5000:5000"
volumes:
- artifactory_data:/data
depends_on:
arti-api:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:5000/v2/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
volumes:
artifactory_data:
driver: local

View File

@@ -0,0 +1,22 @@
version: '3.8'
services:
arti-api:
build: .
ports:
- "8000:8000"
volumes:
- artifactory_data:/data
environment:
- PYTHONUNBUFFERED=1
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
restart: unless-stopped
volumes:
artifactory_data:
driver: local

View File

@@ -0,0 +1,405 @@
apiVersion: v1
kind: Namespace
metadata:
name: artifactory
labels:
name: artifactory
---
# Network Policy for Artifactory Services
# Allows internal network (192.168.100.0/24) full access
# Restricts external access to health endpoints only
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: artifactory-access-control
namespace: artifactory
spec:
podSelector:
matchLabels:
tier: artifactory
policyTypes:
- Ingress
- Egress
ingress:
# Rule 1: Allow full access from internal network
- from:
- ipBlock:
cidr: 192.168.100.0/24
ports:
- protocol: TCP
port: 8000 # Arti-API
- protocol: TCP
port: 8080 # Chart Museum
- protocol: TCP
port: 5000 # Docker Registry
# Rule 2: Allow inter-pod communication within namespace
- from:
- namespaceSelector:
matchLabels:
name: artifactory
- podSelector: {}
ports:
- protocol: TCP
port: 8000
- protocol: TCP
port: 8080
- protocol: TCP
port: 5000
# Rule 3: Allow external access to health endpoints only
# Note: This allows access to all ports but should be combined
# with Ingress controller path filtering for full security
- from: []
ports:
- protocol: TCP
port: 8000 # For /health endpoint
- protocol: TCP
port: 8080 # For /health endpoint
- protocol: TCP
port: 5000 # For /v2/ endpoint
egress:
# Allow DNS resolution
- to: []
ports:
- protocol: TCP
port: 53
- protocol: UDP
port: 53
# Allow outbound HTTP/HTTPS for package downloads
- to: []
ports:
- protocol: TCP
port: 80
- protocol: TCP
port: 443
# Allow inter-pod communication
- to:
- namespaceSelector:
matchLabels:
name: artifactory
- podSelector: {}
---
# Ingress with path-based restrictions
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: artifactory-ingress
namespace: artifactory
annotations:
kubernetes.io/ingress.class: "nginx"
# Configuration to restrict external access to specific paths
nginx.ingress.kubernetes.io/configuration-snippet: |
# Allow internal network full access
if ($remote_addr ~ "^192\.168\.100\.") {
set $internal_access 1;
}
# For external access, only allow specific paths
if ($internal_access != 1) {
# Block access to management endpoints
if ($uri ~ "^/(users|debian|helm|refresh|docs|redoc|openapi\.json)") {
return 403 "Access denied - Internal network only";
}
# Block Chart Museum API endpoints
if ($uri ~ "^/api/") {
return 403 "Access denied - Internal network only";
}
# Block Docker Registry push/pull (only allow health check)
if ($uri ~ "^/v2/.*/(manifests|blobs)") {
return 403 "Access denied - Internal network only";
}
}
spec:
rules:
- host: artifactory.local
http:
paths:
# Arti-API
- path: /
pathType: Prefix
backend:
service:
name: arti-api-service
port:
number: 8000
- host: charts.artifactory.local
http:
paths:
# Chart Museum
- path: /
pathType: Prefix
backend:
service:
name: chartmuseum-service
port:
number: 8080
- host: registry.artifactory.local
http:
paths:
# Docker Registry
- path: /
pathType: Prefix
backend:
service:
name: docker-registry-service
port:
number: 5000
---
# Update existing deployments to include tier label
apiVersion: apps/v1
kind: Deployment
metadata:
name: arti-api
namespace: artifactory
labels:
app: arti-api
tier: artifactory
spec:
replicas: 1
selector:
matchLabels:
app: arti-api
tier: artifactory
template:
metadata:
labels:
app: arti-api
tier: artifactory
spec:
containers:
- name: arti-api
image: hexah/arti-api:1.0.1
ports:
- containerPort: 8000
env:
- name: PYTHONUNBUFFERED
value: "1"
volumeMounts:
- name: artifactory-storage
mountPath: /data
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
volumes:
- name: artifactory-storage
persistentVolumeClaim:
claimName: artifactory-pvc
---
apiVersion: v1
kind: Service
metadata:
name: arti-api-service
namespace: artifactory
labels:
app: arti-api
tier: artifactory
spec:
type: ClusterIP
ports:
- port: 8000
targetPort: 8000
protocol: TCP
selector:
app: arti-api
tier: artifactory
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: chartmuseum
namespace: artifactory
labels:
app: chartmuseum
tier: artifactory
spec:
replicas: 1
selector:
matchLabels:
app: chartmuseum
tier: artifactory
template:
metadata:
labels:
app: chartmuseum
tier: artifactory
spec:
containers:
- name: chartmuseum
image: chartmuseum/chartmuseum:latest
ports:
- containerPort: 8080
env:
- name: STORAGE
value: "local"
- name: STORAGE_LOCAL_ROOTDIR
value: "/data/charts"
- name: PORT
value: "8080"
- name: AUTH_ANONYMOUS_GET
value: "false"
- name: HTPASSWD_PATH
value: "/data/htpasswd"
- name: AUTH_REALM
value: "Chart Museum"
- name: ALLOW_OVERWRITE
value: "true"
- name: DISABLE_API
value: "false"
volumeMounts:
- name: artifactory-storage
mountPath: /data
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
volumes:
- name: artifactory-storage
persistentVolumeClaim:
claimName: artifactory-pvc
---
apiVersion: v1
kind: Service
metadata:
name: chartmuseum-service
namespace: artifactory
labels:
app: chartmuseum
tier: artifactory
spec:
type: ClusterIP
ports:
- port: 8080
targetPort: 8080
protocol: TCP
selector:
app: chartmuseum
tier: artifactory
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: docker-registry
namespace: artifactory
labels:
app: docker-registry
tier: artifactory
spec:
replicas: 1
selector:
matchLabels:
app: docker-registry
tier: artifactory
template:
metadata:
labels:
app: docker-registry
tier: artifactory
spec:
containers:
- name: registry
image: registry:2
ports:
- containerPort: 5000
env:
- name: REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY
value: "/data/docker"
- name: REGISTRY_AUTH
value: "htpasswd"
- name: REGISTRY_AUTH_HTPASSWD_REALM
value: "Registry Realm"
- name: REGISTRY_AUTH_HTPASSWD_PATH
value: "/data/htpasswd"
- name: REGISTRY_HTTP_ADDR
value: "0.0.0.0:5000"
volumeMounts:
- name: artifactory-storage
mountPath: /data
livenessProbe:
httpGet:
path: /v2/
port: 5000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /v2/
port: 5000
initialDelaySeconds: 5
periodSeconds: 5
resources:
requests:
memory: "128Mi"
cpu: "100m"
limits:
memory: "256Mi"
cpu: "200m"
volumes:
- name: artifactory-storage
persistentVolumeClaim:
claimName: artifactory-pvc
---
apiVersion: v1
kind: Service
metadata:
name: docker-registry-service
namespace: artifactory
labels:
app: docker-registry
tier: artifactory
spec:
type: ClusterIP
ports:
- port: 5000
targetPort: 5000
protocol: TCP
selector:
app: docker-registry
tier: artifactory
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: artifactory-pvc
namespace: artifactory
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi
storageClassName: "" # Specify your storage class here

77
arti-api/kubernetes.yaml Normal file
View File

@@ -0,0 +1,77 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: arti-api
labels:
app: arti-api
spec:
replicas: 1
selector:
matchLabels:
app: arti-api
template:
metadata:
labels:
app: arti-api
spec:
containers:
- name: arti-api
image: arti-api:latest
ports:
- containerPort: 8000
env:
- name: PYTHONUNBUFFERED
value: "1"
volumeMounts:
- name: artifactory-storage
mountPath: /data
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
volumes:
- name: artifactory-storage
persistentVolumeClaim:
claimName: artifactory-pvc
---
apiVersion: v1
kind: Service
metadata:
name: arti-api-service
labels:
app: arti-api
spec:
type: ClusterIP
ports:
- port: 8000
targetPort: 8000
protocol: TCP
selector:
app: arti-api
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: artifactory-pvc
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 10Gi
storageClassName: "" # Specify your storage class here

View File

@@ -0,0 +1,5 @@
fastapi==0.104.1
uvicorn[standard]==0.24.0
python-multipart==0.0.6
pydantic==2.5.0
bcrypt==4.1.2

35
arti-api/serve-docs.sh Executable file
View File

@@ -0,0 +1,35 @@
#!/bin/bash
# Swagger Documentation Server
# This script starts a simple HTTP server to serve the OpenAPI documentation
echo "Starting Swagger Documentation Server..."
echo "Building the container first..."
# Build the container
docker build -t arti-api:latest .
# Start the container in the background
echo "Starting Arti-API container..."
docker run -d \
--name arti-api-docs \
-p 8000:8000 \
-v $(pwd)/data:/data \
arti-api:latest
# Wait a moment for the server to start
sleep 3
echo ""
echo "🚀 Arti-API Documentation is now available at:"
echo ""
echo " 📖 Interactive API Docs (Swagger UI): http://localhost:8000/docs"
echo " 📋 Alternative Docs (ReDoc): http://localhost:8000/redoc"
echo " 🔧 OpenAPI JSON Schema: http://localhost:8000/openapi.json"
echo " ❤️ Health Check: http://localhost:8000/health"
echo ""
echo "To stop the documentation server:"
echo " docker stop arti-api-docs && docker rm arti-api-docs"
echo ""
echo "To view logs:"
echo " docker logs -f arti-api-docs"

89
arti-api/setup-full-stack.sh Executable file
View File

@@ -0,0 +1,89 @@
#!/bin/bash
# Complete Artifactory Setup with Authentication
# This script sets up the full artifactory stack with Chart Museum authentication
set -e
echo "🚀 Setting up Complete Artifactory Stack with Authentication..."
echo ""
# Build the Arti-API container
echo "📦 Building Arti-API container..."
docker build -t arti-api:latest .
# Start the complete stack
echo "🔧 Starting the complete artifactory stack..."
docker-compose -f docker-compose-full.yaml up -d
# Wait for services to be ready
echo "⏳ Waiting for services to start..."
sleep 15
# Create initial users
echo "👥 Creating initial users..."
# Check if Arti-API is ready
until curl -s http://localhost:8000/health > /dev/null; do
echo " Waiting for Arti-API to be ready..."
sleep 5
done
# Create admin user
echo " Creating admin user..."
curl -s -X POST "http://localhost:8000/users" \
-H "Content-Type: application/json" \
-d '{"username": "admin", "password": "admin123"}' > /dev/null
# Create developer user
echo " Creating developer user..."
curl -s -X POST "http://localhost:8000/users" \
-H "Content-Type: application/json" \
-d '{"username": "developer", "password": "dev123"}' > /dev/null
# Create readonly user
echo " Creating readonly user..."
curl -s -X POST "http://localhost:8000/users" \
-H "Content-Type: application/json" \
-d '{"username": "readonly", "password": "read123"}' > /dev/null
echo ""
echo "✅ Artifactory stack setup complete!"
echo ""
echo "🌐 Services Available:"
echo " 📖 Arti-API (Management): http://localhost:8000"
echo " 📚 API Documentation: http://localhost:8000/docs"
echo " ⛵ Chart Museum: http://localhost:8080"
echo " 🐳 Docker Registry: http://localhost:5000"
echo ""
echo "🔐 Default Users Created:"
echo " 👑 admin:admin123 (Full access)"
echo " 👨‍💻 developer:dev123 (Development access)"
echo " 👀 readonly:read123 (Read-only access)"
echo ""
echo "🧪 Test Commands:"
echo ""
echo " # Test Chart Museum with authentication:"
echo " curl -u admin:admin123 http://localhost:8080/api/charts"
echo ""
echo " # Test Docker Registry with authentication:"
echo " docker login localhost:5000"
echo " # Username: admin, Password: admin123"
echo ""
echo " # Add Helm repository with authentication:"
echo " helm repo add myrepo http://admin:admin123@localhost:8080"
echo ""
echo " # List users via API:"
echo " curl http://localhost:8000/users"
echo ""
echo "📋 Management:"
echo " # Stop all services:"
echo " docker-compose -f docker-compose-full.yaml down"
echo ""
echo " # View logs:"
echo " docker-compose -f docker-compose-full.yaml logs -f"
echo ""
echo " # Manage users via API:"
echo " curl -X POST http://localhost:8000/users -H 'Content-Type: application/json' -d '{\"username\": \"newuser\", \"password\": \"newpass\"}'"
echo ""
echo "🎉 Your authenticated artifactory is ready!"

165
arti-api/test-network-policies.sh Executable file
View File

@@ -0,0 +1,165 @@
#!/bin/bash
# Test script for Kubernetes Network Policies
# Tests access control for artifactory services
set -e
echo "🔒 Testing Kubernetes Network Policies for Artifactory"
echo "=================================================="
echo ""
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Configuration
NAMESPACE="artifactory"
INTERNAL_TEST_IP="192.168.100.50" # Adjust to your internal network
EXTERNAL_TEST_IP="8.8.8.8" # Simulated external IP
echo "📋 Configuration:"
echo " Namespace: $NAMESPACE"
echo " Internal Network: 192.168.100.0/24"
echo " Test Internal IP: $INTERNAL_TEST_IP"
echo " Test External IP: $EXTERNAL_TEST_IP"
echo ""
# Check if kubectl is available
if ! command -v kubectl &> /dev/null; then
echo "❌ kubectl not found. Please install kubectl first."
exit 1
fi
# Check if namespace exists
if ! kubectl get namespace $NAMESPACE &> /dev/null; then
echo "❌ Namespace '$NAMESPACE' not found."
echo " Please deploy the services first:"
echo " kubectl apply -f kubernetes-with-network-policy.yaml"
exit 1
fi
echo "🔍 Checking deployed resources..."
# Check deployments
echo " Deployments:"
kubectl get deployments -n $NAMESPACE | grep -E "(NAME|arti-api|chartmuseum|docker-registry)" || echo " No deployments found"
# Check services
echo " Services:"
kubectl get services -n $NAMESPACE | grep -E "(NAME|arti-api|chartmuseum|docker-registry)" || echo " No services found"
# Check network policies
echo " Network Policies:"
kubectl get networkpolicies -n $NAMESPACE | grep -E "(NAME|artifactory)" || echo " No network policies found"
echo ""
# Function to test endpoint access
test_endpoint() {
local service=$1
local port=$2
local path=$3
local description=$4
local expected_result=$5
echo -n " Testing $description... "
# Create a test pod to simulate network access
kubectl run test-pod-$RANDOM --rm -i --image=curlimages/curl --restart=Never --quiet -- \
curl -s -o /dev/null -w "%{http_code}" --connect-timeout 5 \
"http://$service.$NAMESPACE.svc.cluster.local:$port$path" 2>/dev/null || echo "000"
}
echo "🧪 Testing Network Access..."
echo ""
# Test internal network access (simulated)
echo "🏠 Internal Network Tests (192.168.100.x should have full access):"
# Note: In a real environment, you would run these tests from pods with the correct source IP
echo " ${YELLOW}Note: These tests run from within the cluster${NC}"
echo " ${YELLOW}In production, source IP filtering would be handled by Ingress${NC}"
# Test health endpoints (should always work)
echo " Health Endpoints (should be accessible):"
kubectl run test-health --rm -i --image=curlimages/curl --restart=Never --quiet -- \
curl -s -f "http://arti-api-service.$NAMESPACE.svc.cluster.local:8000/health" && \
echo -e "${GREEN}Arti-API health endpoint accessible${NC}" || \
echo -e "${RED}Arti-API health endpoint failed${NC}"
kubectl run test-cm-health --rm -i --image=curlimages/curl --restart=Never --quiet -- \
curl -s -f "http://chartmuseum-service.$NAMESPACE.svc.cluster.local:8080/health" && \
echo -e "${GREEN}Chart Museum health endpoint accessible${NC}" || \
echo -e "${RED}Chart Museum health endpoint failed${NC}"
kubectl run test-reg-health --rm -i --image=curlimages/curl --restart=Never --quiet -- \
curl -s -f "http://docker-registry-service.$NAMESPACE.svc.cluster.local:5000/v2/" && \
echo -e "${GREEN}Docker Registry health endpoint accessible${NC}" || \
echo -e "${RED}Docker Registry health endpoint failed${NC}"
echo ""
# Test management endpoints (should work from internal network)
echo " Management Endpoints (should be accessible from internal network):"
kubectl run test-users --rm -i --image=curlimages/curl --restart=Never --quiet -- \
curl -s -f "http://arti-api-service.$NAMESPACE.svc.cluster.local:8000/users" && \
echo -e "${GREEN}Arti-API users endpoint accessible${NC}" || \
echo -e "${RED}Arti-API users endpoint failed${NC}"
echo ""
echo "🌐 Network Policy Verification:"
# Check if network policies are applied
NP_COUNT=$(kubectl get networkpolicies -n $NAMESPACE --no-headers 2>/dev/null | wc -l)
if [ "$NP_COUNT" -gt 0 ]; then
echo -e "${GREEN}Network policies are deployed ($NP_COUNT policies)${NC}"
kubectl get networkpolicies -n $NAMESPACE
else
echo -e "${RED}No network policies found${NC}"
fi
echo ""
echo "📋 Network Policy Details:"
kubectl describe networkpolicy -n $NAMESPACE 2>/dev/null || echo " No network policies to describe"
echo ""
echo "🔧 Manual Testing Commands:"
echo ""
echo " # Test from internal network (run from a pod with source IP 192.168.100.x):"
echo " kubectl run internal-test --rm -i --tty --image=curlimages/curl -- sh"
echo " # Then inside the pod:"
echo " curl http://arti-api-service.$NAMESPACE.svc.cluster.local:8000/users"
echo ""
echo " # Test external access through Ingress (if configured):"
echo " curl http://artifactory.local/health # Should work"
echo " curl http://artifactory.local/users # Should be blocked (403)"
echo ""
echo " # Check pod labels (must match NetworkPolicy selector):"
echo " kubectl get pods -n $NAMESPACE --show-labels"
echo ""
echo " # Verify network policy application:"
echo " kubectl get networkpolicies -n $NAMESPACE -o yaml"
echo ""
echo "📚 Next Steps:"
echo " 1. Configure Ingress controller with path-based filtering"
echo " 2. Test from actual internal network (192.168.100.x)"
echo " 3. Verify external access is properly restricted"
echo " 4. Monitor network policy logs if available"
echo ""
echo "✅ Network Policy test completed!"
echo ""
echo "🔒 Security Summary:"
echo " - NetworkPolicy restricts traffic at network layer"
echo " - Ingress controller provides HTTP path filtering"
echo " - Internal network (192.168.100.0/24) has full access"
echo " - External access limited to health endpoints"
echo " - Inter-pod communication allowed within namespace"

View File

@@ -0,0 +1,330 @@
# Traefik v2 IngressRoute Configuration for Artifactory Services
# Allows internal network (192.168.100.0/24) full access
# Restricts external access to health endpoints only
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: arti-api-ingressroute
namespace: artifactory
spec:
entryPoints:
- web
- websecure
routes:
# Route for health endpoints (accessible externally)
- match: Host(`api.artifactory.local`) && (Path(`/`) || Path(`/health`))
kind: Rule
services:
- name: arti-api-service
port: 8000
middlewares:
- name: api-health-headers
# Route for all other endpoints (internal network only)
- match: Host(`api.artifactory.local`) && !ClientIP(`192.168.100.0/24`)
kind: Rule
services:
- name: arti-api-service
port: 8000
middlewares:
- name: block-external-management
# Route for internal network (full access)
- match: Host(`api.artifactory.local`) && ClientIP(`192.168.100.0/24`)
kind: Rule
services:
- name: arti-api-service
port: 8000
middlewares:
- name: internal-access-headers
tls:
secretName: artifactory-tls
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: docker-registry-ingressroute
namespace: artifactory
spec:
entryPoints:
- web
- websecure
routes:
# Route for health endpoint (accessible externally)
- match: Host(`registry.artifactory.local`) && Path(`/v2/`)
kind: Rule
services:
- name: docker-registry-service
port: 5000
middlewares:
- name: registry-health-headers
# Block external access to push/pull operations
- match: Host(`registry.artifactory.local`) && (PathPrefix(`/v2/`) && !Path(`/v2/`)) && !ClientIP(`192.168.100.0/24`)
kind: Rule
services:
- name: docker-registry-service
port: 5000
middlewares:
- name: block-external-registry-ops
# Route for internal network (full access)
- match: Host(`registry.artifactory.local`) && ClientIP(`192.168.100.0/24`)
kind: Rule
services:
- name: docker-registry-service
port: 5000
middlewares:
- name: internal-access-headers
tls:
secretName: artifactory-tls
---
# Middleware to add security headers for health endpoints
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: api-health-headers
namespace: artifactory
spec:
headers:
customRequestHeaders:
X-Access-Type: "external-health"
customResponseHeaders:
X-Allowed-Endpoints: "health-only"
X-Access-Level: "limited"
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: charts-health-headers
namespace: artifactory
spec:
headers:
customRequestHeaders:
X-Access-Type: "external-health"
customResponseHeaders:
X-Allowed-Endpoints: "health-only"
X-Access-Level: "limited"
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: registry-health-headers
namespace: artifactory
spec:
headers:
customRequestHeaders:
X-Access-Type: "external-health"
customResponseHeaders:
X-Allowed-Endpoints: "health-only"
X-Access-Level: "limited"
---
# Middleware to block external access to management endpoints
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: block-external-management
namespace: artifactory
spec:
errors:
status:
- "403"
service:
name: error-service
port: 80
query: "/403.html"
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: block-external-charts-api
namespace: artifactory
spec:
errors:
status:
- "403"
service:
name: error-service
port: 80
query: "/403.html"
---
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: block-external-registry-ops
namespace: artifactory
spec:
errors:
status:
- "403"
service:
name: error-service
port: 80
query: "/403.html"
---
# Middleware for internal network access
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: internal-access-headers
namespace: artifactory
spec:
headers:
customRequestHeaders:
X-Access-Type: "internal"
customResponseHeaders:
X-Access-Level: "full"
X-Network: "internal"
---
# Middleware for external Chart Museum access (limited)
apiVersion: traefik.containo.us/v1alpha1
kind: Middleware
metadata:
name: charts-external-access
namespace: artifactory
spec:
headers:
customRequestHeaders:
X-Access-Type: "external-limited"
customResponseHeaders:
X-Access-Level: "read-only"
X-Blocked-Paths: "/api/*"
---
# Error service for displaying 403 pages
apiVersion: apps/v1
kind: Deployment
metadata:
name: error-service
namespace: artifactory
labels:
app: error-service
spec:
replicas: 1
selector:
matchLabels:
app: error-service
template:
metadata:
labels:
app: error-service
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: error-pages
mountPath: /usr/share/nginx/html
resources:
requests:
memory: "32Mi"
cpu: "50m"
limits:
memory: "64Mi"
cpu: "100m"
volumes:
- name: error-pages
configMap:
name: error-pages-config
---
apiVersion: v1
kind: Service
metadata:
name: error-service
namespace: artifactory
labels:
app: error-service
spec:
type: ClusterIP
ports:
- port: 80
targetPort: 80
protocol: TCP
selector:
app: error-service
---
# ConfigMap with custom error pages
apiVersion: v1
kind: ConfigMap
metadata:
name: error-pages-config
namespace: artifactory
data:
403.html: |
<!DOCTYPE html>
<html>
<head>
<title>Access Denied - Artifactory</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
padding: 50px;
background-color: #f8f9fa;
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.error-code {
font-size: 4em;
color: #dc3545;
margin-bottom: 20px;
}
.error-message {
font-size: 1.5em;
color: #333;
margin-bottom: 20px;
}
.error-description {
color: #666;
margin-bottom: 30px;
}
.access-info {
background: #e3f2fd;
padding: 20px;
border-radius: 4px;
border-left: 4px solid #2196f3;
}
</style>
</head>
<body>
<div class="container">
<div class="error-code">403</div>
<div class="error-message">Access Denied</div>
<div class="error-description">
This endpoint is restricted to internal network access only.
</div>
<div class="access-info">
<strong>For Internal Network Users (192.168.100.0/24):</strong><br>
You have full access to all management endpoints.<br><br>
<strong>For External Users:</strong><br>
Only health check endpoints are available:
<ul style="text-align: left; display: inline-block;">
<li>API Health: <code>/health</code></li>
<li>Chart Museum: <code>/health</code></li>
<li>Docker Registry: <code>/v2/</code></li>
</ul>
</div>
</div>
</body>
</html>
index.html: |
<!DOCTYPE html>
<html>
<head>
<title>Artifactory Error Service</title>
</head>
<body>
<h1>Artifactory Error Service</h1>
<p>This service provides custom error pages for the Artifactory platform.</p>
</body>
</html>

View File

@@ -0,0 +1,161 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: arti-api
namespace: {{ .Values.global.Category }}--{{ .Values.global.Name }}--{{ .Values.global.Type }}
spec:
entryPoints:
- websecure
routes:
# Internal network gets full access
- match: Host(`{{ .Values.global.Api.Url }}`) && ClientIP(`192.168.100.0/24`)
kind: Rule
priority: 1000
services:
- name: api
port: 8000
# External users only get root path
- match: Host(`{{ .Values.global.Api.Url }}`) && Path(`/`)
kind: Rule
priority: 500
services:
- name: api
port: 8000
# Block all other external access
- match: Host(`{{ .Values.global.Api.Url }}`)
kind: Rule
priority: 100
services:
- name: blocked-service
port: 80
tls:
certResolver: letsencrypt
---
# Service for blocked requests
apiVersion: v1
kind: Service
metadata:
name: blocked-service
namespace: {{ .Values.global.Category }}--{{ .Values.global.Name }}--{{ .Values.global.Type }}
spec:
selector:
app: blocked-nginx
ports:
- port: 80
targetPort: 80
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: blocked-nginx
namespace: {{ .Values.global.Category }}--{{ .Values.global.Name }}--{{ .Values.global.Type }}
spec:
replicas: 1
selector:
matchLabels:
app: blocked-nginx
template:
metadata:
labels:
app: blocked-nginx
spec:
containers:
- name: nginx
image: nginx:alpine
ports:
- containerPort: 80
volumeMounts:
- name: nginx-config
mountPath: /etc/nginx/conf.d
- name: nginx-html
mountPath: /usr/share/nginx/html
volumes:
- name: nginx-config
configMap:
name: blocked-nginx-config
- name: nginx-html
configMap:
name: blocked-nginx-html
---
apiVersion: v1
kind: ConfigMap
metadata:
name: blocked-nginx-config
namespace: {{ .Values.global.Category }}--{{ .Values.global.Name }}--{{ .Values.global.Type }}
data:
default.conf: |
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
# Ensure all requests serve the index.html
error_page 404 /index.html;
}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: blocked-nginx-html
namespace: {{ .Values.global.Category }}--{{ .Values.global.Name }}--{{ .Values.global.Type }}
data:
index.html: |
<!DOCTYPE html>
<html>
<head>
<title>Access Denied - Artifactory</title>
<style>
body {
font-family: Arial, sans-serif;
text-align: center;
padding: 50px;
background-color: #f8f9fa;
}
.container {
max-width: 600px;
margin: 0 auto;
background: white;
padding: 40px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.error-code {
font-size: 4em;
color: #dc3545;
margin-bottom: 20px;
}
.error-message {
font-size: 1.5em;
color: #333;
margin-bottom: 20px;
}
.error-description {
color: #666;
margin-bottom: 30px;
}
.access-info {
background: #e3f2fd;
padding: 20px;
border-radius: 4px;
border-left: 4px solid #2196f3;
}
</style>
</head>
<body>
<div class="container">
<div class="error-code">403</div>
<div class="error-message">Access Denied</div>
<div class="error-description">
This endpoint is only accessible from the internal network.
</div>
</div>
</body>
</html>