mirror of
https://github.com/lukaszraczylo/kubemirror.git
synced 2026-07-08 11:14:37 +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 }}
|
RELEASE_VERSION: ${{ steps.version.outputs.version }}
|
||||||
run: |
|
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
|
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
|
# Build output
|
||||||
bin/
|
bin/
|
||||||
dist/
|
dist/
|
||||||
kubemirror
|
|
||||||
/kubemirror
|
/kubemirror
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
|
|||||||
@@ -1,19 +1,88 @@
|
|||||||
# KubeMirror
|
# 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
|
## Features
|
||||||
|
|
||||||
- **Universal Resource Support**: Mirror any Kubernetes resource type - Secrets, ConfigMaps, Ingresses, Services, CRDs, and more
|
| Category | Feature |
|
||||||
- **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
|
| **Resources** | Mirror any Kubernetes resource type - Secrets, ConfigMaps, Ingresses, Services, CRDs, and more |
|
||||||
- **Content Change Detection**: Multi-layer strategy (generation field + content hash) to avoid unnecessary syncs
|
| **Resources** | Auto-discovery of all mirrorable resources with periodic refresh |
|
||||||
- **API-Friendly**: Cluster-scoped watches with server-side filtering reduce API server load by 90%+
|
| **Resources** | Safety deny list prevents mirroring dangerous resources (Pods, Events, Nodes) |
|
||||||
- **Production-Ready**: Leader election, health checks, metrics, graceful shutdown
|
| **Targeting** | Mirror to specific namespaces, pattern-matched namespaces (`app-*`), or all labeled namespaces |
|
||||||
- **Drift Detection**: Automatically fixes manually modified target resources
|
| **Targeting** | Configurable maximum targets per source (default: 100) |
|
||||||
- **Pattern Matching**: Support glob patterns like `app-*`, `prod-*`
|
| **Targeting** | Namespace opt-in required for "all-labeled" mirrors |
|
||||||
- **Safety Limits**: Configurable maximum targets, namespace opt-in for "all" mirrors
|
| **Sync** | Multi-layer change detection: generation field + SHA256 content hash |
|
||||||
- **Finalizer-based Cleanup**: Ensures all mirrors are deleted when source is removed
|
| **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
|
## Quick Start
|
||||||
|
|
||||||
@@ -21,6 +90,7 @@ A Kubernetes controller for automatically mirroring any resource type (Secrets,
|
|||||||
|
|
||||||
- Kubernetes 1.28+
|
- Kubernetes 1.28+
|
||||||
- kubectl configured
|
- kubectl configured
|
||||||
|
- Helm 3.x (for Helm installation)
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
@@ -36,21 +106,49 @@ helm install kubemirror lukaszraczylo/kubemirror \
|
|||||||
--namespace kubemirror-system \
|
--namespace kubemirror-system \
|
||||||
--create-namespace
|
--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 \
|
helm install kubemirror lukaszraczylo/kubemirror \
|
||||||
--namespace kubemirror-system \
|
--namespace kubemirror-system \
|
||||||
--create-namespace \
|
--create-namespace \
|
||||||
--set controller.maxTargets=200 \
|
--set controller.maxTargets=200 \
|
||||||
--set controller.workerThreads=10
|
--set controller.workerThreads=10 \
|
||||||
|
--set controller.rateLimitQPS=100
|
||||||
# Verify installation
|
|
||||||
helm status kubemirror -n kubemirror-system
|
|
||||||
kubectl -n kubemirror-system get pods
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**Development:** To test the local chart during development:
|
**Development:**
|
||||||
```bash
|
```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
|
#### Using kubectl
|
||||||
@@ -70,27 +168,28 @@ kubectl -n kubemirror-system get pods
|
|||||||
kubectl -n kubemirror-system logs -l app.kubernetes.io/name=kubemirror
|
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
|
```yaml
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Secret
|
kind: Secret
|
||||||
metadata:
|
metadata:
|
||||||
name: my-secret
|
name: database-credentials
|
||||||
namespace: default
|
namespace: default
|
||||||
labels:
|
labels:
|
||||||
kubemirror.raczylo.com/enabled: "true" # Required for filtering
|
kubemirror.raczylo.com/enabled: "true" # Required for server-side filtering
|
||||||
annotations:
|
annotations:
|
||||||
kubemirror.raczylo.com/sync: "true" # Enable mirroring
|
kubemirror.raczylo.com/sync: "true" # Enable mirroring
|
||||||
kubemirror.raczylo.com/target-namespaces: "app1,app2,app3"
|
kubemirror.raczylo.com/target-namespaces: "app1,app2,app3"
|
||||||
type: Opaque
|
type: Opaque
|
||||||
data:
|
data:
|
||||||
|
username: YWRtaW4=
|
||||||
password: cGFzc3dvcmQ=
|
password: cGFzc3dvcmQ=
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Mirror to pattern-matched namespaces
|
### Mirror to Pattern-Matched Namespaces
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
@@ -104,113 +203,305 @@ metadata:
|
|||||||
kubemirror.raczylo.com/sync: "true"
|
kubemirror.raczylo.com/sync: "true"
|
||||||
kubemirror.raczylo.com/target-namespaces: "app-*,prod-*"
|
kubemirror.raczylo.com/target-namespaces: "app-*,prod-*"
|
||||||
data:
|
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
|
```yaml
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Secret
|
kind: Secret
|
||||||
metadata:
|
metadata:
|
||||||
name: shared-tls
|
name: shared-tls-cert
|
||||||
namespace: default
|
namespace: default
|
||||||
labels:
|
labels:
|
||||||
kubemirror.raczylo.com/enabled: "true"
|
kubemirror.raczylo.com/enabled: "true"
|
||||||
annotations:
|
annotations:
|
||||||
kubemirror.raczylo.com/sync: "true"
|
kubemirror.raczylo.com/sync: "true"
|
||||||
kubemirror.raczylo.com/target-namespaces: "all-labeled"
|
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
|
```yaml
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Namespace
|
kind: Namespace
|
||||||
metadata:
|
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:
|
labels:
|
||||||
kubemirror.raczylo.com/allow-mirrors: "true"
|
kubemirror.raczylo.com/allow-mirrors: "true"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture
|
### Mirror Custom Resources (CRDs)
|
||||||
|
|
||||||
- **Discovery Manager**: Auto-discovers all mirrorable resource types with periodic refresh
|
KubeMirror works with any custom resource:
|
||||||
- **Source Reconciler**: Watches labeled resources, creates/updates mirrors
|
|
||||||
- **Target Reconciler**: Watches mirrored resources, detects drift and orphans
|
```yaml
|
||||||
- **Namespace Reconciler**: Watches namespace creation, auto-creates mirrors for patterns
|
apiVersion: traefik.io/v1alpha1
|
||||||
- **Content Hash**: SHA256 of actual content (excludes Kubernetes metadata)
|
kind: Middleware
|
||||||
- **Field Indexing**: O(1) lookups for reverse references (target → source)
|
metadata:
|
||||||
- **Safety Filtering**: Deny list prevents mirroring dangerous resources (Pods, Events, etc.)
|
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
|
## Configuration
|
||||||
|
|
||||||
### Helm Chart Values
|
### Helm Chart Values
|
||||||
|
|
||||||
Key configuration options in `values.yaml`:
|
Complete configuration reference:
|
||||||
|
|
||||||
```yaml
|
| Parameter | Description | Default | Example |
|
||||||
controller:
|
|-----------|-------------|---------|---------|
|
||||||
# Resource Discovery
|
| **Resource Discovery** | | | |
|
||||||
resourceTypes: [] # Explicit list (e.g., ["Secret.v1", "ConfigMap.v1"])
|
| `controller.resourceTypes` | Explicit resource type list (empty = auto-discover all) | `[]` | `["Secret.v1", "ConfigMap.v1", "Ingress.v1.networking.k8s.io"]` |
|
||||||
# If empty, auto-discovers all mirrorable resources
|
| `controller.discoveryInterval` | Rediscovery interval for auto-discovery mode | `5m` | `10m`, `1h` |
|
||||||
discoveryInterval: "5m" # How often to rediscover resources (auto-discovery mode)
|
| **Performance & Limits** | | | |
|
||||||
|
| `controller.leaderElect` | Enable leader election for HA | `true` | `true`, `false` |
|
||||||
# Performance & Limits
|
| `controller.maxTargets` | Maximum mirrors per source resource | `100` | `50`, `200`, `500` |
|
||||||
leaderElect: true # Enable leader election for HA
|
| `controller.workerThreads` | Concurrent reconciliation workers | `5` | `10`, `20` |
|
||||||
maxTargets: 100 # Max mirrors per source resource
|
| `controller.rateLimitQPS` | API rate limit (queries per second) | `50.0` | `100.0`, `200.0` |
|
||||||
workerThreads: 5 # Concurrent reconciliation workers
|
| `controller.rateLimitBurst` | API burst allowance | `100` | `200`, `500` |
|
||||||
rateLimitQPS: 50.0 # API rate limit (queries per second)
|
| **Namespace Filtering** | | | |
|
||||||
rateLimitBurst: 100 # API burst allowance
|
| `controller.excludedNamespaces` | Comma-separated namespace exclusion list | `""` | `kube-system,kube-public,kube-node-lease` |
|
||||||
|
| `controller.includedNamespaces` | Comma-separated namespace inclusion list | `""` | `app-*,prod-*` |
|
||||||
# Namespace Filtering
|
| **Observability** | | | |
|
||||||
excludedNamespaces: "" # Comma-separated exclusion list
|
| `controller.metricsBindAddress` | Metrics endpoint address | `:8080` | `:9090` |
|
||||||
includedNamespaces: "" # Comma-separated inclusion list
|
| `controller.healthProbeBindAddress` | Health probe endpoint address | `:8081` | `:8082` |
|
||||||
|
| **Resources** | | | |
|
||||||
resources:
|
| `resources.limits.cpu` | CPU limit | `500m` | `1000m`, `2000m` |
|
||||||
limits:
|
| `resources.limits.memory` | Memory limit | `512Mi` | `256Mi`, `1Gi` |
|
||||||
cpu: 500m
|
| `resources.requests.cpu` | CPU request | `100m` | `200m`, `500m` |
|
||||||
memory: 512Mi
|
| `resources.requests.memory` | Memory request | `128Mi` | `64Mi`, `256Mi` |
|
||||||
requests:
|
|
||||||
cpu: 100m
|
|
||||||
memory: 128Mi
|
|
||||||
```
|
|
||||||
|
|
||||||
### Command-line Flags
|
### Command-line Flags
|
||||||
|
|
||||||
Key flags when running the binary directly:
|
When running the binary directly:
|
||||||
|
|
||||||
**Resource Discovery:**
|
**Resource Discovery:**
|
||||||
- `--resource-types`: Comma-separated list of resource types (e.g., `Secret.v1,ConfigMap.v1,Ingress.v1.networking.k8s.io`)
|
- `--resource-types string` - Comma-separated list (e.g., `Secret.v1,ConfigMap.v1,Ingress.v1.networking.k8s.io`)
|
||||||
- If empty, auto-discovers all mirrorable resources
|
- `--discovery-interval duration` - Rediscovery interval (default: 5m)
|
||||||
- `--discovery-interval`: Rediscovery interval for auto-discovery mode (default: 5m)
|
|
||||||
|
|
||||||
**Performance & Limits:**
|
**Performance & Limits:**
|
||||||
- `--leader-elect`: Enable leader election (default: true)
|
- `--leader-elect` - Enable leader election (default: true)
|
||||||
- `--max-targets`: Limit mirrors per source (default: 100)
|
- `--max-targets int` - Max mirrors per source (default: 100)
|
||||||
- `--worker-threads`: Concurrent workers (default: 5)
|
- `--worker-threads int` - Concurrent workers (default: 5)
|
||||||
- `--rate-limit-qps`: API rate limit (default: 50.0)
|
- `--rate-limit-qps float32` - API rate limit (default: 50.0)
|
||||||
- `--rate-limit-burst`: API burst limit (default: 100)
|
- `--rate-limit-burst int` - API burst limit (default: 100)
|
||||||
|
|
||||||
**Namespace Filtering:**
|
**Namespace Filtering:**
|
||||||
- `--excluded-namespaces`: Comma-separated namespace exclusion list
|
- `--excluded-namespaces string` - Comma-separated exclusion list
|
||||||
- `--included-namespaces`: Comma-separated namespace inclusion 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):**
|
**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)
|
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
|
3. Excludes dangerous resources using a comprehensive deny list
|
||||||
4. Periodically rediscovers resources (default: every 5 minutes) to detect new CRDs or resource types
|
4. Periodically rediscovers (default: every 5 minutes) to detect new CRDs
|
||||||
|
|
||||||
**Explicit Mode:**
|
**Explicit Mode:**
|
||||||
Specify exactly which resources to mirror:
|
|
||||||
|
Specify exact resources to mirror:
|
||||||
```yaml
|
```yaml
|
||||||
controller:
|
controller:
|
||||||
resourceTypes:
|
resourceTypes:
|
||||||
@@ -220,43 +511,287 @@ controller:
|
|||||||
- "Middleware.v1alpha1.traefik.io"
|
- "Middleware.v1alpha1.traefik.io"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Safety Features
|
**Safety Features:**
|
||||||
|
|
||||||
Auto-discovery includes built-in safety:
|
- **Deny List:** Never mirrors: Pods, Events, Nodes, Endpoints, EndpointSlice, Leases, PersistentVolumes, and other cluster-scoped or dangerous resources
|
||||||
- **Deny List**: Never mirrors Pods, Events, Nodes, Endpoints, Leases, etc.
|
- **Namespaced Only:** Only discovers namespaced resources (cluster-scoped excluded)
|
||||||
- **Namespaced Only**: Only discovers namespaced resources (cluster-scoped are excluded)
|
- **Verb Filtering:** Resources must support all CRUD operations
|
||||||
- **Verb Filtering**: Resources must support all required CRUD operations
|
- **Opt-In Required:** Resources must have `kubemirror.raczylo.com/enabled: "true"` label
|
||||||
- **Opt-In Required**: Resources must have `kubemirror.raczylo.com/enabled: "true"` label
|
|
||||||
|
|
||||||
### Monitoring Discovery
|
**Monitoring Discovery:**
|
||||||
|
|
||||||
View discovered resources in the logs:
|
|
||||||
```bash
|
```bash
|
||||||
|
# View discovered resources
|
||||||
kubectl logs -n kubemirror-system -l app.kubernetes.io/name=kubemirror | grep "resource discovery"
|
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:
|
### Components
|
||||||
- Secrets mirrored to all namespaces
|
|
||||||
- ConfigMaps mirrored to specific namespaces
|
- **Discovery Manager**: Automatically discovers all mirrorable resource types with periodic refresh
|
||||||
- Traefik Middlewares (custom resources) mirroring
|
- **Source Reconciler**: Watches labeled source resources, creates/updates mirrors across target namespaces
|
||||||
- Comprehensive testing scenarios
|
- **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
|
```bash
|
||||||
# Apply examples
|
# Deploy ServiceMonitor for Prometheus Operator
|
||||||
kubectl apply -k examples/
|
kubectl apply -f monitoring/servicemonitor.yaml
|
||||||
|
|
||||||
# Watch mirroring in action
|
# Deploy Alert Rules
|
||||||
kubectl logs -n kubemirror-system -l app.kubernetes.io/name=kubemirror -f
|
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
|
## Development
|
||||||
|
|
||||||
### Using Makefile
|
### Building
|
||||||
|
|
||||||
```bash
|
```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
|
make test
|
||||||
|
|
||||||
# Run tests with race detector
|
# Run tests with race detector
|
||||||
@@ -265,109 +800,39 @@ make test-race
|
|||||||
# Run benchmarks
|
# Run benchmarks
|
||||||
make bench
|
make bench
|
||||||
|
|
||||||
# Build binary
|
# Run specific package tests
|
||||||
make build
|
go test -v ./pkg/controller/...
|
||||||
|
|
||||||
# Run locally
|
# Run with coverage
|
||||||
make run
|
go test -cover ./...
|
||||||
|
go test -coverprofile=coverage.out ./...
|
||||||
# Build Docker image
|
go tool cover -html=coverage.out
|
||||||
make docker-build
|
|
||||||
|
|
||||||
# Run linters
|
|
||||||
make lint
|
|
||||||
|
|
||||||
# Full CI checks
|
|
||||||
make ci
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Manual Commands
|
### Releasing
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run tests
|
# Test release locally (dry run)
|
||||||
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)
|
|
||||||
make release-dry
|
make release-dry
|
||||||
|
|
||||||
# Create release (requires git tag)
|
# Create and push tag (triggers CI/CD)
|
||||||
git tag -a v0.1.0 -m "Release v0.1.0"
|
git tag -a v0.2.0 -m "Release v0.2.0: Universal resource support"
|
||||||
git push origin v0.1.0
|
git push origin v0.2.0
|
||||||
# GitHub Actions will automatically build and release
|
|
||||||
|
# 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
|
## Documentation
|
||||||
|
|
||||||
- [CLAUDE.md](CLAUDE.md) - Project specification and requirements
|
|
||||||
- [examples/](examples/) - Working examples and testing scenarios
|
- [examples/](examples/) - Working examples and testing scenarios
|
||||||
- [monitoring/](monitoring/) - Prometheus, Grafana, and alerting setup
|
- [monitoring/](monitoring/) - Prometheus metrics, Grafana dashboards, alerting setup
|
||||||
- [Helm Chart](charts/kubemirror/) - Kubernetes deployment via Helm
|
- [Helm Chart Documentation](charts/kubemirror/README.md) - Kubernetes deployment via Helm
|
||||||
- [Project Repository](https://github.com/lukaszraczylo/kubemirror)
|
- [GitHub Repository](https://github.com/lukaszraczylo/kubemirror) - Source code and issue tracker
|
||||||
|
|
||||||
## License
|
## 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:
|
data:
|
||||||
config: "value"
|
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-secret.yaml
|
||||||
- source-configmap.yaml
|
- source-configmap.yaml
|
||||||
- traefik-middleware.yaml
|
- traefik-middleware.yaml
|
||||||
|
- transform-configmap.yaml
|
||||||
|
- transform-secret.yaml
|
||||||
|
- transform-deployment.yaml
|
||||||
|
|
||||||
commonLabels:
|
commonLabels:
|
||||||
managed-by: kustomize
|
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/constants"
|
||||||
"github.com/lukaszraczylo/kubemirror/pkg/hash"
|
"github.com/lukaszraczylo/kubemirror/pkg/hash"
|
||||||
|
"github.com/lukaszraczylo/kubemirror/pkg/transformer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CreateMirror creates a mirror resource in the target namespace.
|
// CreateMirror creates a mirror resource in the target namespace.
|
||||||
// It copies the source resource's spec/data and adds ownership annotations.
|
// 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) {
|
func CreateMirror(source runtime.Object, targetNamespace string) (runtime.Object, error) {
|
||||||
// Compute content hash of source
|
// Compute content hash of source
|
||||||
sourceHash, err := hash.ComputeContentHash(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)
|
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) {
|
switch src := source.(type) {
|
||||||
case *corev1.Secret:
|
case *corev1.Secret:
|
||||||
return createSecretMirror(src, targetNamespace, sourceHash)
|
mirror, err = createSecretMirror(src, targetNamespace, sourceHash)
|
||||||
case *corev1.ConfigMap:
|
case *corev1.ConfigMap:
|
||||||
return createConfigMapMirror(src, targetNamespace, sourceHash)
|
mirror, err = createConfigMapMirror(src, targetNamespace, sourceHash)
|
||||||
default:
|
default:
|
||||||
// For unstructured/CRD resources
|
// 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.
|
// 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.
|
// 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 {
|
func UpdateMirror(mirror, source runtime.Object) error {
|
||||||
// Compute new source hash
|
// Compute new source hash
|
||||||
sourceHash, err := hash.ComputeContentHash(source)
|
sourceHash, err := hash.ComputeContentHash(source)
|
||||||
@@ -186,12 +202,77 @@ func UpdateMirror(mirror, source runtime.Object) error {
|
|||||||
updateMirrorAnnotations(m, source, sourceHash)
|
updateMirrorAnnotations(m, source, sourceHash)
|
||||||
default:
|
default:
|
||||||
// Unstructured
|
// 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
|
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.
|
// updateMirrorAnnotations updates the ownership annotations on a mirror.
|
||||||
func updateMirrorAnnotations(mirror metav1.Object, source runtime.Object, sourceHash string) {
|
func updateMirrorAnnotations(mirror metav1.Object, source runtime.Object, sourceHash string) {
|
||||||
sourceObj, _ := source.(metav1.Object)
|
sourceObj, _ := source.(metav1.Object)
|
||||||
@@ -275,3 +356,52 @@ func GetSourceReference(mirror metav1.Object) (namespace, name, uid string, foun
|
|||||||
|
|
||||||
return namespace, name, uid, true
|
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