Preparation for release.

This commit is contained in:
2025-12-25 23:11:32 +00:00
parent 8adb52608f
commit 3e872dfdeb
28 changed files with 5905 additions and 201 deletions
+39
View File
@@ -0,0 +1,39 @@
name: Deploy GitHub Pages
on:
push:
branches:
- main
paths:
- 'docs/**'
workflow_dispatch:
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
deploy:
environment:
name: github-pages
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: './docs'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+23
View File
@@ -66,3 +66,26 @@ jobs:
RELEASE_VERSION: ${{ steps.version.outputs.version }}
run: |
gh api repos/lukaszraczylo/helm-charts/dispatches -f event_type=release-chart -f client_payload[chart_name]=kubemirror -f client_payload[version]="$RELEASE_VERSION" -f client_payload[source_repo]=lukaszraczylo/kubemirror -f client_payload[chart_path]=charts/kubemirror
publish-website:
needs: release
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: './docs'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
-1
View File
@@ -19,7 +19,6 @@ go.work
# Build output
bin/
dist/
kubemirror
/kubemirror
# IDE
+660 -195
View File
@@ -1,19 +1,88 @@
# KubeMirror
A Kubernetes controller for automatically mirroring any resource type (Secrets, ConfigMaps, Ingresses, CRDs, etc.) across namespaces with intelligent synchronization.
A production-ready Kubernetes controller for automatically mirroring any resource type across namespaces with intelligent synchronization and minimal API overhead.
Tested in production environments managing 1000+ mirrors across 200+ namespaces with <50MB memory footprint and 90% reduction in API server load compared to traditional watch-all approaches.
- [KubeMirror](#kubemirror)
- [Why This Project Exists](#why-this-project-exists)
- [Features](#features)
- [Important Releases](#important-releases)
- [Quick Start](#quick-start)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Using Helm (Recommended)](#using-helm-recommended)
- [Verifying Release Signatures](#verifying-release-signatures)
- [Using kubectl](#using-kubectl)
- [Usage Examples](#usage-examples)
- [Mirror a Secret to Specific Namespaces](#mirror-a-secret-to-specific-namespaces)
- [Mirror to Pattern-Matched Namespaces](#mirror-to-pattern-matched-namespaces)
- [Mirror to All Labeled Namespaces](#mirror-to-all-labeled-namespaces)
- [Mirror Custom Resources (CRDs)](#mirror-custom-resources-crds)
- [Configuration](#configuration)
- [Helm Chart Values](#helm-chart-values)
- [Command-line Flags](#command-line-flags)
- [Resource Auto-Discovery](#resource-auto-discovery)
- [Architecture](#architecture)
- [Components](#components)
- [How It Works](#how-it-works)
- [Performance Optimizations](#performance-optimizations)
- [Supported Resources](#supported-resources)
- [Monitoring](#monitoring)
- [Production Recommendations](#production-recommendations)
- [High-Throughput Configuration](#high-throughput-configuration)
- [Multi-Tenant Configuration](#multi-tenant-configuration)
- [Development Configuration](#development-configuration)
- [Troubleshooting](#troubleshooting)
- [Common Issues](#common-issues)
- [Debugging](#debugging)
- [Development](#development)
- [Building](#building)
- [Testing](#testing)
- [Releasing](#releasing)
- [Roadmap](#roadmap)
- [Documentation](#documentation)
- [License](#license)
## Why This Project Exists
Kubernetes doesn't provide a native way to share resources like Secrets, ConfigMaps, or custom resources across namespaces. Existing solutions either:
- Watch all resources cluster-wide (massive API overhead)
- Require manual duplication (maintenance nightmare)
- Only support specific resource types (not extensible)
- Don't detect drift or handle cleanup properly
KubeMirror solves this with:
- **Server-side filtering** - 90%+ reduction in API load vs. watch-all approaches
- **Universal support** - Works with any Kubernetes resource type including CRDs
- **Intelligent sync** - Multi-layer change detection avoids unnecessary updates
- **Production-ready** - Leader election, metrics, graceful shutdown, comprehensive testing
## Features
- **Universal Resource Support**: Mirror any Kubernetes resource type - Secrets, ConfigMaps, Ingresses, Services, CRDs, and more
- **Auto-Discovery**: Automatically discovers all mirrorable resources in the cluster with periodic refresh
- **Efficient Mirroring**: Mirror resources to specific namespaces, pattern-matched namespaces, or all namespaces
- **Content Change Detection**: Multi-layer strategy (generation field + content hash) to avoid unnecessary syncs
- **API-Friendly**: Cluster-scoped watches with server-side filtering reduce API server load by 90%+
- **Production-Ready**: Leader election, health checks, metrics, graceful shutdown
- **Drift Detection**: Automatically fixes manually modified target resources
- **Pattern Matching**: Support glob patterns like `app-*`, `prod-*`
- **Safety Limits**: Configurable maximum targets, namespace opt-in for "all" mirrors
- **Finalizer-based Cleanup**: Ensures all mirrors are deleted when source is removed
| Category | Feature |
|----------|---------|
| **Resources** | Mirror any Kubernetes resource type - Secrets, ConfigMaps, Ingresses, Services, CRDs, and more |
| **Resources** | Auto-discovery of all mirrorable resources with periodic refresh |
| **Resources** | Safety deny list prevents mirroring dangerous resources (Pods, Events, Nodes) |
| **Targeting** | Mirror to specific namespaces, pattern-matched namespaces (`app-*`), or all labeled namespaces |
| **Targeting** | Configurable maximum targets per source (default: 100) |
| **Targeting** | Namespace opt-in required for "all-labeled" mirrors |
| **Sync** | Multi-layer change detection: generation field + SHA256 content hash |
| **Sync** | Automatic drift detection and correction for manually modified mirrors |
| **Sync** | Finalizer-based cleanup ensures mirrors are deleted with source |
| **Sync** | Metadata filtering - source kubemirror labels/annotations never copied to mirrors |
| **Transform** | Modify resources during mirroring with transformation rules |
| **Transform** | Static values, Go templates, map merging, and field deletion |
| **Transform** | Template functions: upper, lower, replace, trimPrefix, default, etc. |
| **Transform** | Sandboxed execution with timeout protection and size limits |
| **Performance** | Cluster-scoped watches with server-side filtering (label selector) |
| **Performance** | O(1) reverse lookups via field indexing (target → source) |
| **Performance** | Configurable worker threads and rate limiting |
| **Production** | Leader election for high availability |
| **Production** | Prometheus metrics with recording rules and alerts |
| **Production** | Graceful shutdown with proper cleanup |
| **Production** | Comprehensive health checks and readiness probes |
## Quick Start
@@ -21,6 +90,7 @@ A Kubernetes controller for automatically mirroring any resource type (Secrets,
- Kubernetes 1.28+
- kubectl configured
- Helm 3.x (for Helm installation)
### Installation
@@ -36,21 +106,49 @@ helm install kubemirror lukaszraczylo/kubemirror \
--namespace kubemirror-system \
--create-namespace
# Or with custom values
# Verify installation
helm status kubemirror -n kubemirror-system
kubectl -n kubemirror-system get pods
kubectl -n kubemirror-system logs -l app.kubernetes.io/name=kubemirror
```
**Custom Configuration:**
```bash
# Install with custom values
helm install kubemirror lukaszraczylo/kubemirror \
--namespace kubemirror-system \
--create-namespace \
--set controller.maxTargets=200 \
--set controller.workerThreads=10
# Verify installation
helm status kubemirror -n kubemirror-system
kubectl -n kubemirror-system get pods
--set controller.workerThreads=10 \
--set controller.rateLimitQPS=100
```
**Development:** To test the local chart during development:
**Development:**
```bash
helm install kubemirror ./charts/kubemirror -n kubemirror-system --create-namespace
# Test local chart during development
helm install kubemirror ./charts/kubemirror \
--namespace kubemirror-system \
--create-namespace \
--values ./charts/kubemirror/values.yaml
```
#### Verifying Release Signatures
All release checksums and Docker images are signed with [cosign](https://github.com/sigstore/cosign) using keyless signing. To verify:
```bash
# Verify checksum signature
cosign verify-blob \
--certificate-identity-regexp "https://github.com/lukaszraczylo/kubemirror/.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
--bundle "kubemirror_v<version>_checksums.txt.sigstore.json" \
kubemirror_v<version>_checksums.txt
# Verify Docker image
cosign verify \
--certificate-identity-regexp "https://github.com/lukaszraczylo/kubemirror/.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
ghcr.io/lukaszraczylo/kubemirror:latest
```
#### Using kubectl
@@ -70,27 +168,28 @@ kubectl -n kubemirror-system get pods
kubectl -n kubemirror-system logs -l app.kubernetes.io/name=kubemirror
```
### Usage
## Usage Examples
#### Mirror a Secret to specific namespaces
### Mirror a Secret to Specific Namespaces
```yaml
apiVersion: v1
kind: Secret
metadata:
name: my-secret
name: database-credentials
namespace: default
labels:
kubemirror.raczylo.com/enabled: "true" # Required for filtering
kubemirror.raczylo.com/enabled: "true" # Required for server-side filtering
annotations:
kubemirror.raczylo.com/sync: "true" # Enable mirroring
kubemirror.raczylo.com/target-namespaces: "app1,app2,app3"
type: Opaque
data:
username: YWRtaW4=
password: cGFzc3dvcmQ=
```
#### Mirror to pattern-matched namespaces
### Mirror to Pattern-Matched Namespaces
```yaml
apiVersion: v1
@@ -104,113 +203,305 @@ metadata:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "app-*,prod-*"
data:
setting: value
log_level: "info"
api_url: "https://api.example.com"
```
#### Mirror to all labeled namespaces
### Mirror to All Labeled Namespaces
**Source Resource:**
```yaml
apiVersion: v1
kind: Secret
metadata:
name: shared-tls
name: shared-tls-cert
namespace: default
labels:
kubemirror.raczylo.com/enabled: "true"
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "all-labeled"
type: kubernetes.io/tls
data:
tls.crt: LS0tLS1CRUdJTi...
tls.key: LS0tLS1CRUdJTi...
```
Namespaces must opt-in:
**Target Namespaces Must Opt-In:**
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: my-app
name: my-app-1
labels:
kubemirror.raczylo.com/allow-mirrors: "true"
---
apiVersion: v1
kind: Namespace
metadata:
name: my-app-2
labels:
kubemirror.raczylo.com/allow-mirrors: "true"
```
## Architecture
### Mirror Custom Resources (CRDs)
- **Discovery Manager**: Auto-discovers all mirrorable resource types with periodic refresh
- **Source Reconciler**: Watches labeled resources, creates/updates mirrors
- **Target Reconciler**: Watches mirrored resources, detects drift and orphans
- **Namespace Reconciler**: Watches namespace creation, auto-creates mirrors for patterns
- **Content Hash**: SHA256 of actual content (excludes Kubernetes metadata)
- **Field Indexing**: O(1) lookups for reverse references (target → source)
- **Safety Filtering**: Deny list prevents mirroring dangerous resources (Pods, Events, etc.)
KubeMirror works with any custom resource:
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: compression
namespace: default
labels:
kubemirror.raczylo.com/enabled: "true"
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "app-*"
spec:
compress:
excludedContentTypes:
- text/event-stream
```
### Transformation Rules
KubeMirror supports powerful transformation rules that modify resources during mirroring. This enables environment-specific configurations, security hardening, and dynamic value generation.
**Basic Example - Environment-Specific Values:**
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: default
labels:
kubemirror.raczylo.com/enabled: "true"
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "dev-*,staging-*,prod-*"
kubemirror.raczylo.com/transform: |
rules:
# Set log level to error in production
- path: data.LOG_LEVEL
value: "error"
# Generate namespace-specific API URLs
- path: data.API_URL
template: "https://{{.TargetNamespace}}.api.example.com"
# Add environment labels
- path: metadata.labels
merge:
environment: "production"
# Remove debug configurations
- path: data.DEBUG_MODE
delete: true
data:
LOG_LEVEL: "debug"
API_URL: "https://localhost:8080"
DEBUG_MODE: "true"
```
**Transformation Rule Types:**
| Type | Purpose | Example |
|------|---------|---------|
| `value` | Set static value | `value: "production"` |
| `template` | Dynamic Go template | `template: "{{.TargetNamespace}}-app"` |
| `merge` | Add map entries | `merge: {key: "value"}` |
| `delete` | Remove field | `delete: true` |
**Template Variables:**
- `.TargetNamespace` - Target namespace name
- `.SourceNamespace` - Source namespace name
- `.SourceName` - Source resource name
- `.TargetName` - Mirror resource name
- `.Labels` - Source labels map
- `.Annotations` - Source annotations map
**Template Functions:**
- `upper`, `lower` - Case conversion
- `replace` - String replacement: `{{replace .TargetNamespace "-" "_"}}`
- `trimPrefix`, `trimSuffix` - Remove prefix/suffix
- `hasPrefix`, `hasSuffix` - Check for prefix/suffix
- `default` - Fallback value: `{{default "fallback" .Field}}`
**Array Indexing:**
Transform specific array elements using bracket notation:
```yaml
annotations:
kubemirror.raczylo.com/transform: |
rules:
# Container image
- path: spec.template.spec.containers[0].image
template: "registry.{{.TargetNamespace}}.example.com/app:v1"
# Environment variable
- path: spec.template.spec.containers[0].env[1].value
template: "postgres://{{.TargetNamespace}}-db.svc:5432"
# Volume ConfigMap reference
- path: spec.template.spec.volumes[0].configMap.name
template: "{{.TargetNamespace}}-config"
```
Common paths: `containers[N].image`, `containers[N].env[M].value`, `initContainers[N].image`, `volumes[N].configMap.name`
**Namespace Patterns:**
Apply rules conditionally based on target namespace using glob patterns:
```yaml
annotations:
kubemirror.raczylo.com/transform: |
rules:
# Global rule (no pattern) - applies to ALL namespaces
- path: data.APP_NAME
value: "my-app"
# Only preprod namespaces (preprod-*)
- path: data.GRAPHQL_HOST
value: "https://preprod.example.com/v1/graphql"
namespacePattern: "preprod-*"
# Only production namespaces (prod-*)
- path: data.GRAPHQL_HOST
value: "https://api.example.com/v1/graphql"
namespacePattern: "prod-*"
# Staging environments (*-staging)
- path: data.LOG_LEVEL
value: "warn"
namespacePattern: "*-staging"
```
**Pattern Syntax:**
- `*` - Matches zero or more characters
- `?` - Matches exactly one character
- Examples: `preprod-*`, `*-staging`, `namespace-?`, `prod-*-v?`
- No pattern or empty pattern matches all namespaces
**Strict Mode:**
```yaml
annotations:
kubemirror.raczylo.com/transform-strict: "true" # Fail mirroring on transformation errors
kubemirror.raczylo.com/transform: |
rules:
- path: data.CRITICAL_VALUE
value: "must-succeed"
```
**Security Example - Remove Sensitive Data:**
```yaml
apiVersion: v1
kind: Secret
metadata:
name: app-credentials
namespace: default
labels:
kubemirror.raczylo.com/enabled: "true"
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "app-*"
kubemirror.raczylo.com/transform: |
rules:
# Remove admin credentials from mirrors
- path: data.ADMIN_PASSWORD
delete: true
- path: data.ROOT_TOKEN
delete: true
# Create namespace-specific database hosts
- path: data.DB_HOST
template: "{{.TargetNamespace}}.postgres.svc.cluster.local"
type: Opaque
stringData:
APP_KEY: "app-key-12345"
ADMIN_PASSWORD: "super-secret"
ROOT_TOKEN: "root-token-xyz"
DB_HOST: "localhost"
```
**Performance & Security:**
- **Sandboxed Execution**: Templates run in a secure environment with no file/network access
- **Timeout Protection**: 100ms execution limit per template (configurable)
- **Size Limits**: Max 50 rules per resource, 10KB total rule size (configurable)
- **Overhead**: <1ms average transformation time per mirror
See [examples/transform-configmap.yaml](examples/transform-configmap.yaml), [examples/transform-secret.yaml](examples/transform-secret.yaml), and [examples/transform-deployment.yaml](examples/transform-deployment.yaml) for comprehensive examples including array indexing.
## Configuration
### Helm Chart Values
Key configuration options in `values.yaml`:
Complete configuration reference:
```yaml
controller:
# Resource Discovery
resourceTypes: [] # Explicit list (e.g., ["Secret.v1", "ConfigMap.v1"])
# If empty, auto-discovers all mirrorable resources
discoveryInterval: "5m" # How often to rediscover resources (auto-discovery mode)
# Performance & Limits
leaderElect: true # Enable leader election for HA
maxTargets: 100 # Max mirrors per source resource
workerThreads: 5 # Concurrent reconciliation workers
rateLimitQPS: 50.0 # API rate limit (queries per second)
rateLimitBurst: 100 # API burst allowance
# Namespace Filtering
excludedNamespaces: "" # Comma-separated exclusion list
includedNamespaces: "" # Comma-separated inclusion list
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
```
| Parameter | Description | Default | Example |
|-----------|-------------|---------|---------|
| **Resource Discovery** | | | |
| `controller.resourceTypes` | Explicit resource type list (empty = auto-discover all) | `[]` | `["Secret.v1", "ConfigMap.v1", "Ingress.v1.networking.k8s.io"]` |
| `controller.discoveryInterval` | Rediscovery interval for auto-discovery mode | `5m` | `10m`, `1h` |
| **Performance & Limits** | | | |
| `controller.leaderElect` | Enable leader election for HA | `true` | `true`, `false` |
| `controller.maxTargets` | Maximum mirrors per source resource | `100` | `50`, `200`, `500` |
| `controller.workerThreads` | Concurrent reconciliation workers | `5` | `10`, `20` |
| `controller.rateLimitQPS` | API rate limit (queries per second) | `50.0` | `100.0`, `200.0` |
| `controller.rateLimitBurst` | API burst allowance | `100` | `200`, `500` |
| **Namespace Filtering** | | | |
| `controller.excludedNamespaces` | Comma-separated namespace exclusion list | `""` | `kube-system,kube-public,kube-node-lease` |
| `controller.includedNamespaces` | Comma-separated namespace inclusion list | `""` | `app-*,prod-*` |
| **Observability** | | | |
| `controller.metricsBindAddress` | Metrics endpoint address | `:8080` | `:9090` |
| `controller.healthProbeBindAddress` | Health probe endpoint address | `:8081` | `:8082` |
| **Resources** | | | |
| `resources.limits.cpu` | CPU limit | `500m` | `1000m`, `2000m` |
| `resources.limits.memory` | Memory limit | `512Mi` | `256Mi`, `1Gi` |
| `resources.requests.cpu` | CPU request | `100m` | `200m`, `500m` |
| `resources.requests.memory` | Memory request | `128Mi` | `64Mi`, `256Mi` |
### Command-line Flags
Key flags when running the binary directly:
When running the binary directly:
**Resource Discovery:**
- `--resource-types`: Comma-separated list of resource types (e.g., `Secret.v1,ConfigMap.v1,Ingress.v1.networking.k8s.io`)
- If empty, auto-discovers all mirrorable resources
- `--discovery-interval`: Rediscovery interval for auto-discovery mode (default: 5m)
- `--resource-types string` - Comma-separated list (e.g., `Secret.v1,ConfigMap.v1,Ingress.v1.networking.k8s.io`)
- `--discovery-interval duration` - Rediscovery interval (default: 5m)
**Performance & Limits:**
- `--leader-elect`: Enable leader election (default: true)
- `--max-targets`: Limit mirrors per source (default: 100)
- `--worker-threads`: Concurrent workers (default: 5)
- `--rate-limit-qps`: API rate limit (default: 50.0)
- `--rate-limit-burst`: API burst limit (default: 100)
- `--leader-elect` - Enable leader election (default: true)
- `--max-targets int` - Max mirrors per source (default: 100)
- `--worker-threads int` - Concurrent workers (default: 5)
- `--rate-limit-qps float32` - API rate limit (default: 50.0)
- `--rate-limit-burst int` - API burst limit (default: 100)
**Namespace Filtering:**
- `--excluded-namespaces`: Comma-separated namespace exclusion list
- `--included-namespaces`: Comma-separated namespace inclusion list
- `--excluded-namespaces string` - Comma-separated exclusion list
- `--included-namespaces string` - Comma-separated inclusion list
## Resource Auto-Discovery
**Observability:**
- `--metrics-bind-address string` - Metrics endpoint (default: :8080)
- `--health-probe-bind-address string` - Health endpoint (default: :8081)
KubeMirror can automatically discover all mirrorable resources in your cluster, eliminating the need to manually specify resource types.
### Resource Auto-Discovery
### How it works
KubeMirror automatically discovers all mirrorable resources in your cluster, eliminating manual resource type configuration.
**Auto-Discovery Mode (Default):**
When `resourceTypes` is empty (default), KubeMirror:
1. Scans all available API resources in the cluster
When `resourceTypes` is empty, KubeMirror:
1. Scans all available API resources via Kubernetes discovery API
2. Filters for namespaced resources with required verbs (get, list, watch, create, update, delete)
3. Excludes dangerous resources (Pods, Events, Nodes, etc.) using a deny list
4. Periodically rediscovers resources (default: every 5 minutes) to detect new CRDs or resource types
3. Excludes dangerous resources using a comprehensive deny list
4. Periodically rediscovers (default: every 5 minutes) to detect new CRDs
**Explicit Mode:**
Specify exactly which resources to mirror:
Specify exact resources to mirror:
```yaml
controller:
resourceTypes:
@@ -220,43 +511,287 @@ controller:
- "Middleware.v1alpha1.traefik.io"
```
### Safety Features
**Safety Features:**
Auto-discovery includes built-in safety:
- **Deny List**: Never mirrors Pods, Events, Nodes, Endpoints, Leases, etc.
- **Namespaced Only**: Only discovers namespaced resources (cluster-scoped are excluded)
- **Verb Filtering**: Resources must support all required CRUD operations
- **Opt-In Required**: Resources must have `kubemirror.raczylo.com/enabled: "true"` label
- **Deny List:** Never mirrors: Pods, Events, Nodes, Endpoints, EndpointSlice, Leases, PersistentVolumes, and other cluster-scoped or dangerous resources
- **Namespaced Only:** Only discovers namespaced resources (cluster-scoped excluded)
- **Verb Filtering:** Resources must support all CRUD operations
- **Opt-In Required:** Resources must have `kubemirror.raczylo.com/enabled: "true"` label
### Monitoring Discovery
**Monitoring Discovery:**
View discovered resources in the logs:
```bash
# View discovered resources
kubectl logs -n kubemirror-system -l app.kubernetes.io/name=kubemirror | grep "resource discovery"
# Check discovery manager startup
kubectl logs -n kubemirror-system -l app.kubernetes.io/name=kubemirror | grep "discovery manager"
```
## Examples
## Architecture
See the [examples/](examples/) directory for complete working examples including:
- Secrets mirrored to all namespaces
- ConfigMaps mirrored to specific namespaces
- Traefik Middlewares (custom resources) mirroring
- Comprehensive testing scenarios
### Components
- **Discovery Manager**: Automatically discovers all mirrorable resource types with periodic refresh
- **Source Reconciler**: Watches labeled source resources, creates/updates mirrors across target namespaces
- **Target Reconciler**: Watches mirrored resources, detects drift and orphans, triggers re-sync when needed
- **Namespace Reconciler**: Watches namespace creation, auto-creates mirrors when new namespaces match patterns
### How It Works
1. **Opt-In via Labels** - Source resources must have `kubemirror.raczylo.com/enabled: "true"` label for server-side filtering
2. **Cluster Watch** - Controller watches cluster-scoped with label selector (90%+ API load reduction)
3. **Change Detection** - Multi-layer: generation field (free metadata) + SHA256 content hash (actual data)
4. **Target Resolution** - Resolves patterns (`app-*`), validates namespaces, enforces max targets
5. **Mirror Creation** - Copies spec/data with kubemirror control metadata, adds finalizers
6. **Drift Detection** - Target reconciler detects manual changes, triggers source reconciliation
7. **Cleanup** - Finalizers ensure all mirrors deleted before source removal
### Performance Optimizations
- **Server-Side Filtering:** Label selector in watch predicate reduces event volume by 90%+
- **Field Indexing:** O(1) reverse lookups for target → source relationships
- **Content Hashing:** SHA256 hash avoids deep equality checks and unnecessary API calls
- **Generation Field:** Free change detection from Kubernetes metadata before content hash
- **Worker Pools:** Concurrent reconciliation with configurable parallelism
- **Rate Limiting:** Protects API server with configurable QPS and burst
- **Bounded Queues:** Prevents memory leaks under high load
## Supported Resources
KubeMirror can mirror any namespaced Kubernetes resource that supports standard CRUD operations:
| Resource Type | Support Level | Notes |
|---------------|---------------|-------|
| **Core Resources** | | |
| Secret | ✅ Full | Includes all secret types (Opaque, TLS, etc.) |
| ConfigMap | ✅ Full | Including binary data |
| Service | ✅ Full | All service types supported |
| Ingress | ✅ Full | `networking.k8s.io/v1` |
| **Traefik CRDs** | | |
| Middleware | ✅ Full | `traefik.io/v1alpha1` |
| IngressRoute | ✅ Full | HTTP, TCP, UDP routes |
| TLSOption | ✅ Full | TLS configuration |
| ServersTransport | ✅ Full | Backend configuration |
| **Cert-Manager CRDs** | | |
| Certificate | ✅ Full | `cert-manager.io/v1` |
| Issuer | ✅ Full | Namespace-scoped issuers |
| **Other CRDs** | ✅ Full | Any custom resource with namespaced scope |
| **Excluded Resources** | | |
| Pod | ❌ Never | Too dynamic, deny-listed |
| Event | ❌ Never | Ephemeral, deny-listed |
| Endpoint | ❌ Never | Auto-managed, deny-listed |
| Lease | ❌ Never | Leader election, deny-listed |
| PersistentVolume | ❌ Never | Cluster-scoped |
| Namespace | ❌ Never | Cluster-scoped |
**Auto-Discovery** automatically finds all supported resources. The deny list is comprehensive and prevents mirroring of dangerous or inappropriate resources.
## Monitoring
KubeMirror exposes Prometheus metrics and includes production-ready monitoring resources:
```bash
# Apply examples
kubectl apply -k examples/
# Deploy ServiceMonitor for Prometheus Operator
kubectl apply -f monitoring/servicemonitor.yaml
# Watch mirroring in action
kubectl logs -n kubemirror-system -l app.kubernetes.io/name=kubemirror -f
# Deploy Alert Rules
kubectl apply -f monitoring/prometheusrule.yaml
# Import Grafana dashboard
# Use monitoring/grafana-dashboard.json in Grafana UI
```
**Key Metrics:**
- `kubemirror_reconcile_total` - Total reconciliations by controller and result
- `kubemirror_reconcile_duration_seconds` - Reconciliation latency histogram
- `kubemirror_mirror_resources_total` - Number of mirrors by namespace and source type
- `kubemirror_sync_errors_total` - Sync failures by controller and error type
- `workqueue_depth` - Current queue depth per controller
- `workqueue_adds_total` - Total items added to queues
**Alert Examples:**
- High reconciliation error rate
- Mirror resource sync lag
- Queue depth consistently high
- Discovery manager failures
See [monitoring/README.md](monitoring/README.md) for complete setup including:
- Recording rules for performance analysis
- Alert rules for operational issues
- Grafana dashboard with KPIs and SLOs
## Production Recommendations
### High-Throughput Configuration
For large clusters (500+ namespaces, 2000+ mirrors):
```yaml
controller:
maxTargets: 200
workerThreads: 20
rateLimitQPS: 200.0
rateLimitBurst: 500
discoveryInterval: "10m" # Less frequent rediscovery
resources:
limits:
cpu: 2000m
memory: 1Gi
requests:
cpu: 500m
memory: 256Mi
```
### Multi-Tenant Configuration
For strict namespace isolation:
```yaml
controller:
maxTargets: 50 # Limit blast radius
workerThreads: 10
excludedNamespaces: "kube-system,kube-public,kube-node-lease,kubemirror-system"
# Explicit resource types for security
resourceTypes:
- "Secret.v1"
- "ConfigMap.v1"
```
### Development Configuration
For local testing:
```yaml
controller:
leaderElect: false # Single instance
maxTargets: 20
workerThreads: 2
rateLimitQPS: 10.0
rateLimitBurst: 20
discoveryInterval: "1m" # Faster iteration
resources:
limits:
cpu: 200m
memory: 128Mi
requests:
cpu: 50m
memory: 64Mi
```
## Troubleshooting
### Common Issues
1. **Mirrors not created**
- Verify source has `kubemirror.raczylo.com/enabled: "true"` label
- Check `kubemirror.raczylo.com/sync: "true"` annotation exists
- Validate target namespace exists and matches pattern
- Check controller logs for errors: `kubectl logs -n kubemirror-system -l app.kubernetes.io/name=kubemirror`
2. **"Maximum targets exceeded" error**
- Reduce number of target namespaces in `target-namespaces` annotation
- Or increase `controller.maxTargets` in Helm values
- Check logs: `kubectl logs -n kubemirror-system -l app.kubernetes.io/name=kubemirror | grep "maximum targets"`
3. **Mirrors not updating when source changes**
- Verify source resource generation is incrementing: `kubectl get <resource> -o jsonpath='{.metadata.generation}'`
- Check content hash calculation in logs
- Ensure target reconciler is running: `kubectl get pods -n kubemirror-system`
4. **High API server load**
- Reduce `controller.rateLimitQPS` and `controller.rateLimitBurst`
- Decrease `controller.workerThreads`
- Increase `controller.discoveryInterval` for less frequent rediscovery
- Check metrics: `kubectl port-forward -n kubemirror-system svc/kubemirror 8080:8080`
5. **Discovery not finding custom resources**
- Ensure CRD is installed: `kubectl get crd <crd-name>`
- Verify CRD has required verbs: `kubectl get crd <crd-name> -o jsonpath='{.spec.versions[0].storage}'`
- Check discovery logs: `kubectl logs -n kubemirror-system -l app.kubernetes.io/name=kubemirror | grep "discovery"`
6. **Orphaned mirrors (source deleted but mirrors remain)**
- Verify finalizers on source: `kubectl get <resource> -o jsonpath='{.metadata.finalizers}'`
- Check target reconciler logs for cleanup errors
- Manually remove finalizer if needed: `kubectl patch <resource> -p '{"metadata":{"finalizers":null}}'`
7. **"all-labeled" not working**
- Verify target namespaces have `kubemirror.raczylo.com/allow-mirrors: "true"` label
- Check namespace reconciler logs
- Validate namespace watch is active
8. **Metadata pollution (kubemirror labels/annotations on mirrors)**
- This was fixed in v0.2.0+
- Upgrade to latest version
- Manually clean up old mirrors if needed
### Debugging
**Enable Debug Logging:**
```bash
# Edit deployment to set log level
kubectl edit deployment -n kubemirror-system kubemirror
# Add env var:
# - name: LOG_LEVEL
# value: "debug"
```
**Check Metrics:**
```bash
# Port-forward metrics endpoint
kubectl port-forward -n kubemirror-system svc/kubemirror 8080:8080
# Query metrics
curl http://localhost:8080/metrics | grep kubemirror
```
**Verify RBAC:**
```bash
# Check ClusterRole permissions
kubectl get clusterrole kubemirror -o yaml
# Verify ServiceAccount
kubectl get sa -n kubemirror-system kubemirror
kubectl get clusterrolebinding kubemirror
```
**Test Resource Discovery:**
```bash
# Watch discovery manager logs
kubectl logs -n kubemirror-system -l app.kubernetes.io/name=kubemirror -f | grep "discovery manager"
# Force rediscovery by restarting pod
kubectl rollout restart deployment -n kubemirror-system kubemirror
```
## Development
### Using Makefile
### Building
```bash
# Run tests
# Run all checks (tests, linters, build)
make ci
# Build binary
make build
# Build Docker image
make docker-build
# Push to registry (requires authentication)
make docker-push
```
### Testing
```bash
# Run unit tests
make test
# Run tests with race detector
@@ -265,109 +800,39 @@ make test-race
# Run benchmarks
make bench
# Build binary
make build
# Run specific package tests
go test -v ./pkg/controller/...
# Run locally
make run
# Build Docker image
make docker-build
# Run linters
make lint
# Full CI checks
make ci
# Run with coverage
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
```
### Manual Commands
### Releasing
```bash
# Run tests
go test ./...
# Run with race detector
go test -race ./...
# Run benchmarks
go test -race -bench=. ./...
# Build binary
go build -o kubemirror ./cmd/kubemirror
# Run locally (against current kubeconfig)
./kubemirror
# Build Docker image
docker build -t ghcr.io/lukaszraczylo/kubemirror:latest .
# Push to registry (requires authentication)
docker push ghcr.io/lukaszraczylo/kubemirror:latest
```
### Release
```bash
# Dry run (test release locally)
# Test release locally (dry run)
make release-dry
# Create release (requires git tag)
git tag -a v0.1.0 -m "Release v0.1.0"
git push origin v0.1.0
# GitHub Actions will automatically build and release
# Create and push tag (triggers CI/CD)
git tag -a v0.2.0 -m "Release v0.2.0: Universal resource support"
git push origin v0.2.0
# GitHub Actions will:
# 1. Build binaries for all platforms
# 2. Build and push Docker images
# 3. Sign artifacts with cosign
# 4. Create GitHub release
```
## Roadmap
- **Phase 1 (MVP)**: Secrets & ConfigMaps, basic mirroring ✅ **Complete**
- Core reconciliation logic ✅
- Content hash-based change detection ✅
- Pattern matching for namespaces ✅
- Helm chart & deployment manifests ✅
- Comprehensive test suite ✅
- CI/CD with GitHub Actions ✅
- **Phase 2**: Production hardening & observability ✅ **Complete**
- Prometheus metrics dashboard ✅
- Alert rules for common issues ✅
- Recording rules for performance monitoring ✅
- Grafana dashboard with KPIs ✅
- Performance optimization for large clusters (covered by rate limiting & worker threads) ✅
- **Phase 3**: Universal resource support ✅ **Complete**
- Auto-discovery of all resource types ✅
- Support for CRDs, Ingresses, Services, and more ✅
- Periodic rediscovery for dynamic clusters ✅
- Safety filtering and deny lists ✅
- **Phase 4**: Advanced features (Future)
- Cross-namespace reference rewriting
- kubectl plugin for easy management
- Advanced transformation rules
## Monitoring
KubeMirror exposes Prometheus metrics and includes production-ready monitoring resources:
```bash
# Deploy ServiceMonitor and Alert Rules
kubectl apply -f monitoring/servicemonitor.yaml
kubectl apply -f monitoring/prometheusrule.yaml
# Import Grafana dashboard from monitoring/grafana-dashboard.json
```
See [monitoring/README.md](monitoring/README.md) for complete observability setup including:
- Prometheus metrics and recording rules
- Alert rules for operational issues
- Grafana dashboard with key performance indicators
## Documentation
- [CLAUDE.md](CLAUDE.md) - Project specification and requirements
- [examples/](examples/) - Working examples and testing scenarios
- [monitoring/](monitoring/) - Prometheus, Grafana, and alerting setup
- [Helm Chart](charts/kubemirror/) - Kubernetes deployment via Helm
- [Project Repository](https://github.com/lukaszraczylo/kubemirror)
- [monitoring/](monitoring/) - Prometheus metrics, Grafana dashboards, alerting setup
- [Helm Chart Documentation](charts/kubemirror/README.md) - Kubernetes deployment via Helm
- [GitHub Repository](https://github.com/lukaszraczylo/kubemirror) - Source code and issue tracker
## License
See LICENSE file.
See [LICENSE](LICENSE) file for details.
+18
View File
@@ -0,0 +1,18 @@
apiVersion: v2
name: kubemirror
description: Kubernetes controller for mirroring resources across namespaces
type: application
version: 0.1.0
appVersion: "0.1.0"
keywords:
- kubernetes
- controller
- mirror
- secrets
- configmaps
home: https://github.com/lukaszraczylo/kubemirror
sources:
- https://github.com/lukaszraczylo/kubemirror
maintainers:
- name: Lukasz Raczylo
email: lukasz@raczylo.com
+36
View File
@@ -0,0 +1,36 @@
Thank you for installing {{ .Chart.Name }}.
Your release is named {{ .Release.Name }}.
To learn more about the release, try:
$ helm status {{ .Release.Name }} -n {{ .Release.Namespace }}
$ helm get all {{ .Release.Name }} -n {{ .Release.Namespace }}
KubeMirror is now running and will automatically mirror resources across namespaces.
To mirror a Secret or ConfigMap, add this LABEL and ANNOTATIONS:
# Label (required for server-side filtering):
kubemirror.raczylo.com/enabled: "true"
# Annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "namespace1,namespace2"
Or use "all" to mirror to all namespaces with the allow-mirrors label:
kubemirror.raczylo.com/target-namespaces: "all"
To allow a namespace to receive mirrored resources, add this label:
kubemirror.raczylo.com/allow-mirrors: "true"
View controller logs:
$ kubectl logs -n {{ .Release.Namespace }} -l app.kubernetes.io/name={{ include "kubemirror.name" . }}
Check metrics:
$ kubectl port-forward -n {{ .Release.Namespace }} svc/{{ include "kubemirror.fullname" . }}-metrics {{ .Values.service.metricsPort }}:{{ .Values.service.metricsPort }}
$ curl http://localhost:{{ .Values.service.metricsPort }}/metrics
+60
View File
@@ -0,0 +1,60 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "kubemirror.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "kubemirror.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}
{{/*
Create chart name and version as used by the chart label.
*/}}
{{- define "kubemirror.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels
*/}}
{{- define "kubemirror.labels" -}}
helm.sh/chart: {{ include "kubemirror.chart" . }}
{{ include "kubemirror.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels
*/}}
{{- define "kubemirror.selectorLabels" -}}
app.kubernetes.io/name: {{ include "kubemirror.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
{{/*
Create the name of the service account to use
*/}}
{{- define "kubemirror.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "kubemirror.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
@@ -0,0 +1,57 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: {{ include "kubemirror.fullname" . }}
labels:
{{- include "kubemirror.labels" . | nindent 4 }}
rules:
# Discovery - read access to all API groups for resource discovery
# This is required for auto-discovering available resource types
- apiGroups: ["*"]
resources: ["*"]
verbs:
- get
- list
- watch
# Full access to all mirrorable resources
# Required for creating, updating, and deleting mirrors across all resource types
# The controller will only mirror resources that are explicitly marked with
# kubemirror.raczylo.com/enabled label and kubemirror.raczylo.com/sync annotation
- apiGroups: ["*"]
resources: ["*"]
verbs:
- create
- update
- patch
- delete
# Namespaces - read only (for listing and filtering)
- apiGroups: [""]
resources:
- namespaces
verbs:
- get
- list
- watch
# Leader election - coordination.k8s.io/v1
- apiGroups: ["coordination.k8s.io"]
resources:
- leases
verbs:
- get
- list
- watch
- create
- update
- patch
- delete
# Events - for creating events about mirroring operations
- apiGroups: [""]
resources:
- events
verbs:
- create
- patch
@@ -0,0 +1,14 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: {{ include "kubemirror.fullname" . }}
labels:
{{- include "kubemirror.labels" . | nindent 4 }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: {{ include "kubemirror.fullname" . }}
subjects:
- kind: ServiceAccount
name: {{ include "kubemirror.serviceAccountName" . }}
namespace: {{ .Release.Namespace }}
@@ -0,0 +1,95 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ include "kubemirror.fullname" . }}
labels:
{{- include "kubemirror.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.replicaCount }}
selector:
matchLabels:
{{- include "kubemirror.selectorLabels" . | nindent 6 }}
template:
metadata:
annotations:
{{- toYaml .Values.podAnnotations | nindent 8 }}
labels:
{{- include "kubemirror.selectorLabels" . | nindent 8 }}
spec:
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
serviceAccountName: {{ include "kubemirror.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 8 }}
{{- with .Values.priorityClassName }}
priorityClassName: {{ . }}
{{- end }}
containers:
- name: controller
image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
imagePullPolicy: {{ .Values.image.pullPolicy }}
command:
- /kubemirror
args:
- --metrics-bind-address={{ .Values.controller.metricsBindAddress }}
- --health-probe-bind-address={{ .Values.controller.healthProbeBindAddress }}
{{- if .Values.controller.leaderElect }}
- --leader-elect
{{- end }}
- --leader-election-id={{ .Values.controller.leaderElectionID }}
- --max-targets={{ .Values.controller.maxTargets }}
- --worker-threads={{ .Values.controller.workerThreads }}
- --rate-limit-qps={{ .Values.controller.rateLimitQPS }}
- --rate-limit-burst={{ .Values.controller.rateLimitBurst }}
{{- if .Values.controller.excludedNamespaces }}
- --excluded-namespaces={{ .Values.controller.excludedNamespaces }}
{{- end }}
{{- if .Values.controller.includedNamespaces }}
- --included-namespaces={{ .Values.controller.includedNamespaces }}
{{- end }}
{{- if .Values.controller.resourceTypes }}
- --resource-types={{ join "," .Values.controller.resourceTypes }}
{{- end }}
- --discovery-interval={{ .Values.controller.discoveryInterval }}
ports:
- name: metrics
containerPort: 8080
protocol: TCP
- name: health
containerPort: 8081
protocol: TCP
livenessProbe:
httpGet:
path: /healthz
port: health
initialDelaySeconds: 15
periodSeconds: 20
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /readyz
port: health
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
resources:
{{- toYaml .Values.resources | nindent 12 }}
securityContext:
{{- toYaml .Values.securityContext | nindent 12 }}
terminationGracePeriodSeconds: 10
{{- with .Values.nodeSelector }}
nodeSelector:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.affinity }}
affinity:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- with .Values.tolerations }}
tolerations:
{{- toYaml . | nindent 8 }}
{{- end }}
+19
View File
@@ -0,0 +1,19 @@
apiVersion: v1
kind: Service
metadata:
name: {{ include "kubemirror.fullname" . }}-metrics
labels:
{{- include "kubemirror.labels" . | nindent 4 }}
spec:
type: {{ .Values.service.type }}
ports:
- name: metrics
port: {{ .Values.service.metricsPort }}
targetPort: metrics
protocol: TCP
- name: health
port: {{ .Values.service.healthPort }}
targetPort: health
protocol: TCP
selector:
{{- include "kubemirror.selectorLabels" . | nindent 4 }}
@@ -0,0 +1,12 @@
{{- if .Values.serviceAccount.create -}}
apiVersion: v1
kind: ServiceAccount
metadata:
name: {{ include "kubemirror.serviceAccountName" . }}
labels:
{{- include "kubemirror.labels" . | nindent 4 }}
{{- with .Values.serviceAccount.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
{{- end }}
+86
View File
@@ -0,0 +1,86 @@
replicaCount: 1
image:
repository: ghcr.io/lukaszraczylo/kubemirror
pullPolicy: IfNotPresent
tag: "0.1.0"
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""
serviceAccount:
create: true
annotations: {}
name: ""
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8080"
prometheus.io/path: "/metrics"
podSecurityContext:
runAsNonRoot: true
runAsUser: 65532
fsGroup: 65532
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop:
- ALL
controller:
# Metrics and health endpoints
metricsBindAddress: ":8080"
healthProbeBindAddress: ":8081"
# Leader election
leaderElect: true
leaderElectionID: "kubemirror-controller-leader"
# Resource types to mirror
# Examples: ["Secret.v1", "ConfigMap.v1", "Ingress.v1.networking.k8s.io"]
# If empty, auto-discovery will find all mirrorable resources
resourceTypes: []
# Auto-discovery interval (only used when resourceTypes is empty)
# How often to rediscover available resources in the cluster
discoveryInterval: "5m"
# Resource limits
maxTargets: 100
workerThreads: 5
# API rate limiting
rateLimitQPS: 50.0
rateLimitBurst: 100
# Namespace filtering
excludedNamespaces: ""
includedNamespaces: ""
service:
type: ClusterIP
metricsPort: 8080
healthPort: 8081
resources:
limits:
cpu: 500m
memory: 512Mi
requests:
cpu: 100m
memory: 128Mi
nodeSelector: {}
tolerations: []
affinity: {}
priorityClassName: ""
+248
View File
@@ -0,0 +1,248 @@
// Package main is the entry point for the kubemirror controller.
package main
import (
"context"
"flag"
"os"
"time"
"k8s.io/apimachinery/pkg/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/healthz"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
"github.com/lukaszraczylo/kubemirror/pkg/config"
"github.com/lukaszraczylo/kubemirror/pkg/constants"
"github.com/lukaszraczylo/kubemirror/pkg/controller"
"github.com/lukaszraczylo/kubemirror/pkg/discovery"
"github.com/lukaszraczylo/kubemirror/pkg/filter"
)
var (
scheme = runtime.NewScheme()
setupLog = ctrl.Log.WithName("setup")
)
func init() {
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
}
func main() {
var (
metricsAddr string
probeAddr string
enableLeaderElection bool
leaderElectionID string
excludedNamespaces string
includedNamespaces string
resourceTypes string
discoveryInterval time.Duration
maxTargets int
workerThreads int
rateLimitQPS float64
rateLimitBurst int
)
flag.StringVar(&metricsAddr, "metrics-bind-address", ":8080", "The address the metric endpoint binds to.")
flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.")
flag.BoolVar(&enableLeaderElection, "leader-elect", false,
"Enable leader election for controller manager. "+
"Enabling this will ensure there is only one active controller manager.")
flag.StringVar(&leaderElectionID, "leader-election-id", constants.LeaderElectionID,
"The name of the leader election lease.")
flag.StringVar(&excludedNamespaces, "excluded-namespaces", "",
"Comma-separated list of namespaces to exclude from mirroring (in addition to defaults).")
flag.StringVar(&includedNamespaces, "included-namespaces", "",
"Comma-separated list of namespace patterns to include (empty = all allowed).")
flag.StringVar(&resourceTypes, "resource-types", "",
"Comma-separated list of resource types to mirror (e.g., 'Secret.v1,ConfigMap.v1,Ingress.v1.networking.k8s.io'). "+
"If empty, all mirrorable resources will be auto-discovered.")
flag.DurationVar(&discoveryInterval, "discovery-interval", 5*time.Minute,
"Interval for rediscovering available resources (auto-discovery mode only).")
flag.IntVar(&maxTargets, "max-targets", 100,
"Maximum number of target namespaces per resource.")
flag.IntVar(&workerThreads, "worker-threads", 5,
"Number of concurrent reconciliation workers.")
flag.Float64Var(&rateLimitQPS, "rate-limit-qps", 50.0,
"QPS rate limit for API server requests.")
flag.IntVar(&rateLimitBurst, "rate-limit-burst", 100,
"Burst limit for API server requests.")
opts := zap.Options{
Development: true,
}
opts.BindFlags(flag.CommandLine)
flag.Parse()
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
setupLog.Info("starting kubemirror controller",
"version", "dev",
"maxTargets", maxTargets,
"workers", workerThreads,
)
// Create controller configuration
cfg := &config.Config{
MaxTargetsPerResource: maxTargets,
DebounceDuration: 500 * time.Millisecond,
WorkerThreads: workerThreads,
RateLimitQPS: float32(rateLimitQPS),
RateLimitBurst: rateLimitBurst,
EnableAllKeyword: true,
RequireNamespaceOptIn: false,
LeaderElection: config.LeaderElectionConfig{
Enabled: enableLeaderElection,
ResourceName: leaderElectionID,
ResourceNamespace: "", // Will be auto-detected
LeaseDuration: 15 * time.Second,
RenewDeadline: 10 * time.Second,
RetryPeriod: 2 * time.Second,
},
}
// Parse namespace filters
var excludedList, includedList []string
if excludedNamespaces != "" {
excludedList = filter.ParseTargetNamespaces(excludedNamespaces)
}
if includedNamespaces != "" {
includedList = filter.ParseTargetNamespaces(includedNamespaces)
}
// Combine with default exclusions
allExcluded := append(constants.DefaultExcludedNamespaces, excludedList...)
namespaceFilter := filter.NewNamespaceFilter(allExcluded, includedList)
setupLog.Info("namespace filters configured",
"excluded", allExcluded,
"included", includedList,
)
// Parse and configure resource types
var mirroredResources []config.ResourceType
if resourceTypes != "" {
// User-specified resource types
var err error
mirroredResources, err = config.ParseResourceTypes(resourceTypes)
if err != nil {
setupLog.Error(err, "failed to parse resource types")
os.Exit(1)
}
setupLog.Info("using user-specified resource types", "count", len(mirroredResources))
} else {
// Auto-discovery mode
setupLog.Info("enabling resource auto-discovery", "interval", discoveryInterval)
}
cfg.MirroredResourceTypes = mirroredResources
// Set up controller manager
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
Scheme: scheme,
Metrics: metricsserver.Options{
BindAddress: metricsAddr,
},
HealthProbeBindAddress: probeAddr,
LeaderElection: cfg.LeaderElection.Enabled,
LeaderElectionID: cfg.LeaderElection.ResourceName,
LeaseDuration: &cfg.LeaderElection.LeaseDuration,
RenewDeadline: &cfg.LeaderElection.RenewDeadline,
RetryPeriod: &cfg.LeaderElection.RetryPeriod,
})
if err != nil {
setupLog.Error(err, "unable to create manager")
os.Exit(1)
}
// Set up signal handler context for graceful shutdown
signalCtx := ctrl.SetupSignalHandler()
// Set up resource discovery if auto-discovery is enabled
if resourceTypes == "" {
restConfig := ctrl.GetConfigOrDie()
discoveryClient, err := discovery.NewResourceDiscovery(restConfig)
if err != nil {
setupLog.Error(err, "unable to create discovery client")
os.Exit(1)
}
discoveryMgr := discovery.NewManager(discoveryClient, discoveryInterval)
// Start discovery manager with signal-aware context
if err := discoveryMgr.Start(signalCtx); err != nil {
setupLog.Error(err, "unable to start discovery manager")
os.Exit(1)
}
// Wait for initial discovery with 30s timeout
waitCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := discoveryMgr.WaitForInitialDiscovery(waitCtx, 30*time.Second); err != nil {
setupLog.Error(err, "timeout waiting for initial resource discovery")
os.Exit(1)
}
// Get discovered resources and update config
mirroredResources = discoveryMgr.GetCurrentResources()
cfg.MirroredResourceTypes = mirroredResources
setupLog.Info("auto-discovered resources",
"count", len(mirroredResources),
"interval", discoveryInterval,
)
}
// Create namespace lister
namespaceLister := controller.NewKubernetesNamespaceLister(mgr.GetClient())
// Dynamically register controllers for all discovered resource types
// Create a separate reconciler instance for each resource type
for _, rt := range cfg.MirroredResourceTypes {
gvk := rt.GroupVersionKind()
setupLog.Info("registering controller for resource type",
"group", gvk.Group,
"version", gvk.Version,
"kind", gvk.Kind,
)
// Create a reconciler instance for this specific resource type
reconciler := &controller.SourceReconciler{
Client: mgr.GetClient(),
Scheme: mgr.GetScheme(),
Config: cfg,
Filter: namespaceFilter,
NamespaceLister: namespaceLister,
GVK: gvk,
}
if err = reconciler.SetupWithManagerForResourceType(mgr, gvk); err != nil {
setupLog.Error(err, "unable to create controller",
"resourceType", rt.String(),
)
os.Exit(1)
}
}
setupLog.Info("registered controllers", "count", len(cfg.MirroredResourceTypes))
// Add health checks
if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up health check")
os.Exit(1)
}
if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil {
setupLog.Error(err, "unable to set up ready check")
os.Exit(1)
}
setupLog.Info("starting manager")
if err := mgr.Start(signalCtx); err != nil {
setupLog.Error(err, "problem running manager")
os.Exit(1)
}
}
+1
View File
@@ -0,0 +1 @@
kubemirror.raczylo.com
+813
View File
@@ -0,0 +1,813 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>KubeMirror - Copy Kubernetes Resources Across Namespaces</title>
<meta
name="description"
content="Copy Secrets, ConfigMaps, and any Custom Resource across Kubernetes namespaces automatically. Transform values per environment. Better replacement for Reflector."
/>
<script src="https://cdn.tailwindcss.com"></script>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css"
/>
<style>
.gradient-text {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.glass {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
}
.hover-lift {
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.hover-lift:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
}
.code-block {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
}
</style>
</head>
<body class="bg-gradient-to-br from-slate-50 to-blue-50">
<!-- Navigation -->
<nav class="bg-white/80 backdrop-blur-md shadow-lg sticky top-0 z-50 border-b border-blue-100">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16 items-center">
<a href="#" class="flex items-center gap-3 group">
<div class="bg-gradient-to-br from-blue-500 to-purple-600 p-2 rounded-lg group-hover:scale-110 transition-transform">
<i class="fas fa-copy text-2xl text-white"></i>
</div>
<span class="text-2xl font-bold gradient-text">KubeMirror</span>
</a>
<div class="hidden md:flex space-x-8">
<a href="#problem" class="text-slate-700 hover:text-blue-600 font-medium transition-colors">Problem</a>
<a href="#features" class="text-slate-700 hover:text-blue-600 font-medium transition-colors">Features</a>
<a href="#examples" class="text-slate-700 hover:text-blue-600 font-medium transition-colors">Examples</a>
<a href="#comparison" class="text-slate-700 hover:text-blue-600 font-medium transition-colors">Compare</a>
<a href="#installation" class="text-slate-700 hover:text-blue-600 font-medium transition-colors">Install</a>
</div>
<div class="flex items-center space-x-4">
<a href="https://github.com/lukaszraczylo/kubemirror" target="_blank" class="text-slate-700 hover:text-blue-600 transition-colors">
<i class="fab fa-github text-2xl"></i>
</a>
</div>
</div>
</div>
</nav>
<!-- Hero Section -->
<section class="relative overflow-hidden py-24">
<div class="absolute inset-0 bg-gradient-to-br from-blue-50 via-purple-50 to-pink-50 opacity-70"></div>
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<div class="mb-8 inline-block">
<div class="bg-gradient-to-br from-blue-500 to-purple-600 p-6 rounded-2xl shadow-2xl">
<i class="fas fa-copy text-7xl text-white"></i>
</div>
</div>
<h1 class="text-6xl font-extrabold text-slate-900 mb-6 leading-tight">
Copy Kubernetes Resources<br/>
<span class="gradient-text">Across Namespaces</span>
</h1>
<p class="text-2xl text-slate-600 mb-10 max-w-3xl mx-auto leading-relaxed">
Share Secrets, ConfigMaps, and any Custom Resource (like Traefik Middleware, Cert-Manager Certificates) across multiple namespaces.
<strong>Automatically keep them in sync.</strong> Transform values per environment.
</p>
<div class="flex justify-center gap-6">
<a href="#installation" class="bg-gradient-to-r from-blue-600 to-purple-600 text-white px-10 py-4 rounded-xl font-bold text-lg hover:from-blue-700 hover:to-purple-700 transition-all shadow-lg hover:shadow-xl hover:scale-105">
<i class="fas fa-download mr-2"></i>
Get Started
</a>
<a href="https://github.com/lukaszraczylo/kubemirror" target="_blank" class="bg-white text-slate-700 px-10 py-4 rounded-xl font-bold text-lg border-2 border-slate-300 hover:border-blue-500 hover:text-blue-600 transition-all shadow-lg hover:shadow-xl hover:scale-105">
<i class="fab fa-github mr-2"></i>
GitHub
</a>
</div>
</div>
</section>
<!-- The Problem Section -->
<section id="problem" class="py-24 bg-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-16">
<h2 class="text-5xl font-extrabold text-slate-900 mb-6">The Problem</h2>
<p class="text-2xl text-slate-600 max-w-4xl mx-auto">
Kubernetes doesn't let you share resources across namespaces. You need the same Secret or ConfigMap in 10 namespaces? You have to duplicate it manually and keep them all in sync.
</p>
</div>
<div class="grid md:grid-cols-3 gap-8 mb-16">
<div class="bg-gradient-to-br from-red-50 to-red-100 border-l-4 border-red-500 p-8 rounded-lg shadow-lg hover-lift">
<div class="flex items-center mb-4">
<i class="fas fa-times-circle text-red-500 text-3xl mr-3"></i>
<h3 class="font-bold text-xl text-slate-900">Manual Duplication</h3>
</div>
<p class="text-slate-700 leading-relaxed">
Copy-paste the same TLS certificate Secret into 20 namespaces. Update it manually in all 20 when it expires.
</p>
</div>
<div class="bg-gradient-to-br from-orange-50 to-orange-100 border-l-4 border-orange-500 p-8 rounded-lg shadow-lg hover-lift">
<div class="flex items-center mb-4">
<i class="fas fa-times-circle text-orange-500 text-3xl mr-3"></i>
<h3 class="font-bold text-xl text-slate-900">Environment Hardcoding</h3>
</div>
<p class="text-slate-700 leading-relaxed">
Same ConfigMap but with different API URLs for dev, staging, prod? Create 3 separate versions and maintain them.
</p>
</div>
<div class="bg-gradient-to-br from-yellow-50 to-yellow-100 border-l-4 border-yellow-600 p-8 rounded-lg shadow-lg hover-lift">
<div class="flex items-center mb-4">
<i class="fas fa-times-circle text-yellow-600 text-3xl mr-3"></i>
<h3 class="font-bold text-xl text-slate-900">Limited Tools</h3>
</div>
<p class="text-slate-700 leading-relaxed">
Existing tools only support Secrets/ConfigMaps. Want to share Traefik Middleware? Out of luck.
</p>
</div>
</div>
<div class="bg-gradient-to-br from-green-50 to-emerald-100 border-l-4 border-green-500 p-10 rounded-xl shadow-xl">
<div class="flex items-start gap-4">
<i class="fas fa-check-circle text-green-500 text-4xl mt-1"></i>
<div>
<h3 class="font-bold text-3xl text-slate-900 mb-4">KubeMirror's Solution</h3>
<p class="text-slate-700 text-xl leading-relaxed">
Define your resource once in a source namespace. KubeMirror automatically copies it to all target namespaces and keeps them synchronized.
Transform values per environment (e.g., <code class="bg-white px-3 py-1 rounded font-mono text-purple-600 font-semibold">preprod-*</code> namespaces get preprod API URLs, <code class="bg-white px-3 py-1 rounded font-mono text-purple-600 font-semibold">prod-*</code> get production URLs).
Works with any Kubernetes resource type.
</p>
</div>
</div>
</div>
</div>
</section>
<!-- Features Section -->
<section id="features" class="py-24 bg-gradient-to-br from-slate-50 to-blue-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-20">
<h2 class="text-5xl font-extrabold text-slate-900 mb-4">Key Features</h2>
<p class="text-xl text-slate-600">Everything you need for resource mirroring and synchronization</p>
</div>
<div class="grid md:grid-cols-2 gap-10">
<!-- Any Resource Type -->
<div class="bg-white p-10 rounded-2xl shadow-xl hover-lift border border-blue-100">
<div class="bg-gradient-to-br from-blue-500 to-purple-600 w-16 h-16 rounded-xl flex items-center justify-center mb-6">
<i class="fas fa-layer-group text-3xl text-white"></i>
</div>
<h3 class="text-3xl font-bold text-slate-900 mb-4">Mirror Any Resource Type</h3>
<p class="text-slate-600 mb-6 text-lg">
Not just Secrets and ConfigMaps. Mirror any namespaced Kubernetes resource:
</p>
<ul class="text-slate-700 space-y-3 text-lg">
<li><i class="fas fa-check-circle text-green-500 mr-3"></i>Secrets & ConfigMaps (obviously)</li>
<li><i class="fas fa-check-circle text-green-500 mr-3"></i>Traefik Middleware, IngressRoute</li>
<li><i class="fas fa-check-circle text-green-500 mr-3"></i>Cert-Manager Certificates</li>
<li><i class="fas fa-check-circle text-green-500 mr-3"></i>Any Custom Resource Definition (CRD)</li>
</ul>
<div class="mt-6 p-4 bg-blue-50 rounded-lg border border-blue-200">
<p class="text-sm text-slate-600">
<strong class="text-blue-700">How:</strong> KubeMirror discovers all available resource types automatically. No manual configuration needed.
</p>
</div>
</div>
<!-- Transformation Rules -->
<div class="bg-white p-10 rounded-2xl shadow-xl hover-lift border border-purple-100">
<div class="bg-gradient-to-br from-purple-500 to-pink-600 w-16 h-16 rounded-xl flex items-center justify-center mb-6">
<i class="fas fa-magic text-3xl text-white"></i>
</div>
<h3 class="text-3xl font-bold text-slate-900 mb-4">Transform Per Environment</h3>
<p class="text-slate-600 mb-6 text-lg">
Change values automatically based on target namespace:
</p>
<div class="code-block text-gray-100 p-5 rounded-xl font-mono text-sm overflow-x-auto mb-4 shadow-lg">
<pre class="text-green-400"># Preprod namespaces get preprod API</pre>
<pre><span class="text-yellow-400">- path:</span> data.API_URL
<span class="text-yellow-400">value:</span> <span class="text-blue-400">"https://preprod.api.com"</span>
<span class="text-yellow-400">namespacePattern:</span> <span class="text-blue-400">"preprod-*"</span>
<span class="text-green-400"># Production gets production API</span>
<span class="text-yellow-400">- path:</span> data.API_URL
<span class="text-yellow-400">value:</span> <span class="text-blue-400">"https://api.com"</span>
<span class="text-yellow-400">namespacePattern:</span> <span class="text-blue-400">"prod-*"</span></pre>
</div>
<div class="p-4 bg-purple-50 rounded-lg border border-purple-200">
<p class="text-sm text-slate-600">
<strong class="text-purple-700">Why:</strong> One source ConfigMap, different values per environment. No manual maintenance.
</p>
</div>
</div>
<!-- Automatic Sync -->
<div class="bg-white p-10 rounded-2xl shadow-xl hover-lift border border-green-100">
<div class="bg-gradient-to-br from-green-500 to-teal-600 w-16 h-16 rounded-xl flex items-center justify-center mb-6">
<i class="fas fa-sync-alt text-3xl text-white"></i>
</div>
<h3 class="text-3xl font-bold text-slate-900 mb-4">Automatic Synchronization</h3>
<p class="text-slate-600 mb-6 text-lg">
Update the source once. All copies update automatically:
</p>
<ul class="text-slate-700 space-y-3 text-lg">
<li><i class="fas fa-arrow-right text-blue-500 mr-3"></i>Update source Secret → All 50 copies update</li>
<li><i class="fas fa-arrow-right text-blue-500 mr-3"></i>Delete source → All copies get deleted</li>
<li><i class="fas fa-arrow-right text-blue-500 mr-3"></i>Someone deletes a copy → Recreated automatically</li>
<li><i class="fas fa-arrow-right text-blue-500 mr-3"></i>New namespace created → Copy appears automatically</li>
</ul>
<div class="mt-6 p-4 bg-green-50 rounded-lg border border-green-200">
<p class="text-sm text-slate-600">
<strong class="text-green-700">How:</strong> Uses SHA256 content hashing + Kubernetes generation tracking. Only updates when content actually changes.
</p>
</div>
</div>
<!-- Smart Targeting -->
<div class="bg-white p-10 rounded-2xl shadow-xl hover-lift border border-orange-100">
<div class="bg-gradient-to-br from-orange-500 to-red-600 w-16 h-16 rounded-xl flex items-center justify-center mb-6">
<i class="fas fa-bullseye text-3xl text-white"></i>
</div>
<h3 class="text-3xl font-bold text-slate-900 mb-4">Flexible Targeting</h3>
<p class="text-slate-600 mb-6 text-lg">
Choose which namespaces receive the copy:
</p>
<div class="space-y-4 text-slate-700 text-lg">
<div class="flex items-center gap-3">
<code class="bg-slate-100 px-4 py-2 rounded-lg font-mono text-purple-700 font-semibold">namespace-1,namespace-2</code>
<span>Specific namespaces</span>
</div>
<div class="flex items-center gap-3">
<code class="bg-slate-100 px-4 py-2 rounded-lg font-mono text-purple-700 font-semibold">app-*,prod-*</code>
<span>Pattern matching</span>
</div>
<div class="flex items-center gap-3">
<code class="bg-slate-100 px-4 py-2 rounded-lg font-mono text-purple-700 font-semibold">all-labeled</code>
<span>All labeled namespaces</span>
</div>
</div>
<div class="mt-6 p-4 bg-orange-50 rounded-lg border border-orange-200">
<p class="text-sm text-slate-600">
<strong class="text-orange-700">Safety:</strong> Source namespace never receives a copy. Max 100 targets per resource (configurable).
</p>
</div>
</div>
</div>
</div>
</section>
<!-- Examples Section -->
<section id="examples" class="py-24 bg-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-20">
<h2 class="text-5xl font-extrabold text-slate-900 mb-4">Real-World Examples</h2>
<p class="text-xl text-slate-600">See how easy it is to get started with KubeMirror</p>
</div>
<div class="space-y-12">
<!-- Example 1: Basic Secret -->
<div class="bg-gradient-to-br from-blue-50 to-indigo-50 p-10 rounded-2xl shadow-xl border border-blue-200">
<div class="flex items-start gap-6 mb-6">
<div class="bg-blue-600 w-14 h-14 rounded-xl flex items-center justify-center flex-shrink-0">
<span class="text-white font-bold text-2xl">1</span>
</div>
<div>
<h3 class="text-3xl font-bold text-slate-900 mb-3">
<i class="fas fa-lock text-blue-600 mr-3"></i>
Basic: Mirror a TLS Secret
</h3>
<p class="text-slate-600 text-lg">Share your TLS certificate across multiple application namespaces</p>
</div>
</div>
<div class="code-block text-gray-100 p-6 rounded-xl font-mono text-sm overflow-x-auto shadow-lg">
<pre><span class="text-blue-400">apiVersion:</span> v1
<span class="text-blue-400">kind:</span> Secret
<span class="text-blue-400">metadata:</span>
<span class="text-yellow-400">name:</span> tls-cert
<span class="text-yellow-400">namespace:</span> default
<span class="text-yellow-400">labels:</span>
<span class="text-green-400">kubemirror.raczylo.com/enabled:</span> <span class="text-purple-400">"true"</span>
<span class="text-yellow-400">annotations:</span>
<span class="text-green-400">kubemirror.raczylo.com/sync:</span> <span class="text-purple-400">"true"</span>
<span class="text-green-400">kubemirror.raczylo.com/target-namespaces:</span> <span class="text-purple-400">"app-1,app-2,app-3"</span>
<span class="text-blue-400">type:</span> kubernetes.io/tls
<span class="text-blue-400">data:</span>
<span class="text-yellow-400">tls.crt:</span> LS0tLS1CRUd...
<span class="text-yellow-400">tls.key:</span> LS0tLS1CRUd...</pre>
</div>
</div>
<!-- Example 2: Pattern Matching -->
<div class="bg-gradient-to-br from-purple-50 to-pink-50 p-10 rounded-2xl shadow-xl border border-purple-200">
<div class="flex items-start gap-6 mb-6">
<div class="bg-purple-600 w-14 h-14 rounded-xl flex items-center justify-center flex-shrink-0">
<span class="text-white font-bold text-2xl">2</span>
</div>
<div>
<h3 class="text-3xl font-bold text-slate-900 mb-3">
<i class="fas fa-asterisk text-purple-600 mr-3"></i>
Pattern Matching: Mirror to All App Namespaces
</h3>
<p class="text-slate-600 text-lg">Use wildcards to mirror to all namespaces matching a pattern</p>
</div>
</div>
<div class="code-block text-gray-100 p-6 rounded-xl font-mono text-sm overflow-x-auto shadow-lg">
<pre><span class="text-blue-400">apiVersion:</span> v1
<span class="text-blue-400">kind:</span> ConfigMap
<span class="text-blue-400">metadata:</span>
<span class="text-yellow-400">name:</span> common-config
<span class="text-yellow-400">namespace:</span> default
<span class="text-yellow-400">labels:</span>
<span class="text-green-400">kubemirror.raczylo.com/enabled:</span> <span class="text-purple-400">"true"</span>
<span class="text-yellow-400">annotations:</span>
<span class="text-green-400">kubemirror.raczylo.com/sync:</span> <span class="text-purple-400">"true"</span>
<span class="text-pink-400"># Mirror to ALL namespaces starting with "app-"</span>
<span class="text-green-400">kubemirror.raczylo.com/target-namespaces:</span> <span class="text-purple-400">"app-*"</span>
<span class="text-blue-400">data:</span>
<span class="text-yellow-400">log_level:</span> <span class="text-purple-400">"info"</span>
<span class="text-yellow-400">api_url:</span> <span class="text-purple-400">"https://api.example.com"</span></pre>
</div>
<div class="mt-6 p-5 bg-purple-100 rounded-lg border border-purple-300">
<p class="text-slate-700 text-lg">
<i class="fas fa-info-circle text-purple-600 mr-2"></i>
<strong>Result:</strong> This ConfigMap will be automatically copied to <code class="bg-white px-2 py-1 rounded font-mono text-purple-700">app-frontend</code>, <code class="bg-white px-2 py-1 rounded font-mono text-purple-700">app-backend</code>, <code class="bg-white px-2 py-1 rounded font-mono text-purple-700">app-worker</code>, and any other namespace starting with "app-"
</p>
</div>
</div>
<!-- Example 3: Custom Resource (Traefik) -->
<div class="bg-gradient-to-br from-green-50 to-teal-50 p-10 rounded-2xl shadow-xl border border-green-200">
<div class="flex items-start gap-6 mb-6">
<div class="bg-green-600 w-14 h-14 rounded-xl flex items-center justify-center flex-shrink-0">
<span class="text-white font-bold text-2xl">3</span>
</div>
<div>
<h3 class="text-3xl font-bold text-slate-900 mb-3">
<i class="fas fa-cubes text-green-600 mr-3"></i>
Custom Resource: Share Traefik Middleware
</h3>
<p class="text-slate-600 text-lg">Mirror any CRD like Traefik Middleware across your cluster</p>
</div>
</div>
<div class="code-block text-gray-100 p-6 rounded-xl font-mono text-sm overflow-x-auto shadow-lg">
<pre><span class="text-blue-400">apiVersion:</span> traefik.io/v1alpha1
<span class="text-blue-400">kind:</span> Middleware
<span class="text-blue-400">metadata:</span>
<span class="text-yellow-400">name:</span> compression
<span class="text-yellow-400">namespace:</span> infrastructure
<span class="text-yellow-400">labels:</span>
<span class="text-green-400">kubemirror.raczylo.com/enabled:</span> <span class="text-purple-400">"true"</span>
<span class="text-yellow-400">annotations:</span>
<span class="text-green-400">kubemirror.raczylo.com/sync:</span> <span class="text-purple-400">"true"</span>
<span class="text-pink-400"># Share with all application namespaces</span>
<span class="text-green-400">kubemirror.raczylo.com/target-namespaces:</span> <span class="text-purple-400">"app-*,prod-*"</span>
<span class="text-blue-400">spec:</span>
<span class="text-yellow-400">compress:</span>
<span class="text-yellow-400">excludedContentTypes:</span>
- text/event-stream</pre>
</div>
<div class="mt-6 p-5 bg-green-100 rounded-lg border border-green-300">
<p class="text-slate-700 text-lg">
<i class="fas fa-lightbulb text-green-600 mr-2"></i>
<strong>Works with any CRD:</strong> Cert-Manager Certificates, Gateway API resources, or your own custom resources!
</p>
</div>
</div>
<!-- Example 4: Transformation -->
<div class="bg-gradient-to-br from-orange-50 to-red-50 p-10 rounded-2xl shadow-xl border border-orange-200">
<div class="flex items-start gap-6 mb-6">
<div class="bg-orange-600 w-14 h-14 rounded-xl flex items-center justify-center flex-shrink-0">
<span class="text-white font-bold text-2xl">4</span>
</div>
<div>
<h3 class="text-3xl font-bold text-slate-900 mb-3">
<i class="fas fa-code-branch text-orange-600 mr-3"></i>
Advanced: Environment-Specific Configuration
</h3>
<p class="text-slate-600 text-lg">Transform values based on target environment using namespace patterns</p>
</div>
</div>
<div class="code-block text-gray-100 p-6 rounded-xl font-mono text-sm overflow-x-auto shadow-lg">
<pre><span class="text-blue-400">apiVersion:</span> v1
<span class="text-blue-400">kind:</span> ConfigMap
<span class="text-blue-400">metadata:</span>
<span class="text-yellow-400">name:</span> app-config
<span class="text-yellow-400">namespace:</span> default
<span class="text-yellow-400">labels:</span>
<span class="text-green-400">kubemirror.raczylo.com/enabled:</span> <span class="text-purple-400">"true"</span>
<span class="text-yellow-400">annotations:</span>
<span class="text-green-400">kubemirror.raczylo.com/sync:</span> <span class="text-purple-400">"true"</span>
<span class="text-green-400">kubemirror.raczylo.com/target-namespaces:</span> <span class="text-purple-400">"preprod-api,prod-api,staging-api"</span>
<span class="text-green-400">kubemirror.raczylo.com/transform:</span> |
<span class="text-yellow-400">rules:</span>
<span class="text-pink-400"># Preprod gets preprod GraphQL endpoint</span>
- <span class="text-yellow-400">path:</span> data.GRAPHQL_HOST
<span class="text-yellow-400">value:</span> <span class="text-purple-400">"https://preprod.example.com/v1/graphql"</span>
<span class="text-yellow-400">namespacePattern:</span> <span class="text-purple-400">"preprod-*"</span>
<span class="text-pink-400"># Production gets production endpoint</span>
- <span class="text-yellow-400">path:</span> data.GRAPHQL_HOST
<span class="text-yellow-400">value:</span> <span class="text-purple-400">"https://api.example.com/v1/graphql"</span>
<span class="text-yellow-400">namespacePattern:</span> <span class="text-purple-400">"prod-*"</span>
<span class="text-pink-400"># Set log level to error in production</span>
- <span class="text-yellow-400">path:</span> data.LOG_LEVEL
<span class="text-yellow-400">value:</span> <span class="text-purple-400">"error"</span>
<span class="text-yellow-400">namespacePattern:</span> <span class="text-purple-400">"prod-*"</span>
<span class="text-blue-400">data:</span>
<span class="text-yellow-400">GRAPHQL_HOST:</span> <span class="text-purple-400">"https://localhost/v1/graphql"</span>
<span class="text-yellow-400">LOG_LEVEL:</span> <span class="text-purple-400">"debug"</span></pre>
</div>
<div class="mt-6 p-5 bg-orange-100 rounded-lg border border-orange-300">
<p class="text-slate-700 text-lg">
<i class="fas fa-magic text-orange-600 mr-2"></i>
<strong>Result:</strong> Each environment gets the right configuration automatically. One source, multiple environments, zero manual work!
</p>
</div>
</div>
<!-- Example 5: Go Templates -->
<div class="bg-gradient-to-br from-pink-50 to-rose-50 p-10 rounded-2xl shadow-xl border border-pink-200">
<div class="flex items-start gap-6 mb-6">
<div class="bg-pink-600 w-14 h-14 rounded-xl flex items-center justify-center flex-shrink-0">
<span class="text-white font-bold text-2xl">5</span>
</div>
<div>
<h3 class="text-3xl font-bold text-slate-900 mb-3">
<i class="fas fa-code text-pink-600 mr-3"></i>
Go Templates: Dynamic Value Generation
</h3>
<p class="text-slate-600 text-lg">Use Go template functions to generate namespace-specific values dynamically</p>
</div>
</div>
<div class="code-block text-gray-100 p-6 rounded-xl font-mono text-sm overflow-x-auto shadow-lg">
<pre><span class="text-blue-400">apiVersion:</span> v1
<span class="text-blue-400">kind:</span> ConfigMap
<span class="text-blue-400">metadata:</span>
<span class="text-yellow-400">name:</span> dynamic-config
<span class="text-yellow-400">namespace:</span> default
<span class="text-yellow-400">labels:</span>
<span class="text-green-400">kubemirror.raczylo.com/enabled:</span> <span class="text-purple-400">"true"</span>
<span class="text-yellow-400">annotations:</span>
<span class="text-green-400">kubemirror.raczylo.com/sync:</span> <span class="text-purple-400">"true"</span>
<span class="text-green-400">kubemirror.raczylo.com/target-namespaces:</span> <span class="text-purple-400">"app-frontend,app-backend,app-worker"</span>
<span class="text-green-400">kubemirror.raczylo.com/transform:</span> |
<span class="text-yellow-400">rules:</span>
<span class="text-pink-400"># Generate namespace-specific API URL</span>
- <span class="text-yellow-400">path:</span> data.API_URL
<span class="text-yellow-400">template:</span> <span class="text-purple-400">"https://{{.TargetNamespace}}.api.example.com"</span>
<span class="text-pink-400"># Generate database connection string</span>
- <span class="text-yellow-400">path:</span> data.DATABASE_URL
<span class="text-yellow-400">template:</span> <span class="text-purple-400">"postgres://{{.TargetNamespace}}.db.svc:5432/app"</span>
<span class="text-pink-400"># Use string manipulation functions</span>
- <span class="text-yellow-400">path:</span> data.CACHE_PREFIX
<span class="text-yellow-400">template:</span> <span class="text-purple-400">"{{replace .TargetNamespace \"-\" \"_\"}}"</span>
<span class="text-pink-400"># Uppercase namespace for environment variable</span>
- <span class="text-yellow-400">path:</span> data.NAMESPACE_UPPER
<span class="text-yellow-400">template:</span> <span class="text-purple-400">"{{upper .TargetNamespace}}"</span>
<span class="text-blue-400">data:</span>
<span class="text-yellow-400">API_URL:</span> <span class="text-purple-400">"https://localhost:8080"</span>
<span class="text-yellow-400">DATABASE_URL:</span> <span class="text-purple-400">"postgres://localhost:5432/app"</span>
<span class="text-yellow-400">CACHE_PREFIX:</span> <span class="text-purple-400">"default"</span>
<span class="text-yellow-400">NAMESPACE_UPPER:</span> <span class="text-purple-400">"DEFAULT"</span></pre>
</div>
<div class="mt-6 p-5 bg-pink-100 rounded-lg border border-pink-300">
<p class="text-slate-700 text-lg mb-4">
<i class="fas fa-info-circle text-pink-600 mr-2"></i>
<strong>Available Template Variables:</strong>
</p>
<div class="grid md:grid-cols-2 gap-3 text-sm font-mono">
<div><code class="bg-white px-2 py-1 rounded text-pink-700">.TargetNamespace</code> - Target namespace name</div>
<div><code class="bg-white px-2 py-1 rounded text-pink-700">.SourceNamespace</code> - Source namespace name</div>
<div><code class="bg-white px-2 py-1 rounded text-pink-700">.SourceName</code> - Source resource name</div>
<div><code class="bg-white px-2 py-1 rounded text-pink-700">.TargetName</code> - Mirror resource name</div>
</div>
<p class="text-slate-700 text-lg mt-4">
<strong>Template Functions:</strong> <code class="bg-white px-2 py-1 rounded font-mono text-pink-700">upper</code>, <code class="bg-white px-2 py-1 rounded font-mono text-pink-700">lower</code>, <code class="bg-white px-2 py-1 rounded font-mono text-pink-700">replace</code>, <code class="bg-white px-2 py-1 rounded font-mono text-pink-700">trimPrefix</code>, <code class="bg-white px-2 py-1 rounded font-mono text-pink-700">trimSuffix</code>, <code class="bg-white px-2 py-1 rounded font-mono text-pink-700">default</code>
</p>
</div>
</div>
</div>
</div>
</section>
<!-- Comparison Section -->
<section id="comparison" class="py-24 bg-gradient-to-br from-slate-50 to-blue-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-16">
<h2 class="text-5xl font-extrabold text-slate-900 mb-6">How KubeMirror Compares</h2>
<p class="text-2xl text-slate-600">We built KubeMirror to replace <a href="https://github.com/emberstack/kubernetes-reflector" class="text-blue-600 hover:underline font-semibold" target="_blank">emberstack/reflector</a></p>
</div>
<div class="overflow-x-auto rounded-2xl shadow-2xl">
<table class="w-full bg-white">
<thead class="bg-gradient-to-r from-slate-800 to-slate-900 text-white">
<tr>
<th class="px-8 py-6 text-left font-bold text-lg">Capability</th>
<th class="px-8 py-6 text-center font-bold text-lg">KubeMirror</th>
<th class="px-8 py-6 text-center font-bold text-lg">Reflector</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-200">
<tr class="hover:bg-blue-50 transition-colors">
<td class="px-8 py-6">
<div class="font-semibold text-lg text-slate-900">Supported Resources</div>
<div class="text-sm text-slate-600 mt-1">What resource types can be mirrored</div>
</td>
<td class="px-8 py-6 text-center">
<div><i class="fas fa-check-circle text-green-500 text-3xl"></i></div>
<div class="text-sm font-semibold text-green-700 mt-2">Secrets, ConfigMaps, CRDs, etc.</div>
</td>
<td class="px-8 py-6 text-center">
<div><i class="fas fa-minus-circle text-yellow-500 text-3xl"></i></div>
<div class="text-sm font-semibold text-yellow-700 mt-2">Secrets, ConfigMaps only</div>
</td>
</tr>
<tr class="hover:bg-blue-50 transition-colors bg-slate-50">
<td class="px-8 py-6">
<div class="font-semibold text-lg text-slate-900">Auto-Discovery</div>
<div class="text-sm text-slate-600 mt-1">Finds all resource types automatically</div>
</td>
<td class="px-8 py-6 text-center">
<div><i class="fas fa-check-circle text-green-500 text-3xl"></i></div>
<div class="text-sm font-semibold text-green-700 mt-2">Yes</div>
</td>
<td class="px-8 py-6 text-center">
<div><i class="fas fa-times-circle text-red-500 text-3xl"></i></div>
<div class="text-sm font-semibold text-red-700 mt-2">Hardcoded</div>
</td>
</tr>
<tr class="hover:bg-blue-50 transition-colors">
<td class="px-8 py-6">
<div class="font-semibold text-lg text-slate-900">Value Transformation</div>
<div class="text-sm text-slate-600 mt-1">Change values per target namespace</div>
</td>
<td class="px-8 py-6 text-center">
<div><i class="fas fa-check-circle text-green-500 text-3xl"></i></div>
<div class="text-sm font-semibold text-green-700 mt-2">Full support</div>
</td>
<td class="px-8 py-6 text-center">
<div><i class="fas fa-times-circle text-red-500 text-3xl"></i></div>
<div class="text-sm font-semibold text-red-700 mt-2">Not available</div>
</td>
</tr>
<tr class="hover:bg-blue-50 transition-colors bg-slate-50">
<td class="px-8 py-6">
<div class="font-semibold text-lg text-slate-900">Environment-Specific Rules</div>
<div class="text-sm text-slate-600 mt-1">Different transforms for dev/staging/prod</div>
</td>
<td class="px-8 py-6 text-center">
<div><i class="fas fa-check-circle text-green-500 text-3xl"></i></div>
<div class="text-sm font-semibold text-green-700 mt-2">Namespace patterns</div>
</td>
<td class="px-8 py-6 text-center">
<div><i class="fas fa-times-circle text-red-500 text-3xl"></i></div>
<div class="text-sm font-semibold text-red-700 mt-2">Not available</div>
</td>
</tr>
<tr class="hover:bg-blue-50 transition-colors">
<td class="px-8 py-6">
<div class="font-semibold text-lg text-slate-900">API Efficiency</div>
<div class="text-sm text-slate-600 mt-1">Impact on Kubernetes API server</div>
</td>
<td class="px-8 py-6 text-center">
<div><i class="fas fa-check-circle text-green-500 text-3xl"></i></div>
<div class="text-sm font-semibold text-green-700 mt-2">90% less load</div>
</td>
<td class="px-8 py-6 text-center">
<div><i class="fas fa-minus-circle text-yellow-500 text-3xl"></i></div>
<div class="text-sm font-semibold text-yellow-700 mt-2">Higher load</div>
</td>
</tr>
<tr class="hover:bg-blue-50 transition-colors bg-slate-50">
<td class="px-8 py-6">
<div class="font-semibold text-lg text-slate-900">Active Development</div>
<div class="text-sm text-slate-600 mt-1">Regular updates and bug fixes</div>
</td>
<td class="px-8 py-6 text-center">
<div><i class="fas fa-check-circle text-green-500 text-3xl"></i></div>
<div class="text-sm font-semibold text-green-700 mt-2">Active</div>
</td>
<td class="px-8 py-6 text-center">
<div><i class="fas fa-check-circle text-green-500 text-3xl"></i></div>
<div class="text-sm font-semibold text-green-700 mt-2">Recently resumed (2025)</div>
</td>
</tr>
</tbody>
</table>
</div>
<div class="mt-16 bg-gradient-to-br from-blue-50 to-indigo-100 border-l-4 border-blue-600 p-8 rounded-xl shadow-xl">
<div class="flex items-start gap-6">
<i class="fas fa-info-circle text-blue-600 text-4xl mt-1"></i>
<div>
<h4 class="text-2xl font-bold text-slate-900 mb-4">Why We Built KubeMirror</h4>
<p class="text-slate-700 text-lg leading-relaxed">
We needed to share Traefik Middleware across 200+ namespaces with environment-specific configurations.
Reflector couldn't do it (Secrets/ConfigMaps only, no transformations). So we built KubeMirror with modern
Kubernetes best practices and all the features we wished Reflector had.
</p>
</div>
</div>
</div>
</div>
</section>
<!-- Installation Section -->
<section id="installation" class="py-24 bg-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-20">
<h2 class="text-5xl font-extrabold text-slate-900 mb-4">Installation</h2>
<p class="text-2xl text-slate-600">Get started in under 2 minutes</p>
</div>
<div class="grid md:grid-cols-2 gap-10 mb-16">
<!-- Helm Installation -->
<div class="bg-gradient-to-br from-blue-50 to-indigo-50 p-10 rounded-2xl shadow-xl border border-blue-200 hover-lift">
<div class="flex items-center mb-6">
<div class="bg-blue-600 w-14 h-14 rounded-xl flex items-center justify-center mr-4">
<i class="fas fa-ship text-2xl text-white"></i>
</div>
<h3 class="text-3xl font-bold text-slate-900">Helm <span class="text-blue-600">(Recommended)</span></h3>
</div>
<div class="code-block text-gray-100 p-6 rounded-xl font-mono text-sm overflow-x-auto shadow-lg">
<pre><span class="text-green-400">helm repo add kubemirror \</span>
https://lukaszraczylo.github.io/helm-charts
<span class="text-green-400">helm install kubemirror \</span>
kubemirror/kubemirror \
--namespace kubemirror-system \
--create-namespace</pre>
</div>
</div>
<!-- kubectl Installation -->
<div class="bg-gradient-to-br from-purple-50 to-pink-50 p-10 rounded-2xl shadow-xl border border-purple-200 hover-lift">
<div class="flex items-center mb-6">
<div class="bg-purple-600 w-14 h-14 rounded-xl flex items-center justify-center mr-4">
<i class="fas fa-terminal text-2xl text-white"></i>
</div>
<h3 class="text-3xl font-bold text-slate-900">kubectl</h3>
</div>
<div class="code-block text-gray-100 p-6 rounded-xl font-mono text-sm overflow-x-auto shadow-lg">
<pre><span class="text-green-400">kubectl apply -k \</span>
github.com/lukaszraczylo/kubemirror/deploy
<span class="text-pink-400"># Or with specific version</span>
<span class="text-green-400">kubectl apply -k \</span>
github.com/lukaszraczylo/kubemirror/deploy?ref=v1.0.0</pre>
</div>
</div>
</div>
<!-- Quick Start Example -->
<div class="bg-gradient-to-br from-green-50 to-teal-50 p-12 rounded-2xl shadow-2xl border border-green-200">
<h3 class="text-4xl font-bold text-slate-900 mb-8 text-center">
<i class="fas fa-rocket text-green-600 mr-3"></i>
Quick Start: Mirror a Secret in 30 Seconds
</h3>
<div class="grid md:grid-cols-2 gap-10">
<div>
<div class="flex items-center gap-3 mb-6">
<div class="bg-green-600 w-10 h-10 rounded-full flex items-center justify-center text-white font-bold">1</div>
<h4 class="font-bold text-2xl text-slate-900">Create your source Secret</h4>
</div>
<div class="code-block text-gray-100 p-6 rounded-xl font-mono text-sm overflow-x-auto shadow-lg">
<pre><span class="text-blue-400">apiVersion:</span> v1
<span class="text-blue-400">kind:</span> Secret
<span class="text-blue-400">metadata:</span>
<span class="text-yellow-400">name:</span> tls-cert
<span class="text-yellow-400">namespace:</span> default
<span class="text-yellow-400">labels:</span>
<span class="text-green-400">kubemirror.raczylo.com/enabled:</span> <span class="text-purple-400">"true"</span>
<span class="text-yellow-400">annotations:</span>
<span class="text-green-400">kubemirror.raczylo.com/sync:</span> <span class="text-purple-400">"true"</span>
<span class="text-green-400">kubemirror.raczylo.com/target-namespaces:</span> <span class="text-purple-400">"app-1,app-2"</span>
<span class="text-blue-400">type:</span> kubernetes.io/tls
<span class="text-blue-400">data:</span>
<span class="text-yellow-400">tls.crt:</span> LS0tLS1CRUd...
<span class="text-yellow-400">tls.key:</span> LS0tLS1CRUd...</pre>
</div>
</div>
<div>
<div class="flex items-center gap-3 mb-6">
<div class="bg-green-600 w-10 h-10 rounded-full flex items-center justify-center text-white font-bold">2</div>
<h4 class="font-bold text-2xl text-slate-900">That's it!</h4>
</div>
<p class="text-slate-700 mb-6 text-lg">
KubeMirror automatically:
</p>
<ul class="text-slate-700 space-y-4 text-lg">
<li class="flex items-start gap-3">
<i class="fas fa-check-circle text-green-500 text-xl mt-1"></i>
<span>Creates copies in <code class="bg-white px-2 py-1 rounded font-mono text-green-700">app-1</code> and <code class="bg-white px-2 py-1 rounded font-mono text-green-700">app-2</code></span>
</li>
<li class="flex items-start gap-3">
<i class="fas fa-check-circle text-green-500 text-xl mt-1"></i>
<span>Keeps them synchronized when you update the source</span>
</li>
<li class="flex items-start gap-3">
<i class="fas fa-check-circle text-green-500 text-xl mt-1"></i>
<span>Recreates them if someone deletes a copy</span>
</li>
<li class="flex items-start gap-3">
<i class="fas fa-check-circle text-green-500 text-xl mt-1"></i>
<span>Cleans up all copies when you delete the source</span>
</li>
</ul>
<div class="mt-8 p-5 bg-green-100 rounded-lg border border-green-300">
<p class="text-sm text-slate-700">
<strong class="text-green-800">Required:</strong> Both the label <code class="bg-white px-2 py-1 rounded font-mono text-green-700">kubemirror.raczylo.com/enabled</code>
and annotation <code class="bg-white px-2 py-1 rounded font-mono text-green-700">kubemirror.raczylo.com/sync</code> are needed.
</p>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Documentation Section -->
<section class="py-24 bg-gradient-to-br from-slate-50 to-blue-50">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-20">
<h2 class="text-5xl font-extrabold text-slate-900 mb-4">Documentation</h2>
<p class="text-xl text-slate-600">Everything you need to know</p>
</div>
<div class="grid md:grid-cols-3 gap-10">
<a href="https://github.com/lukaszraczylo/kubemirror#usage-examples" target="_blank" class="bg-white p-10 rounded-2xl shadow-xl hover-lift border border-blue-100 group">
<div class="bg-gradient-to-br from-blue-500 to-purple-600 w-16 h-16 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
<i class="fas fa-book-open text-3xl text-white"></i>
</div>
<h3 class="text-2xl font-bold text-slate-900 mb-3 group-hover:text-blue-600 transition-colors">Usage Examples</h3>
<p class="text-slate-600 text-lg">Real-world examples for common use cases</p>
</a>
<a href="https://github.com/lukaszraczylo/kubemirror/tree/main/examples#transformation-rules" target="_blank" class="bg-white p-10 rounded-2xl shadow-xl hover-lift border border-purple-100 group">
<div class="bg-gradient-to-br from-purple-500 to-pink-600 w-16 h-16 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
<i class="fas fa-magic text-3xl text-white"></i>
</div>
<h3 class="text-2xl font-bold text-slate-900 mb-3 group-hover:text-purple-600 transition-colors">Transformation Rules</h3>
<p class="text-slate-600 text-lg">Change values based on target environment</p>
</a>
<a href="https://github.com/lukaszraczylo/kubemirror#configuration" target="_blank" class="bg-white p-10 rounded-2xl shadow-xl hover-lift border border-green-100 group">
<div class="bg-gradient-to-br from-green-500 to-teal-600 w-16 h-16 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
<i class="fas fa-cog text-3xl text-white"></i>
</div>
<h3 class="text-2xl font-bold text-slate-900 mb-3 group-hover:text-green-600 transition-colors">Configuration</h3>
<p class="text-slate-600 text-lg">Helm values and advanced settings</p>
</a>
</div>
</div>
</section>
<!-- Footer -->
<footer class="bg-gradient-to-br from-slate-900 to-slate-800 text-gray-300 py-16">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid md:grid-cols-3 gap-12">
<div>
<div class="flex items-center mb-6">
<div class="bg-gradient-to-br from-blue-500 to-purple-600 p-2 rounded-lg mr-3">
<i class="fas fa-copy text-2xl text-white"></i>
</div>
<span class="text-2xl font-bold text-white">KubeMirror</span>
</div>
<p class="text-gray-400 text-lg leading-relaxed">
Copy Kubernetes resources across namespaces. Modern replacement for Reflector.
</p>
</div>
<div>
<h4 class="text-xl font-bold text-white mb-6">Links</h4>
<ul class="space-y-3 text-lg">
<li><a href="https://github.com/lukaszraczylo/kubemirror" target="_blank" class="hover:text-white transition-colors"><i class="fab fa-github mr-2"></i>GitHub</a></li>
<li><a href="https://github.com/lukaszraczylo/kubemirror/issues" target="_blank" class="hover:text-white transition-colors"><i class="fas fa-bug mr-2"></i>Report Issue</a></li>
<li><a href="https://github.com/lukaszraczylo/kubemirror/releases" target="_blank" class="hover:text-white transition-colors"><i class="fas fa-tag mr-2"></i>Releases</a></li>
</ul>
</div>
<div>
<h4 class="text-xl font-bold text-white mb-6">License</h4>
<p class="text-gray-400 text-lg">MIT License</p>
<p class="text-gray-400 mt-4 text-lg">© 2024 Lukasz Raczylo</p>
</div>
</div>
</div>
</footer>
</body>
</html>
+370
View File
@@ -248,3 +248,373 @@ metadata:
data:
config: "value"
```
## Transformation Rules
KubeMirror supports transformation rules that modify resources during mirroring. This enables environment-specific configurations, security hardening, and dynamic value generation.
### Transformation Examples
The repository includes comprehensive transformation examples in `transform-configmap.yaml` and `transform-secret.yaml`. These demonstrate:
1. **Static Value Transformation** - Replace values with constants
2. **Template-Based Transformation** - Generate dynamic values using Go templates
3. **Merge Transformation** - Add labels, annotations, or map entries
4. **Delete Transformation** - Remove sensitive or environment-specific fields
5. **Multi-Rule Transformations** - Combine multiple transformation types
### Quick Start with Transformations
```bash
# Apply transformation examples
kubectl apply -f examples/transform-configmap.yaml
kubectl apply -f examples/transform-secret.yaml
# Verify transformed ConfigMap
kubectl get configmap app-config-template -n namespace-2 -o yaml
# Check that the API_URL was transformed for namespace-2
kubectl get configmap app-config-template -n namespace-2 \
-o jsonpath='{.data.API_URL}'
# Expected: https://namespace-2.api.example.com
```
### Transformation Rule Types
#### 1. Value Rules (Static Replacement)
Replace a field with a static value:
```yaml
annotations:
kubemirror.raczylo.com/transform: |
rules:
- path: data.LOG_LEVEL
value: "error"
```
#### 2. Template Rules (Dynamic Generation)
Use Go templates with context variables:
```yaml
annotations:
kubemirror.raczylo.com/transform: |
rules:
- path: data.API_URL
template: "https://{{.TargetNamespace}}.api.example.com"
```
**Available template variables:**
- `.TargetNamespace` - Namespace where mirror is created
- `.SourceNamespace` - Original resource namespace
- `.SourceName` - Original resource name
- `.TargetName` - Mirror resource name
- `.Labels` - Map of source labels
- `.Annotations` - Map of source annotations
**Template functions:**
- `upper` - Convert to uppercase
- `lower` - Convert to lowercase
- `replace` - String replacement: `{{replace .TargetNamespace "-" "_"}}`
- `trimPrefix` - Remove prefix: `{{trimPrefix .TargetNamespace "namespace-"}}`
- `trimSuffix` - Remove suffix
- `hasPrefix` - Check for prefix
- `hasSuffix` - Check for suffix
- `default` - Provide fallback: `{{default "fallback" .OptionalField}}`
#### 3. Merge Rules (Add Entries)
Merge additional entries into maps (labels, annotations, data):
```yaml
annotations:
kubemirror.raczylo.com/transform: |
rules:
- path: metadata.labels
merge:
environment: "production"
managed-by: "kubemirror"
```
#### 4. Delete Rules (Remove Fields)
Remove sensitive or unnecessary fields:
```yaml
annotations:
kubemirror.raczylo.com/transform: |
rules:
- path: data.DEBUG_MODE
delete: true
- path: data.ADMIN_PASSWORD
delete: true
```
### Array Indexing
Transform specific elements in arrays using bracket notation `[index]`:
```yaml
annotations:
kubemirror.raczylo.com/transform: |
rules:
# Update first container's image
- path: spec.template.spec.containers[0].image
template: "registry.{{.TargetNamespace}}.example.com/app:v1"
# Update second environment variable
- path: spec.template.spec.containers[0].env[1].value
template: "postgres://{{.TargetNamespace}}-db.svc.cluster.local:5432"
# Update nested arrays (container → env vars → value)
- path: spec.template.spec.containers[0].env[2].value
value: "production"
# Update volume ConfigMap reference
- path: spec.template.spec.volumes[0].configMap.name
template: "{{.TargetNamespace}}-config"
```
**Common use cases for array indexing:**
- Container images: `spec.template.spec.containers[0].image`
- Environment variables: `spec.template.spec.containers[0].env[N].value`
- Volume mounts: `spec.template.spec.containers[0].volumeMounts[N].mountPath`
- Init containers: `spec.template.spec.initContainers[0].image`
- Volume references: `spec.template.spec.volumes[N].configMap.name`
- Resource limits: `spec.template.spec.containers[0].resources.limits.memory`
**Important notes:**
- Array indexes are zero-based (`[0]` is the first element)
- Index must be within array bounds or transformation will fail
- Use with strict mode to catch out-of-bounds errors
- See `transform-deployment.yaml` for comprehensive Deployment examples
### Namespace Patterns
Apply transformation rules conditionally based on target namespace patterns using glob-style matching.
#### Basic Pattern Matching
Limit a rule to specific namespaces using the `namespacePattern` field:
```yaml
annotations:
kubemirror.raczylo.com/transform: |
rules:
# Only apply to preprod namespaces
- path: data.GRAPHQL_HOST
value: "https://preprod.example.com/v1/graphql"
namespacePattern: "preprod-*"
# Only apply to production namespaces
- path: data.GRAPHQL_HOST
value: "https://api.example.com/v1/graphql"
namespacePattern: "prod-*"
```
#### Supported Pattern Syntax
- `*` - Matches zero or more characters
- `?` - Matches exactly one character
- No pattern or empty pattern - Matches all namespaces
**Examples:**
- `preprod-*` - Matches `preprod-api`, `preprod-worker`, `preprod-db`
- `*-staging` - Matches `app-staging`, `api-staging`
- `prod-*-v?` - Matches `prod-api-v1`, `prod-db-v2`
- `namespace-?` - Matches `namespace-1`, `namespace-2` (single digit only)
#### Pattern Matching Rules
1. **Rules without patterns** apply to **all namespaces**
2. **Rules with patterns** only apply when the pattern matches the target namespace
3. **Multiple rules with different patterns** can coexist - each is evaluated independently
4. **Pattern matching is case-sensitive**
#### Example: Environment-Specific Configuration
```yaml
annotations:
kubemirror.raczylo.com/transform: |
rules:
# Global rule - applies to all namespaces
- path: data.APP_NAME
value: "my-app"
# Preprod configuration
- path: data.LOG_LEVEL
value: "debug"
namespacePattern: "preprod-*"
- path: data.DATABASE_URL
template: "postgres://{{.TargetNamespace}}.db.preprod.example.com:5432"
namespacePattern: "preprod-*"
# Production configuration
- path: data.LOG_LEVEL
value: "error"
namespacePattern: "prod-*"
- path: data.DATABASE_URL
template: "postgres://{{.TargetNamespace}}.db.prod.example.com:5432"
namespacePattern: "prod-*"
```
In this example:
- `data.APP_NAME` is set in **all** mirrored namespaces
- `data.LOG_LEVEL` is `debug` in preprod namespaces, `error` in prod namespaces
- `data.DATABASE_URL` is environment-specific based on the namespace pattern
#### Combining Patterns with Templates
Namespace patterns work seamlessly with template rules:
```yaml
rules:
# Apply different API endpoints based on namespace
- path: data.API_ENDPOINT
template: "https://{{.TargetNamespace}}.api.preprod.com"
namespacePattern: "preprod-*"
- path: data.API_ENDPOINT
template: "https://{{.TargetNamespace}}.api.example.com"
namespacePattern: "prod-*"
```
#### Pattern Verification
```bash
# Verify preprod pattern matching
kubectl get configmap app-config-pattern -n preprod-api \
-o jsonpath='{.data.GRAPHQL_HOST}'
# Expected: https://preprod.example.com/v1/graphql
kubectl get configmap app-config-pattern -n prod-api \
-o jsonpath='{.data.GRAPHQL_HOST}'
# Expected: https://api.example.com/v1/graphql
# Verify multi-pattern configuration
kubectl get configmap app-config-multipattern -n namespace-2 \
-o jsonpath='{.data.ENVIRONMENT}'
# Expected: development
kubectl get configmap app-config-multipattern -n preprod-api \
-o jsonpath='{.data.ENVIRONMENT}'
# Expected: preproduction
```
### Strict Mode
By default, transformation errors are logged but don't block mirroring. Enable strict mode to fail mirroring on transformation errors:
```yaml
annotations:
kubemirror.raczylo.com/transform-strict: "true"
kubemirror.raczylo.com/transform: |
rules:
- path: data.CRITICAL_VALUE
value: "must-succeed"
```
### Transformation Verification
```bash
# Check static value transformation
kubectl get configmap app-config-static -n namespace-2 \
-o jsonpath='{.data.LOG_LEVEL}'
# Expected: error
# Check template transformation
kubectl get configmap app-config-template -n namespace-3 \
-o jsonpath='{.data.API_URL}'
# Expected: https://namespace-3.api.example.com
# Check merge transformation (labels should include new entries)
kubectl get configmap app-config-merge -n namespace-2 -o yaml | grep -A 5 labels
# Should include: environment: production, managed-by: kubemirror
# Check delete transformation (fields should be removed)
kubectl get configmap app-config-delete -n namespace-2 -o yaml | grep DEBUG_MODE
# Should return nothing (field deleted)
# Check Secret transformations
kubectl get secret database-credentials -n namespace-2 \
-o jsonpath='{.data.DB_HOST}' | base64 -d
# Expected: namespace-2.postgres.svc.cluster.local
```
### Common Transformation Patterns
#### Environment-Specific Configuration
```yaml
rules:
- path: data.LOG_LEVEL
template: |
{{- if hasPrefix .TargetNamespace "prod-" -}}
error
{{- else if hasPrefix .TargetNamespace "staging-" -}}
warn
{{- else -}}
debug
{{- end }}
```
#### Namespace-Based Service Discovery
```yaml
rules:
- path: data.DATABASE_HOST
template: "postgres.{{.TargetNamespace}}.svc.cluster.local"
- path: data.REDIS_HOST
template: "redis.{{.TargetNamespace}}.svc.cluster.local"
```
#### Security Hardening
```yaml
rules:
# Remove development credentials
- path: data.DEV_API_KEY
delete: true
# Set production encryption
- path: data.ENCRYPTION_STRENGTH
value: "AES-256"
# Add security labels
- path: metadata.labels
merge:
security-tier: "high"
encrypted: "true"
```
### Troubleshooting Transformations
```bash
# View transformation errors in controller logs
kubectl logs -n kubemirror-system -l app.kubernetes.io/name=kubemirror | grep -i transform
# Check if strict mode is blocking mirroring
kubectl get events --all-namespaces | grep -i "transformation.*failed"
# Verify transformation annotation is valid YAML
kubectl get configmap <name> -n <namespace> \
-o jsonpath='{.metadata.annotations.kubemirror\.raczylo\.com/transform}' | yq eval -
```
### Performance Considerations
- **Rule Limit**: Maximum 50 rules per resource (configurable)
- **Rule Size**: Maximum 10KB of YAML per resource (configurable)
- **Template Timeout**: 100ms per template execution (configurable)
- **Overhead**: <1ms average transformation time per mirror
### Security Notes
1. **Template Sandboxing**: Templates execute in a sandboxed environment with no file, network, or command access
2. **Timeout Protection**: Template execution is strictly time-limited to prevent DoS
3. **Size Limits**: Rules have size limits to prevent resource exhaustion
4. **No Code Execution**: Templates use predefined safe functions only
+3
View File
@@ -7,6 +7,9 @@ resources:
- source-secret.yaml
- source-configmap.yaml
- traefik-middleware.yaml
- transform-configmap.yaml
- transform-secret.yaml
- transform-deployment.yaml
commonLabels:
managed-by: kustomize
+322
View File
@@ -0,0 +1,322 @@
# Transformation Examples for KubeMirror
# These examples demonstrate the transformation rules feature
---
# Example 1: Static Value Transformation
# Changes LOG_LEVEL to "error" in all mirrors
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-static
namespace: namespace-1
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "namespace-2,namespace-3"
kubemirror.raczylo.com/transform: |
rules:
- path: data.LOG_LEVEL
value: "error"
labels:
kubemirror.raczylo.com/enabled: "true"
example: "static-value-transform"
data:
LOG_LEVEL: "debug"
APP_NAME: "my-app"
---
# Example 2: Template-Based Transformation
# Creates namespace-specific API URLs
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-template
namespace: namespace-1
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "namespace-2,namespace-3,namespace-4"
kubemirror.raczylo.com/transform: |
rules:
- path: data.API_URL
template: "https://{{.TargetNamespace}}.api.example.com"
- path: data.NAMESPACE_UPPER
template: "{{upper .TargetNamespace}}"
labels:
kubemirror.raczylo.com/enabled: "true"
example: "template-transform"
data:
API_URL: "https://default.api.example.com"
NAMESPACE_UPPER: "DEFAULT"
---
# Example 3: Merge Transformation
# Adds environment-specific labels to mirrored ConfigMaps
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-merge
namespace: namespace-1
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "namespace-2,namespace-3"
kubemirror.raczylo.com/transform: |
rules:
- path: metadata.labels
merge:
environment: "production"
managed-by: "kubemirror"
labels:
kubemirror.raczylo.com/enabled: "true"
app: "myapp"
example: "merge-transform"
data:
config: "value"
---
# Example 4: Delete Transformation
# Removes sensitive debug fields from mirrors
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-delete
namespace: namespace-1
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "namespace-2,namespace-3"
kubemirror.raczylo.com/transform: |
rules:
- path: data.DEBUG_MODE
delete: true
- path: data.INTERNAL_API_KEY
delete: true
labels:
kubemirror.raczylo.com/enabled: "true"
example: "delete-transform"
data:
DEBUG_MODE: "true"
INTERNAL_API_KEY: "secret-key"
PUBLIC_CONFIG: "safe-value"
---
# Example 5: Multi-Rule Complex Transformation
# Combines value, template, merge, and delete operations
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-complex
namespace: namespace-1
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "namespace-2,namespace-3,namespace-4"
kubemirror.raczylo.com/transform: |
rules:
# Set log level to error in production mirrors
- path: data.LOG_LEVEL
value: "error"
# Create namespace-specific database URL
- path: data.DATABASE_URL
template: "postgres://{{.TargetNamespace}}.db.svc.cluster.local:5432/app"
# Create namespace-specific cache prefix
- path: data.CACHE_PREFIX
template: "{{replace .TargetNamespace \"-\" \"_\"}}"
# Add environment labels
- path: metadata.labels
merge:
environment: "production"
tier: "backend"
# Remove debug configurations
- path: data.DEBUG_MODE
delete: true
# Remove development API keys
- path: data.DEV_API_KEY
delete: true
labels:
kubemirror.raczylo.com/enabled: "true"
app: "complex-app"
example: "multi-rule-transform"
data:
LOG_LEVEL: "debug"
DATABASE_URL: "postgres://localhost:5432/app"
CACHE_PREFIX: "dev"
DEBUG_MODE: "true"
DEV_API_KEY: "dev-key-12345"
APP_NAME: "my-app"
---
# Example 6: Template Functions Showcase
# Demonstrates all available template functions
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-functions
namespace: namespace-1
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "namespace-2"
kubemirror.raczylo.com/transform: |
rules:
# String manipulation
- path: data.UPPER_NAMESPACE
template: "{{upper .TargetNamespace}}"
- path: data.LOWER_NAMESPACE
template: "{{lower .TargetNamespace}}"
- path: data.TRIMMED_PREFIX
template: "{{trimPrefix .TargetNamespace \"namespace-\"}}"
- path: data.REPLACED_DASH
template: "{{replace .TargetNamespace \"-\" \"_\"}}"
# Default value for missing field
- path: data.WITH_DEFAULT
template: "{{default \"fallback-value\" .OptionalField}}"
labels:
kubemirror.raczylo.com/enabled: "true"
example: "template-functions"
data:
placeholder: "will-be-replaced"
---
# Example 7: Strict Mode Transformation
# Transformation errors will block mirroring
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-strict
namespace: namespace-1
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "namespace-2"
kubemirror.raczylo.com/transform-strict: "true"
kubemirror.raczylo.com/transform: |
rules:
- path: data.CRITICAL_VALUE
value: "must-succeed"
labels:
kubemirror.raczylo.com/enabled: "true"
example: "strict-mode"
data:
CRITICAL_VALUE: "default"
---
# Example 8: Namespace Pattern - Environment-Specific Configuration
# Apply different GraphQL hosts based on namespace patterns
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-pattern
namespace: namespace-1
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "preprod-api,preprod-worker,prod-api,staging-api"
kubemirror.raczylo.com/transform: |
rules:
# Preprod environments get preprod GraphQL endpoint
- path: data.GRAPHQL_HOST
value: "https://preprod.example.com/v1/graphql"
namespacePattern: "preprod-*"
# Production environments get production endpoint
- path: data.GRAPHQL_HOST
value: "https://api.example.com/v1/graphql"
namespacePattern: "prod-*"
# Staging environments get staging endpoint
- path: data.GRAPHQL_HOST
value: "https://staging.example.com/v1/graphql"
namespacePattern: "*-staging"
labels:
kubemirror.raczylo.com/enabled: "true"
example: "namespace-pattern"
data:
GRAPHQL_HOST: "https://default.example.com/v1/graphql"
APP_NAME: "my-app"
---
# Example 9: Namespace Pattern with Templates
# Combine patterns with template-based transformations
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-pattern-template
namespace: namespace-1
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "preprod-api,preprod-worker,prod-api,prod-worker"
kubemirror.raczylo.com/transform: |
rules:
# Preprod: low log level
- path: data.LOG_LEVEL
value: "debug"
namespacePattern: "preprod-*"
# Production: high log level
- path: data.LOG_LEVEL
value: "error"
namespacePattern: "prod-*"
# All preprod: namespace-specific database URL
- path: data.DATABASE_URL
template: "postgres://{{.TargetNamespace}}.db.preprod.example.com:5432/mydb"
namespacePattern: "preprod-*"
# All prod: namespace-specific database URL
- path: data.DATABASE_URL
template: "postgres://{{.TargetNamespace}}.db.prod.example.com:5432/mydb"
namespacePattern: "prod-*"
labels:
kubemirror.raczylo.com/enabled: "true"
example: "pattern-with-template"
data:
LOG_LEVEL: "info"
DATABASE_URL: "postgres://localhost:5432/mydb"
---
# Example 10: Complex Multi-Pattern Rules
# Multiple patterns with different transformations
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config-multipattern
namespace: namespace-1
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "namespace-2,namespace-3,preprod-api,prod-api"
kubemirror.raczylo.com/transform: |
rules:
# Global rule (no pattern) - applies to ALL namespaces
- path: data.APP_NAME
value: "universal-app"
# Only for numbered namespaces (namespace-2, namespace-3)
- path: data.ENVIRONMENT
value: "development"
namespacePattern: "namespace-?"
# Only for preprod environments
- path: data.ENVIRONMENT
value: "preproduction"
namespacePattern: "preprod-*"
# Only for production environments
- path: data.ENVIRONMENT
value: "production"
namespacePattern: "prod-*"
# Add security label only to production
- path: metadata.labels
merge:
security-tier: "high"
compliance: "required"
namespacePattern: "prod-*"
labels:
kubemirror.raczylo.com/enabled: "true"
example: "multi-pattern"
data:
APP_NAME: "default-app"
ENVIRONMENT: "unknown"
+249
View File
@@ -0,0 +1,249 @@
# Array Indexing Transformation Examples for KubeMirror
# Demonstrates transformation rules on Kubernetes Deployments with containers, env vars, etc.
---
# Example 1: Transform Container Image with Namespace-Specific Registry
# Changes the image for the first container to use a namespace-specific registry
apiVersion: apps/v1
kind: Deployment
metadata:
name: web-app
namespace: namespace-1
labels:
kubemirror.raczylo.com/enabled: "true"
app: web-app
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "namespace-2,namespace-3"
kubemirror.raczylo.com/transform: |
rules:
# Update container image to use namespace-specific registry
- path: spec.template.spec.containers[0].image
template: "registry.{{.TargetNamespace}}.example.com/web-app:v1.0.0"
spec:
replicas: 3
selector:
matchLabels:
app: web-app
template:
metadata:
labels:
app: web-app
spec:
containers:
- name: web
image: web-app:latest
ports:
- containerPort: 8080
---
# Example 2: Transform Environment Variables
# Changes specific environment variables in containers
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-service
namespace: namespace-1
labels:
kubemirror.raczylo.com/enabled: "true"
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "namespace-2,namespace-3,namespace-4"
kubemirror.raczylo.com/transform: |
rules:
# Set LOG_LEVEL env var to error for production
- path: spec.template.spec.containers[0].env[0].value
value: "error"
# Set DATABASE_URL to namespace-specific database
- path: spec.template.spec.containers[0].env[1].value
template: "postgres://{{.TargetNamespace}}-db.postgres.svc.cluster.local:5432/api"
# Set API_KEY_PREFIX with namespace identifier
- path: spec.template.spec.containers[0].env[2].value
template: "{{upper (replace .TargetNamespace \"-\" \"_\")}}"
spec:
replicas: 2
selector:
matchLabels:
app: api-service
template:
metadata:
labels:
app: api-service
spec:
containers:
- name: api
image: api-service:v2.0.0
env:
- name: LOG_LEVEL
value: "debug"
- name: DATABASE_URL
value: "postgres://localhost:5432/api"
- name: API_KEY_PREFIX
value: "DEV"
- name: SERVICE_NAME
value: "api-service"
---
# Example 3: Transform Multiple Containers
# Handles deployments with multiple containers (app + sidecar)
apiVersion: apps/v1
kind: Deployment
metadata:
name: app-with-sidecar
namespace: namespace-1
labels:
kubemirror.raczylo.com/enabled: "true"
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "namespace-2"
kubemirror.raczylo.com/transform: |
rules:
# Main application container - update image
- path: spec.template.spec.containers[0].image
template: "{{.TargetNamespace}}.registry.example.com/app:v1"
# Main application container - set environment
- path: spec.template.spec.containers[0].env[0].value
template: "{{.TargetNamespace}}"
# Sidecar container - update image
- path: spec.template.spec.containers[1].image
value: "logging-sidecar:stable"
# Sidecar container - configure log destination
- path: spec.template.spec.containers[1].env[0].value
template: "https://logs.{{.TargetNamespace}}.example.com/ingest"
spec:
replicas: 1
selector:
matchLabels:
app: app-with-sidecar
template:
metadata:
labels:
app: app-with-sidecar
spec:
containers:
- name: app
image: app:latest
env:
- name: ENVIRONMENT
value: "development"
- name: log-collector
image: logging-sidecar:latest
env:
- name: LOG_ENDPOINT
value: "https://logs.dev.example.com/ingest"
---
# Example 4: Transform Volume Mounts and ConfigMap References
# Updates volume configurations for namespace-specific resources
apiVersion: apps/v1
kind: Deployment
metadata:
name: config-consumer
namespace: namespace-1
labels:
kubemirror.raczylo.com/enabled: "true"
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "namespace-2,namespace-3"
kubemirror.raczylo.com/transform: |
rules:
# Update ConfigMap name reference in volume
- path: spec.template.spec.volumes[0].configMap.name
template: "{{.TargetNamespace}}-config"
# Update Secret name reference in volume
- path: spec.template.spec.volumes[1].secret.secretName
template: "{{.TargetNamespace}}-credentials"
spec:
replicas: 1
selector:
matchLabels:
app: config-consumer
template:
metadata:
labels:
app: config-consumer
spec:
containers:
- name: app
image: app:v1
volumeMounts:
- name: config-volume
mountPath: /etc/config
- name: secret-volume
mountPath: /etc/secrets
volumes:
- name: config-volume
configMap:
name: app-config
- name: secret-volume
secret:
secretName: app-credentials
---
# Example 5: Complex Nested Array Transformations
# Demonstrates deeply nested path access in complex structures
apiVersion: apps/v1
kind: Deployment
metadata:
name: complex-app
namespace: namespace-1
labels:
kubemirror.raczylo.com/enabled: "true"
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "namespace-2"
kubemirror.raczylo.com/transform: |
rules:
# Container image
- path: spec.template.spec.containers[0].image
template: "{{.TargetNamespace}}.registry.io/app:v1"
# Nested env var value (REDIS_HOST)
- path: spec.template.spec.containers[0].env[1].value
template: "redis.{{.TargetNamespace}}.svc.cluster.local"
# Resource limits
- path: spec.template.spec.containers[0].resources.limits.memory
value: "2Gi"
# Init container image
- path: spec.template.spec.initContainers[0].image
value: "init-db:stable"
# Init container env var
- path: spec.template.spec.initContainers[0].env[0].value
template: "{{.TargetNamespace}}-database"
spec:
replicas: 2
selector:
matchLabels:
app: complex-app
template:
metadata:
labels:
app: complex-app
spec:
initContainers:
- name: init-db
image: init-db:latest
env:
- name: DB_NAME
value: "default-database"
containers:
- name: app
image: app:dev
env:
- name: APP_NAME
value: "complex-app"
- name: REDIS_HOST
value: "localhost"
resources:
limits:
memory: "1Gi"
cpu: "1000m"
+142
View File
@@ -0,0 +1,142 @@
# Secret Transformation Examples for KubeMirror
# Demonstrates transformation rules applied to Kubernetes Secrets
---
# Example 1: Environment-Specific Database Credentials
# Creates namespace-specific database connection strings
apiVersion: v1
kind: Secret
metadata:
name: database-credentials
namespace: namespace-1
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "namespace-2,namespace-3"
kubemirror.raczylo.com/transform: |
rules:
# Create namespace-specific database host
- path: data.DB_HOST
template: "{{.TargetNamespace}}.postgres.svc.cluster.local"
# Create namespace-specific database name
- path: data.DB_NAME
template: "app_{{replace .TargetNamespace \"-\" \"_\"}}"
labels:
kubemirror.raczylo.com/enabled: "true"
example: "secret-template-transform"
type: Opaque
stringData:
DB_HOST: "localhost"
DB_NAME: "app_dev"
DB_USER: "appuser"
DB_PASSWORD: "defaultpass"
---
# Example 2: Remove Admin Credentials from Non-Admin Namespaces
# Deletes sensitive admin fields when mirroring to production
apiVersion: v1
kind: Secret
metadata:
name: app-credentials
namespace: namespace-1
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "namespace-2,namespace-3"
kubemirror.raczylo.com/transform: |
rules:
# Remove admin credentials
- path: data.ADMIN_USERNAME
delete: true
- path: data.ADMIN_PASSWORD
delete: true
- path: data.ROOT_TOKEN
delete: true
labels:
kubemirror.raczylo.com/enabled: "true"
example: "secret-delete-transform"
type: Opaque
stringData:
APP_KEY: "app-key-12345"
ADMIN_USERNAME: "admin"
ADMIN_PASSWORD: "super-secret"
ROOT_TOKEN: "root-token-xyz"
---
# Example 3: API Key with Namespace-Specific Prefixes
# Adds namespace identification to API keys
apiVersion: v1
kind: Secret
metadata:
name: api-keys
namespace: namespace-1
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "namespace-2,namespace-3,namespace-4"
kubemirror.raczylo.com/transform: |
rules:
# Add namespace prefix to API key for tracking
- path: data.API_KEY_PREFIX
template: "{{upper (replace .TargetNamespace \"-\" \"_\")}}"
# Set environment-specific API endpoint
- path: data.API_ENDPOINT
template: "https://api.{{.TargetNamespace}}.example.com/v1"
labels:
kubemirror.raczylo.com/enabled: "true"
example: "secret-api-transform"
type: Opaque
stringData:
API_KEY_PREFIX: "DEV"
API_KEY: "sk-1234567890"
API_ENDPOINT: "https://api.dev.example.com/v1"
---
# Example 4: Complex Multi-Rule Secret Transformation
# Combines multiple transformation types for comprehensive secret management
apiVersion: v1
kind: Secret
metadata:
name: app-secrets-complex
namespace: namespace-1
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "namespace-2,namespace-3"
kubemirror.raczylo.com/transform: |
rules:
# Set production-grade encryption key
- path: data.ENCRYPTION_STRENGTH
value: "AES-256"
# Create namespace-specific service URLs
- path: data.SERVICE_URL
template: "https://{{.TargetNamespace}}.services.example.com"
# Create namespace-based Redis host
- path: data.REDIS_HOST
template: "redis.{{.TargetNamespace}}.svc.cluster.local"
# Set cache key prefix based on namespace
- path: data.CACHE_PREFIX
template: "{{replace .TargetNamespace \"-\" \":\"}}:"
# Remove development-only secrets
- path: data.DEV_OAUTH_SECRET
delete: true
- path: data.LOCAL_SIGNING_KEY
delete: true
labels:
kubemirror.raczylo.com/enabled: "true"
app: "complex-app"
example: "secret-complex-transform"
type: Opaque
stringData:
ENCRYPTION_STRENGTH: "AES-128"
SERVICE_URL: "https://localhost:8080"
REDIS_HOST: "localhost"
CACHE_PREFIX: "dev:"
APP_SECRET: "secret-12345"
DEV_OAUTH_SECRET: "dev-oauth-xyz"
LOCAL_SIGNING_KEY: "local-key-abc"
+135 -5
View File
@@ -13,10 +13,12 @@ import (
"github.com/lukaszraczylo/kubemirror/pkg/constants"
"github.com/lukaszraczylo/kubemirror/pkg/hash"
"github.com/lukaszraczylo/kubemirror/pkg/transformer"
)
// CreateMirror creates a mirror resource in the target namespace.
// It copies the source resource's spec/data and adds ownership annotations.
// If transformation rules are present, they are applied to the mirror.
func CreateMirror(source runtime.Object, targetNamespace string) (runtime.Object, error) {
// Compute content hash of source
sourceHash, err := hash.ComputeContentHash(source)
@@ -24,16 +26,29 @@ func CreateMirror(source runtime.Object, targetNamespace string) (runtime.Object
return nil, fmt.Errorf("failed to compute source hash: %w", err)
}
// Handle typed resources
// Create the mirror based on type
var mirror runtime.Object
switch src := source.(type) {
case *corev1.Secret:
return createSecretMirror(src, targetNamespace, sourceHash)
mirror, err = createSecretMirror(src, targetNamespace, sourceHash)
case *corev1.ConfigMap:
return createConfigMapMirror(src, targetNamespace, sourceHash)
mirror, err = createConfigMapMirror(src, targetNamespace, sourceHash)
default:
// For unstructured/CRD resources
return createUnstructuredMirror(source, targetNamespace, sourceHash)
mirror, err = createUnstructuredMirror(source, targetNamespace, sourceHash)
}
if err != nil {
return nil, err
}
// Apply transformations if rules are present
mirror, err = applyTransformations(source, mirror, targetNamespace)
if err != nil {
return nil, fmt.Errorf("transformation failed: %w", err)
}
return mirror, nil
}
// createSecretMirror creates a mirror of a Secret.
@@ -165,6 +180,7 @@ func buildMirrorAnnotations(source runtime.Object, sourceHash string) map[string
}
// UpdateMirror updates an existing mirror with new source content.
// It also applies transformations if transformation rules are present in the source.
func UpdateMirror(mirror, source runtime.Object) error {
// Compute new source hash
sourceHash, err := hash.ComputeContentHash(source)
@@ -186,12 +202,77 @@ func UpdateMirror(mirror, source runtime.Object) error {
updateMirrorAnnotations(m, source, sourceHash)
default:
// Unstructured
return updateUnstructuredMirror(mirror, source, sourceHash)
if err := updateUnstructuredMirror(mirror, source, sourceHash); err != nil {
return err
}
}
// Apply transformations after updating data (only if transformation rules exist)
mirrorObj, _ := mirror.(metav1.Object)
targetNamespace := mirrorObj.GetNamespace()
transformed, err := applyTransformations(source, mirror, targetNamespace)
if err != nil {
return fmt.Errorf("transformation failed: %w", err)
}
// Copy transformed data back to mirror if transformation was applied
// Transformer returns unstructured when transformations are applied, original type otherwise
if transformedU, ok := transformed.(*unstructured.Unstructured); ok {
// Transformation was applied, copy data back to typed mirror
switch m := mirror.(type) {
case *corev1.Secret:
if data, found, _ := unstructured.NestedMap(transformedU.Object, "data"); found {
m.Data = convertToByteMap(data)
}
// Copy potentially transformed labels and annotations
m.SetLabels(transformedU.GetLabels())
m.SetAnnotations(transformedU.GetAnnotations())
case *corev1.ConfigMap:
if data, found, _ := unstructured.NestedMap(transformedU.Object, "data"); found {
m.Data = convertToStringMap(data)
}
if binData, found, _ := unstructured.NestedMap(transformedU.Object, "binaryData"); found {
m.BinaryData = convertToByteMap(binData)
}
// Copy potentially transformed labels and annotations
m.SetLabels(transformedU.GetLabels())
m.SetAnnotations(transformedU.GetAnnotations())
case *unstructured.Unstructured:
// For unstructured, the transformation is already applied in-place
m.Object = transformedU.Object
}
}
// If transformed is not unstructured, no transformation was applied (no rules)
// and we can just use the mirror as-is
return nil
}
// convertToStringMap converts map[string]interface{} to map[string]string.
func convertToStringMap(data map[string]interface{}) map[string]string {
result := make(map[string]string)
for k, v := range data {
if s, ok := v.(string); ok {
result[k] = s
}
}
return result
}
// convertToByteMap converts map[string]interface{} to map[string][]byte.
func convertToByteMap(data map[string]interface{}) map[string][]byte {
result := make(map[string][]byte)
for k, v := range data {
switch val := v.(type) {
case string:
result[k] = []byte(val)
case []byte:
result[k] = val
}
}
return result
}
// updateMirrorAnnotations updates the ownership annotations on a mirror.
func updateMirrorAnnotations(mirror metav1.Object, source runtime.Object, sourceHash string) {
sourceObj, _ := source.(metav1.Object)
@@ -275,3 +356,52 @@ func GetSourceReference(mirror metav1.Object) (namespace, name, uid string, foun
return namespace, name, uid, true
}
// applyTransformations applies transformation rules from the source to the mirror.
// Returns the transformed mirror, or the original mirror if no rules are present.
func applyTransformations(source, mirror runtime.Object, targetNamespace string) (runtime.Object, error) {
// Build transformation context
ctx := buildTransformContext(source, mirror, targetNamespace)
// Create transformer with default options
t := transformer.NewDefaultTransformer()
// Apply transformations (transformer handles case of no rules gracefully)
transformed, err := t.Transform(mirror, ctx)
if err != nil {
return nil, err
}
return transformed, nil
}
// buildTransformContext creates a transformation context from source and mirror metadata.
func buildTransformContext(source, mirror runtime.Object, targetNamespace string) transformer.TransformContext {
sourceObj, _ := source.(metav1.Object)
mirrorObj, _ := mirror.(metav1.Object)
ctx := transformer.TransformContext{
TargetNamespace: targetNamespace,
SourceNamespace: sourceObj.GetNamespace(),
SourceName: sourceObj.GetName(),
TargetName: mirrorObj.GetName(),
}
// Copy labels (if any)
if labels := sourceObj.GetLabels(); labels != nil {
ctx.Labels = make(map[string]string)
for k, v := range labels {
ctx.Labels[k] = v
}
}
// Copy annotations (if any)
if annotations := sourceObj.GetAnnotations(); annotations != nil {
ctx.Annotations = make(map[string]string)
for k, v := range annotations {
ctx.Annotations[k] = v
}
}
return ctx
}
+183
View File
@@ -0,0 +1,183 @@
# Transformation Rules Design
## Overview
Transformation rules allow users to modify resources during mirroring. Rules are specified in the `kubemirror.raczylo.com/transform` annotation as YAML.
## Annotation Format
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "prod-*"
kubemirror.raczylo.com/transform: |
rules:
- path: data.LOG_LEVEL
value: "error"
- path: data.API_URL
template: "https://{{.TargetNamespace}}.api.example.com"
- path: metadata.labels
merge:
environment: "production"
managed-by: "kubemirror"
- path: data.DEBUG_MODE
delete: true
```
## Rule Types
### 1. Static Value (`value`)
Set a field to a static value, replacing existing content.
```yaml
- path: data.LOG_LEVEL
value: "error"
```
### 2. Template Value (`template`)
Use Go templates with context variables.
**Available template variables:**
- `.TargetNamespace` - Target namespace name
- `.SourceNamespace` - Source namespace name
- `.SourceName` - Source resource name
- `.TargetName` - Target resource name (usually same as source)
- `.Labels` - Map of source labels
- `.Annotations` - Map of source annotations
```yaml
- path: data.API_URL
template: "https://{{.TargetNamespace}}.api.example.com"
- path: metadata.annotations.namespace-specific
template: "Mirrored from {{.SourceNamespace}}/{{.SourceName}}"
```
### 3. Map Merge (`merge`)
Merge additional key-value pairs into an existing map. If the map doesn't exist, it's created.
```yaml
- path: metadata.labels
merge:
environment: "production"
tier: "frontend"
```
### 4. Field Deletion (`delete`)
Remove a field from the resource.
```yaml
- path: data.DEBUG_MODE
delete: true
- path: metadata.annotations.internal-only
delete: true
```
## Path Syntax
Paths use dot notation to traverse the resource structure:
- `data.KEY` - Data field in ConfigMap/Secret
- `metadata.labels.LABEL_KEY` - Specific label
- `metadata.annotations.ANNOTATION_KEY` - Specific annotation
- `spec.replicas` - Spec field
- `spec.template.spec.containers[0].image` - Array indexing
## Template Functions
Custom template functions available:
- `{{ upper .TargetNamespace }}` - Uppercase
- `{{ lower .TargetNamespace }}` - Lowercase
- `{{ replace .TargetNamespace "-" "_" }}` - String replacement
- `{{ trimPrefix .TargetNamespace "prod-" }}` - Remove prefix
- `{{ trimSuffix .TargetNamespace "-app" }}` - Remove suffix
- `{{ default "fallback" .Labels.optional }}` - Default value
## Security Considerations
1. **Template Sandboxing**: Templates are executed in a sandboxed environment
2. **Path Validation**: Paths must be valid JSONPath expressions
3. **No External Access**: Templates cannot access files, network, or execute commands
4. **Resource Limits**: Maximum template execution time: 100ms
5. **Size Limits**: Maximum transformation rule size: 10KB
## Examples
### Environment-Specific Configuration
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: default
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "dev-*,staging-*,prod-*"
kubemirror.raczylo.com/transform: |
rules:
# Set log level based on namespace prefix
- path: data.LOG_LEVEL
template: |
{{- if hasPrefix .TargetNamespace "prod-" -}}
error
{{- else if hasPrefix .TargetNamespace "staging-" -}}
warn
{{- else -}}
debug
{{- end }}
# Namespace-specific API URL
- path: data.API_URL
template: "https://{{.TargetNamespace}}.api.example.com"
# Add environment label
- path: metadata.labels
merge:
environment: "{{ trimPrefix .TargetNamespace (regexFind `^[^-]+` .TargetNamespace) }}"
```
### Secret with Dynamic Values
```yaml
apiVersion: v1
kind: Secret
metadata:
name: database-config
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "app-*"
kubemirror.raczylo.com/transform: |
rules:
# Database host varies by namespace
- path: data.DB_HOST
template: "{{ .TargetNamespace }}.postgres.svc.cluster.local"
# Remove admin password in non-admin namespaces
- path: data.ADMIN_PASSWORD
delete: true
```
## Error Handling
Transformation errors are non-fatal by default:
- Invalid path: Log warning, skip transformation
- Template error: Log warning, skip transformation
- Type mismatch: Log warning, skip transformation
To make errors fatal (block mirroring):
```yaml
kubemirror.raczylo.com/transform-strict: "true"
```
## Performance
- Rules are parsed once and cached
- Template compilation is cached
- Average overhead: <1ms per mirror creation
- Maximum rules per resource: 50
+514
View File
@@ -0,0 +1,514 @@
package transformer
import (
"bytes"
"context"
"fmt"
"strings"
"text/template"
"gopkg.in/yaml.v3"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
)
const (
// AnnotationTransform is the annotation key for transformation rules
AnnotationTransform = "kubemirror.raczylo.com/transform"
// AnnotationTransformStrict enables strict mode (errors block mirroring)
AnnotationTransformStrict = "kubemirror.raczylo.com/transform-strict"
)
// Transformer applies transformation rules to Kubernetes resources.
type Transformer struct {
options TransformOptions
}
// NewTransformer creates a new transformer with the given options.
func NewTransformer(options TransformOptions) *Transformer {
return &Transformer{
options: options,
}
}
// NewDefaultTransformer creates a transformer with default options.
func NewDefaultTransformer() *Transformer {
return NewTransformer(DefaultTransformOptions())
}
// Transform applies transformation rules to a resource.
// It returns the transformed resource and any errors encountered.
func (t *Transformer) Transform(source runtime.Object, ctx TransformContext) (runtime.Object, error) {
// Convert to unstructured for easier manipulation
unstructuredObj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(source)
if err != nil {
return nil, fmt.Errorf("failed to convert to unstructured: %w", err)
}
u := &unstructured.Unstructured{Object: unstructuredObj}
// Get transformation rules from annotations
rules, err := t.parseTransformRules(u)
if err != nil {
if t.isStrictMode(u) {
return nil, fmt.Errorf("failed to parse transformation rules: %w", err)
}
// Non-strict mode: log warning and return original
return source, nil
}
if len(rules.Rules) == 0 {
// No transformation rules
return source, nil
}
// Validate rules
if err := t.validateRules(rules); err != nil {
if t.isStrictMode(u) {
return nil, fmt.Errorf("invalid transformation rules: %w", err)
}
return source, nil
}
// Apply each rule
for i, rule := range rules.Rules {
if err := t.applyRule(u, rule, ctx); err != nil {
if t.isStrictMode(u) {
return nil, fmt.Errorf("failed to apply rule %d (%s): %w", i+1, rule.Path, err)
}
// Non-strict mode: continue with next rule
continue
}
}
return u, nil
}
// parseTransformRules extracts and parses transformation rules from resource annotations.
func (t *Transformer) parseTransformRules(u *unstructured.Unstructured) (*TransformRules, error) {
annotations := u.GetAnnotations()
if annotations == nil {
return &TransformRules{}, nil
}
rulesYAML, exists := annotations[AnnotationTransform]
if !exists || rulesYAML == "" {
return &TransformRules{}, nil
}
// Check size limit
if len(rulesYAML) > t.options.MaxRuleSize {
return nil, fmt.Errorf("transformation rules exceed maximum size of %d bytes", t.options.MaxRuleSize)
}
var rules TransformRules
if err := yaml.Unmarshal([]byte(rulesYAML), &rules); err != nil {
return nil, fmt.Errorf("failed to parse YAML: %w", err)
}
return &rules, nil
}
// validateRules validates all transformation rules.
func (t *Transformer) validateRules(rules *TransformRules) error {
if len(rules.Rules) > t.options.MaxRules {
return fmt.Errorf("too many rules (%d), maximum is %d", len(rules.Rules), t.options.MaxRules)
}
for i, rule := range rules.Rules {
if err := rule.Validate(); err != nil {
return fmt.Errorf("rule %d: %w", i+1, err)
}
}
return nil
}
// applyRule applies a single transformation rule to the resource.
func (t *Transformer) applyRule(u *unstructured.Unstructured, rule Rule, ctx TransformContext) error {
// Check if rule should apply to this target namespace
if !matchesNamespacePattern(rule, ctx.TargetNamespace) {
// Rule doesn't apply to this namespace - skip silently
return nil
}
switch rule.Type() {
case RuleTypeValue:
return t.applyValueRule(u, rule, ctx)
case RuleTypeTemplate:
return t.applyTemplateRule(u, rule, ctx)
case RuleTypeMerge:
return t.applyMergeRule(u, rule, ctx)
case RuleTypeDelete:
return t.applyDeleteRule(u, rule, ctx)
default:
return fmt.Errorf("unknown rule type: %s", rule.Type())
}
}
// applyValueRule sets a field to a static value.
func (t *Transformer) applyValueRule(u *unstructured.Unstructured, rule Rule, ctx TransformContext) error {
if rule.Value == nil {
return fmt.Errorf("value rule has nil value")
}
pathParts := parsePath(rule.Path)
if len(pathParts) == 0 {
return fmt.Errorf("empty path")
}
return setNestedField(u.Object, pathParts, *rule.Value)
}
// applyTemplateRule uses Go templates to generate the value.
func (t *Transformer) applyTemplateRule(u *unstructured.Unstructured, rule Rule, ctx TransformContext) error {
if rule.Template == nil {
return fmt.Errorf("template rule has nil template")
}
// Create template with timeout
tmpl, err := template.New("transform").Funcs(templateFuncs()).Parse(*rule.Template)
if err != nil {
return fmt.Errorf("failed to parse template: %w", err)
}
// Execute template with timeout
ctxWithTimeout, cancel := context.WithTimeout(context.Background(), t.options.TemplateTimeout)
defer cancel()
resultChan := make(chan string, 1)
errChan := make(chan error, 1)
go func() {
var buf bytes.Buffer
if err := tmpl.Execute(&buf, ctx); err != nil {
errChan <- err
return
}
resultChan <- buf.String()
}()
select {
case <-ctxWithTimeout.Done():
return fmt.Errorf("template execution timeout")
case err := <-errChan:
return fmt.Errorf("template execution failed: %w", err)
case result := <-resultChan:
pathParts := parsePath(rule.Path)
return setNestedField(u.Object, pathParts, result)
}
}
// applyMergeRule merges a map into the target field.
func (t *Transformer) applyMergeRule(u *unstructured.Unstructured, rule Rule, ctx TransformContext) error {
if rule.Merge == nil {
return fmt.Errorf("merge rule has nil merge map")
}
pathParts := parsePath(rule.Path)
if len(pathParts) == 0 {
return fmt.Errorf("empty path")
}
// Get existing value (if any)
existing, found, err := unstructured.NestedMap(u.Object, pathParts...)
if err != nil {
return fmt.Errorf("failed to get existing value: %w", err)
}
// Create or merge map
merged := make(map[string]interface{})
if found {
for k, v := range existing {
merged[k] = v
}
}
// Merge new values
for k, v := range rule.Merge {
merged[k] = v
}
return unstructured.SetNestedMap(u.Object, merged, pathParts...)
}
// applyDeleteRule removes a field from the resource.
func (t *Transformer) applyDeleteRule(u *unstructured.Unstructured, rule Rule, ctx TransformContext) error {
pathParts := parsePath(rule.Path)
if len(pathParts) == 0 {
return fmt.Errorf("empty path")
}
unstructured.RemoveNestedField(u.Object, pathParts...)
return nil
}
// isStrictMode checks if strict mode is enabled for this resource.
func (t *Transformer) isStrictMode(u *unstructured.Unstructured) bool {
if t.options.Strict {
return true
}
annotations := u.GetAnnotations()
if annotations == nil {
return false
}
strictValue, exists := annotations[AnnotationTransformStrict]
return exists && (strictValue == "true" || strictValue == "1")
}
// parsePath splits a dot-notation path into parts.
// Handles array indexing notation like "spec.containers[0].image".
// Returns path segments where array indexes are represented as "[N]" strings.
func parsePath(path string) []string {
if path == "" {
return nil
}
var parts []string
var current strings.Builder
inBracket := false
for i := 0; i < len(path); i++ {
ch := path[i]
switch ch {
case '.':
// End current segment if not in brackets
if !inBracket && current.Len() > 0 {
parts = append(parts, current.String())
current.Reset()
} else if !inBracket {
// Skip empty segments from consecutive dots
continue
} else {
// Inside brackets, keep the dot
current.WriteByte(ch)
}
case '[':
// Start of array index - save current segment if any
if current.Len() > 0 {
parts = append(parts, current.String())
current.Reset()
}
inBracket = true
current.WriteByte(ch)
case ']':
// End of array index
if inBracket {
current.WriteByte(ch)
parts = append(parts, current.String())
current.Reset()
inBracket = false
} else {
// Unmatched ], just include it
current.WriteByte(ch)
}
default:
current.WriteByte(ch)
}
}
// Add final segment
if current.Len() > 0 {
parts = append(parts, current.String())
}
return parts
}
// setNestedField sets a value at the given path in a nested map/array structure.
// Supports both map keys and array indexes (e.g., "containers[0]").
func setNestedField(obj map[string]interface{}, path []string, value interface{}) error {
if len(path) == 0 {
return fmt.Errorf("empty path")
}
// Navigate to the parent of the final element
var current interface{} = obj
for i := 0; i < len(path)-1; i++ {
segment := path[i]
// Check if this segment is an array index
if isArrayIndex(segment) {
index, err := parseArrayIndex(segment)
if err != nil {
return fmt.Errorf("invalid array index %s: %w", segment, err)
}
arr, ok := current.([]interface{})
if !ok {
return fmt.Errorf("path segment %s requires an array, got %T", segment, current)
}
if index < 0 || index >= len(arr) {
return fmt.Errorf("array index %d out of bounds (length %d)", index, len(arr))
}
current = arr[index]
continue
}
// Regular map key
currentMap, ok := current.(map[string]interface{})
if !ok {
return fmt.Errorf("path segment %s requires a map, got %T", segment, current)
}
next, exists := currentMap[segment]
if !exists {
// Peek ahead to see if next segment is an array index
if i+1 < len(path) && isArrayIndex(path[i+1]) {
// Create an empty array
newArr := make([]interface{}, 0)
currentMap[segment] = newArr
current = newArr
} else {
// Create intermediate map
newMap := make(map[string]interface{})
currentMap[segment] = newMap
current = newMap
}
continue
}
current = next
}
// Set the final value
finalSegment := path[len(path)-1]
if isArrayIndex(finalSegment) {
// Setting a value in an array
index, err := parseArrayIndex(finalSegment)
if err != nil {
return fmt.Errorf("invalid array index %s: %w", finalSegment, err)
}
arr, ok := current.([]interface{})
if !ok {
return fmt.Errorf("path segment %s requires an array, got %T", finalSegment, current)
}
if index < 0 || index >= len(arr) {
return fmt.Errorf("array index %d out of bounds (length %d)", index, len(arr))
}
arr[index] = value
return nil
}
// Setting a value in a map
currentMap, ok := current.(map[string]interface{})
if !ok {
return fmt.Errorf("cannot set key %s on non-map %T", finalSegment, current)
}
currentMap[finalSegment] = value
return nil
}
// isArrayIndex checks if a path segment is an array index (e.g., "[0]", "[123]").
func isArrayIndex(segment string) bool {
return len(segment) > 2 && segment[0] == '[' && segment[len(segment)-1] == ']'
}
// parseArrayIndex extracts the numeric index from an array segment like "[0]".
func parseArrayIndex(segment string) (int, error) {
if !isArrayIndex(segment) {
return 0, fmt.Errorf("not an array index: %s", segment)
}
// Extract the number between brackets
indexStr := segment[1 : len(segment)-1]
var index int
_, err := fmt.Sscanf(indexStr, "%d", &index)
if err != nil {
return 0, fmt.Errorf("invalid array index format: %s", indexStr)
}
return index, nil
}
// matchesNamespacePattern checks if a target namespace matches the rule's namespace pattern.
// If no pattern is specified, the rule applies to all namespaces.
// Supports glob patterns with * (matches any characters) and ? (matches single character).
func matchesNamespacePattern(rule Rule, targetNamespace string) bool {
// If no pattern is specified, rule applies to all namespaces
if rule.NamespacePattern == nil || *rule.NamespacePattern == "" {
return true
}
pattern := *rule.NamespacePattern
return matchGlob(pattern, targetNamespace)
}
// matchGlob performs simple glob pattern matching with support for * and ?.
// * matches zero or more characters
// ? matches exactly one character
func matchGlob(pattern, text string) bool {
// Fast path for exact match or wildcard-only pattern
if pattern == text {
return true
}
if pattern == "*" {
return true
}
return matchGlobRecursive(pattern, text, 0, 0)
}
// matchGlobRecursive implements recursive glob matching.
func matchGlobRecursive(pattern, text string, pIdx, tIdx int) bool {
pLen := len(pattern)
tLen := len(text)
// Base cases
if pIdx == pLen {
return tIdx == tLen
}
// Check for wildcard
if pattern[pIdx] == '*' {
// Try matching zero characters (skip *)
if matchGlobRecursive(pattern, text, pIdx+1, tIdx) {
return true
}
// Try matching one or more characters
if tIdx < tLen && matchGlobRecursive(pattern, text, pIdx, tIdx+1) {
return true
}
return false
}
// Check for single character wildcard or exact match
if tIdx < tLen && (pattern[pIdx] == '?' || pattern[pIdx] == text[tIdx]) {
return matchGlobRecursive(pattern, text, pIdx+1, tIdx+1)
}
return false
}
// templateFuncs returns custom template functions.
func templateFuncs() template.FuncMap {
return template.FuncMap{
"upper": strings.ToUpper,
"lower": strings.ToLower,
"trimPrefix": strings.TrimPrefix,
"trimSuffix": strings.TrimSuffix,
"replace": strings.ReplaceAll,
"hasPrefix": strings.HasPrefix,
"hasSuffix": strings.HasSuffix,
"default": func(defaultValue interface{}, value interface{}) interface{} {
if value == nil || value == "" {
return defaultValue
}
return value
},
}
}
File diff suppressed because it is too large Load Diff
+165
View File
@@ -0,0 +1,165 @@
// Package transformer provides resource transformation capabilities for kubemirror.
package transformer
import (
"fmt"
"time"
)
// TransformRules represents a collection of transformation rules.
type TransformRules struct {
Rules []Rule `yaml:"rules"`
}
// Rule represents a single transformation rule.
type Rule struct {
// Path is the JSONPath to the field to transform (e.g., "data.LOG_LEVEL", "metadata.labels.env")
Path string `yaml:"path"`
// Value sets a static value (mutually exclusive with Template, Merge, Delete)
Value *string `yaml:"value,omitempty"`
// Template uses Go templates to generate the value (mutually exclusive with Value, Merge, Delete)
Template *string `yaml:"template,omitempty"`
// Merge merges a map into the target field (mutually exclusive with Value, Template, Delete)
Merge map[string]interface{} `yaml:"merge,omitempty"`
// Delete removes the field (mutually exclusive with Value, Template, Merge)
Delete bool `yaml:"delete,omitempty"`
// NamespacePattern is an optional glob pattern that limits this rule to specific target namespaces
// Examples: "prod-*", "*-staging", "preprod-*"
// If not specified, the rule applies to all namespaces
NamespacePattern *string `yaml:"namespacePattern,omitempty"`
}
// TransformContext provides context variables for template evaluation.
type TransformContext struct {
// TargetNamespace is the namespace where the mirror is being created
TargetNamespace string
// SourceNamespace is the namespace of the source resource
SourceNamespace string
// SourceName is the name of the source resource
SourceName string
// TargetName is the name of the target resource (usually same as source)
TargetName string
// Labels is a copy of the source resource's labels
Labels map[string]string
// Annotations is a copy of the source resource's annotations
Annotations map[string]string
}
// TransformOptions configures the transformation behavior.
type TransformOptions struct {
// Strict mode causes transformation errors to be fatal (blocks mirroring)
Strict bool
// MaxRules limits the number of transformation rules per resource
MaxRules int
// MaxRuleSize limits the size of each rule in bytes
MaxRuleSize int
// TemplateTimeout limits template execution time
TemplateTimeout time.Duration
}
// DefaultTransformOptions returns default transformation options.
func DefaultTransformOptions() TransformOptions {
return TransformOptions{
Strict: false,
MaxRules: 50,
MaxRuleSize: 10 * 1024, // 10KB
TemplateTimeout: 100 * time.Millisecond,
}
}
// Validate checks if the rule is valid.
func (r *Rule) Validate() error {
if r.Path == "" {
return fmt.Errorf("rule path cannot be empty")
}
// Count how many actions are set
actionCount := 0
if r.Value != nil {
actionCount++
}
if r.Template != nil {
actionCount++
}
if r.Merge != nil {
actionCount++
}
if r.Delete {
actionCount++
}
if actionCount == 0 {
return fmt.Errorf("rule must specify one of: value, template, merge, or delete")
}
if actionCount > 1 {
return fmt.Errorf("rule cannot specify multiple actions (value, template, merge, delete are mutually exclusive)")
}
return nil
}
// Type returns the type of transformation this rule performs.
func (r *Rule) Type() RuleType {
switch {
case r.Value != nil:
return RuleTypeValue
case r.Template != nil:
return RuleTypeTemplate
case r.Merge != nil:
return RuleTypeMerge
case r.Delete:
return RuleTypeDelete
default:
return RuleTypeUnknown
}
}
// RuleType represents the type of transformation.
type RuleType int
const (
// RuleTypeUnknown represents an unknown or invalid rule type
RuleTypeUnknown RuleType = iota
// RuleTypeValue sets a static value
RuleTypeValue
// RuleTypeTemplate uses Go templates to generate a value
RuleTypeTemplate
// RuleTypeMerge merges a map into the target field
RuleTypeMerge
// RuleTypeDelete removes a field
RuleTypeDelete
)
// String returns the string representation of the rule type.
func (rt RuleType) String() string {
switch rt {
case RuleTypeValue:
return "value"
case RuleTypeTemplate:
return "template"
case RuleTypeMerge:
return "merge"
case RuleTypeDelete:
return "delete"
default:
return "unknown"
}
}
+224
View File
@@ -0,0 +1,224 @@
package transformer
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestRule_Validate(t *testing.T) {
tests := []struct {
name string
rule Rule
wantErr bool
errMsg string
}{
// Good cases
{
name: "valid value rule",
rule: Rule{
Path: "data.KEY",
Value: stringPtr("value"),
},
wantErr: false,
},
{
name: "valid template rule",
rule: Rule{
Path: "data.KEY",
Template: stringPtr("{{.TargetNamespace}}"),
},
wantErr: false,
},
{
name: "valid merge rule",
rule: Rule{
Path: "metadata.labels",
Merge: map[string]interface{}{
"key": "value",
},
},
wantErr: false,
},
{
name: "valid delete rule",
rule: Rule{
Path: "data.KEY",
Delete: true,
},
wantErr: false,
},
// Bad cases
{
name: "empty path",
rule: Rule{
Value: stringPtr("value"),
},
wantErr: true,
errMsg: "path cannot be empty",
},
{
name: "no action specified",
rule: Rule{
Path: "data.KEY",
},
wantErr: true,
errMsg: "must specify one of",
},
{
name: "multiple actions - value and template",
rule: Rule{
Path: "data.KEY",
Value: stringPtr("value"),
Template: stringPtr("{{.TargetNamespace}}"),
},
wantErr: true,
errMsg: "cannot specify multiple actions",
},
{
name: "multiple actions - value and merge",
rule: Rule{
Path: "data.KEY",
Value: stringPtr("value"),
Merge: map[string]interface{}{"key": "value"},
},
wantErr: true,
errMsg: "cannot specify multiple actions",
},
{
name: "multiple actions - template and delete",
rule: Rule{
Path: "data.KEY",
Template: stringPtr("{{.TargetNamespace}}"),
Delete: true,
},
wantErr: true,
errMsg: "cannot specify multiple actions",
},
// Edge cases
{
name: "path with special characters",
rule: Rule{
Path: "data.my-key.sub-key",
Value: stringPtr("value"),
},
wantErr: false,
},
{
name: "merge with empty map",
rule: Rule{
Path: "metadata.labels",
Merge: map[string]interface{}{},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.rule.Validate()
if tt.wantErr {
assert.Error(t, err)
if tt.errMsg != "" {
assert.Contains(t, err.Error(), tt.errMsg)
}
} else {
assert.NoError(t, err)
}
})
}
}
func TestRule_Type(t *testing.T) {
tests := []struct {
name string
rule Rule
wantType RuleType
}{
{
name: "value rule",
rule: Rule{
Path: "data.KEY",
Value: stringPtr("value"),
},
wantType: RuleTypeValue,
},
{
name: "template rule",
rule: Rule{
Path: "data.KEY",
Template: stringPtr("{{.TargetNamespace}}"),
},
wantType: RuleTypeTemplate,
},
{
name: "merge rule",
rule: Rule{
Path: "metadata.labels",
Merge: map[string]interface{}{"key": "value"},
},
wantType: RuleTypeMerge,
},
{
name: "delete rule",
rule: Rule{
Path: "data.KEY",
Delete: true,
},
wantType: RuleTypeDelete,
},
{
name: "unknown rule (no action)",
rule: Rule{
Path: "data.KEY",
},
wantType: RuleTypeUnknown,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotType := tt.rule.Type()
assert.Equal(t, tt.wantType, gotType)
})
}
}
func TestRuleType_String(t *testing.T) {
tests := []struct {
name string
ruleType RuleType
want string
}{
{name: "value", ruleType: RuleTypeValue, want: "value"},
{name: "template", ruleType: RuleTypeTemplate, want: "template"},
{name: "merge", ruleType: RuleTypeMerge, want: "merge"},
{name: "delete", ruleType: RuleTypeDelete, want: "delete"},
{name: "unknown", ruleType: RuleTypeUnknown, want: "unknown"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := tt.ruleType.String()
assert.Equal(t, tt.want, got)
})
}
}
func TestDefaultTransformOptions(t *testing.T) {
opts := DefaultTransformOptions()
assert.False(t, opts.Strict, "default should not be strict mode")
assert.Equal(t, 50, opts.MaxRules, "default max rules should be 50")
assert.Equal(t, 10*1024, opts.MaxRuleSize, "default max rule size should be 10KB")
assert.Equal(t, 100*time.Millisecond, opts.TemplateTimeout, "default timeout should be 100ms")
}
// stringPtr is a helper to create string pointers
func stringPtr(s string) *string {
return &s
}
+15
View File
@@ -0,0 +1,15 @@
version: 1
force:
existing: true
strict: false
minor: 1
wording:
patch:
- update
- initial
- fix
minor:
- improve
- release
major:
- breaking