mirror of
https://github.com/lukaszraczylo/kubemirror.git
synced 2026-06-05 22:43:51 +00:00
Preparation for release.
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -19,7 +19,6 @@ go.work
|
||||
# Build output
|
||||
bin/
|
||||
dist/
|
||||
kubemirror
|
||||
/kubemirror
|
||||
|
||||
# IDE
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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: ""
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
kubemirror.raczylo.com
|
||||
+813
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -0,0 +1,15 @@
|
||||
version: 1
|
||||
force:
|
||||
existing: true
|
||||
strict: false
|
||||
minor: 1
|
||||
wording:
|
||||
patch:
|
||||
- update
|
||||
- initial
|
||||
- fix
|
||||
minor:
|
||||
- improve
|
||||
- release
|
||||
major:
|
||||
- breaking
|
||||
Reference in New Issue
Block a user