Compare commits

..
6 Commits
10 changed files with 1358 additions and 369 deletions
+61 -56
View File
@@ -1,70 +1,75 @@
name: Release
on:
workflow_dispatch:
push:
tags:
- 'v*'
workflow_dispatch:
push:
paths-ignore:
- "**.md"
- "docs/**"
- "examples/**"
branches:
- main
permissions:
id-token: write
contents: write
packages: write
id-token: write
contents: write
packages: write
jobs:
release:
uses: lukaszraczylo/shared-actions/.github/workflows/go-release.yaml@main
with:
go-version: ">=1.25"
secrets: inherit
publish-helm-chart:
needs: release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
release:
uses: lukaszraczylo/shared-actions/.github/workflows/go-release.yaml@main
with:
fetch-depth: 0
go-version: ">=1.25"
docker-enabled: true
secrets: inherit
- name: Get release version
id: version
run: |
VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0")
VERSION=${VERSION#v}
echo "version=$VERSION" >> $GITHUB_OUTPUT
publish-helm-chart:
needs: release
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Trigger helm-charts release
env:
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
RELEASE_VERSION: ${{ steps.version.outputs.version }}
run: |
gh api repos/lukaszraczylo/helm-charts/dispatches \
-f event_type=release-chart \
-f client_payload[chart_name]=kubemirror \
-f client_payload[version]="$RELEASE_VERSION" \
-f client_payload[source_repo]=lukaszraczylo/kubemirror \
-f client_payload[chart_path]=charts/kubemirror
- name: Get release version
id: version
run: |
VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0")
VERSION=${VERSION#v}
echo "version=$VERSION" >> $GITHUB_OUTPUT
publish-website:
needs: release
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Trigger helm-charts release
env:
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
RELEASE_VERSION: ${{ steps.version.outputs.version }}
run: |
gh api repos/lukaszraczylo/helm-charts/dispatches \
-f event_type=release-chart \
-f client_payload[chart_name]=kubemirror \
-f client_payload[version]="$RELEASE_VERSION" \
-f client_payload[source_repo]=lukaszraczylo/kubemirror \
-f client_payload[chart_path]=charts/kubemirror
- name: Setup Pages
uses: actions/configure-pages@v5
publish-website:
needs: release
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: './docs'
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: "./docs"
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+4 -8
View File
@@ -1,12 +1,8 @@
# Runtime stage - using distroless for minimal attack surface
# Dockerfile for GoReleaser dockers_v2
# GoReleaser organizes binaries by platform: linux/amd64/kubemirror, linux/arm64/kubemirror
FROM gcr.io/distroless/static:nonroot
ARG TARGETPLATFORM
WORKDIR /
# Copy the binary from goreleaser build
COPY kubemirror /kubemirror
# Use nonroot user (65532)
COPY ${TARGETPLATFORM}/kubemirror /kubemirror
USER 65532:65532
ENTRYPOINT ["/kubemirror"]
+111 -2
View File
@@ -17,8 +17,10 @@ Tested in production environments managing 1000+ mirrors across 200+ namespaces
- [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 Namespaces](#mirror-to-all-namespaces)
- [Mirror to All Labeled Namespaces](#mirror-to-all-labeled-namespaces)
- [Mirror Custom Resources (CRDs)](#mirror-custom-resources-crds)
- [Using with ExternalSecrets Operator](#using-with-externalsecrets-operator)
- [Configuration](#configuration)
- [Helm Chart Values](#helm-chart-values)
- [Command-line Flags](#command-line-flags)
@@ -65,9 +67,9 @@ KubeMirror solves this with:
| **Resources** | Mirror any Kubernetes resource type - Secrets, ConfigMaps, Ingresses, Services, CRDs, and more |
| **Resources** | Auto-discovery of all mirrorable resources with periodic refresh |
| **Resources** | Safety deny list prevents mirroring dangerous resources (Pods, Events, Nodes) |
| **Targeting** | Mirror to specific namespaces, pattern-matched namespaces (`app-*`), or all labeled namespaces |
| **Targeting** | Mirror to specific namespaces, patterns (`app-*`), `all` namespaces, or `all-labeled` (opt-in) |
| **Targeting** | Configurable maximum targets per source (default: 100) |
| **Targeting** | Namespace opt-in required for "all-labeled" mirrors |
| **Targeting** | `all-labeled` requires namespace opt-in via `kubemirror.raczylo.com/allow-mirrors` label |
| **Sync** | Multi-layer change detection: generation field + SHA256 content hash |
| **Sync** | Automatic drift detection and correction for manually modified mirrors |
| **Sync** | Finalizer-based cleanup ensures mirrors are deleted with source |
@@ -207,8 +209,33 @@ data:
api_url: "https://api.example.com"
```
### Mirror to All Namespaces
Use the `all` keyword to mirror to every namespace in the cluster (except the source):
**Source Resource:**
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: global-config
namespace: default
labels:
kubemirror.raczylo.com/enabled: "true"
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "all"
data:
cluster_name: "production"
region: "us-west-2"
```
> **⚠️ Use with caution:** The `all` keyword mirrors to ALL namespaces (including kube-system, kube-public, etc.) except the source namespace. Consider using `all-labeled` for safer opt-in behavior.
### Mirror to All Labeled Namespaces
Use `all-labeled` for opt-in mirroring where target namespaces must explicitly allow mirrors:
**Source Resource:**
```yaml
apiVersion: v1
@@ -265,6 +292,88 @@ spec:
- text/event-stream
```
### Using with ExternalSecrets Operator
KubeMirror works seamlessly with the [ExternalSecrets Operator](https://external-secrets.io/) to distribute secrets from external stores (like 1Password, Vault, AWS Secrets Manager) across multiple namespaces.
**Example - Distribute Docker Registry Credentials from 1Password:**
```yaml
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: 1p-docker-config
namespace: default
spec:
# Pull secrets from 1Password/Vault/etc
secretStoreRef:
kind: ClusterSecretStore
name: 1password-homecluster
target:
creationPolicy: Owner # Standard ExternalSecrets setting - KubeMirror strips ownerReferences from mirrors
deletionPolicy: Retain
name: multi-registry-secret
# Include KubeMirror annotations in the secret template
template:
metadata:
labels:
kubemirror.raczylo.com/enabled: "true"
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "all" # or specific namespaces
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: |
{
"auths": {
"ghcr.io": {
"username": "{{ .ghcrUsername | toString }}",
"auth": "{{ printf "%s:%s" .ghcrUsername .ghcrPassword | b64enc }}"
}
}
}
data:
- remoteRef:
key: DockerAuth/ghcrio_username
secretKey: ghcrUsername
- remoteRef:
key: DockerAuth/ghcrio_password
secretKey: ghcrPassword
refreshInterval: 24h
```
**How it Works:**
1. **ExternalSecrets creates the source secret** with KubeMirror labels/annotations (source can be owned by any controller)
2. **KubeMirror detects the source** via the `kubemirror.raczylo.com/enabled` label
3. **KubeMirror creates mirrors** in target namespaces with:
- Labels identifying them as KubeMirror-managed mirrors
- Annotations linking back to the source (namespace, name, UID, content hash)
- **No ownerReferences** - preventing conflicts with source controllers
4. **ExternalSecrets refreshes the source** every 24h, updating only the source secret
5. **KubeMirror detects content changes** via hash comparison and updates all mirrors
6. Each controller manages its own resources independently - no conflicts
**Verification:**
```bash
# Check source secret was created by ExternalSecrets
kubectl get secret multi-registry-secret -n default -o jsonpath='{.metadata.annotations}'
# Verify mirrors were created by KubeMirror
kubectl get secrets --all-namespaces -l kubemirror.raczylo.com/mirror=true
# Check sync status on source
kubectl get secret multi-registry-secret -n default -o jsonpath='{.metadata.annotations.kubemirror\.raczylo\.com/sync-status}'
```
See [examples/externalsecret-dockerconfig.yaml](examples/externalsecret-dockerconfig.yaml) for a complete working example.
### Transformation Rules
KubeMirror supports powerful transformation rules that modify resources during mirroring. This enables environment-specific configurations, security hardening, and dynamic value generation.
+270 -301
View File
@@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html lang="en" class="scroll-smooth">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -20,34 +20,100 @@
-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);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
}
.code-block {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
}
/* Fade-in animation */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.fade-in {
animation: fadeInUp 0.6s ease-out forwards;
opacity: 0;
}
.delay-100 { animation-delay: 0.1s; }
.delay-200 { animation-delay: 0.2s; }
.delay-300 { animation-delay: 0.3s; }
/* Scroll progress bar */
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 4px;
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
z-index: 9999;
transition: width 0.1s ease-out;
}
/* Mobile menu animation */
.mobile-menu {
transform: translateX(100%);
transition: transform 0.3s ease-in-out;
}
.mobile-menu.active {
transform: translateX(0);
}
/* Smooth hover glow */
.glow-on-hover {
position: relative;
overflow: hidden;
}
.glow-on-hover::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: 0;
height: 0;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
transform: translate(-50%, -50%);
transition: width 0.6s, height 0.6s;
}
.glow-on-hover:hover::before {
width: 300px;
height: 300px;
}
</style>
</head>
<body class="bg-gradient-to-br from-slate-50 to-blue-50">
<!-- Scroll Progress Bar -->
<div class="progress-bar" id="progressBar"></div>
<!-- Navigation -->
<nav class="bg-white/80 backdrop-blur-md shadow-lg sticky top-0 z-50 border-b border-blue-100">
<nav class="bg-white/90 backdrop-blur-lg 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">
<div class="bg-gradient-to-br from-blue-500 to-purple-600 p-2 rounded-lg group-hover:scale-110 transition-transform duration-300">
<i class="fas fa-copy text-2xl text-white"></i>
</div>
<span class="text-2xl font-bold gradient-text">KubeMirror</span>
</a>
<!-- Desktop Menu -->
<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>
@@ -55,34 +121,50 @@
<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>
<!-- Mobile Menu Button -->
<button id="mobileMenuBtn" class="md:hidden text-slate-700 hover:text-blue-600">
<i class="fas fa-bars text-2xl"></i>
</button>
</div>
</div>
</div>
</nav>
<!-- Mobile Menu -->
<div id="mobileMenu" class="mobile-menu fixed top-16 right-0 w-64 h-full bg-white shadow-2xl z-40 md:hidden">
<div class="flex flex-col p-6 space-y-4">
<a href="#problem" class="text-slate-700 hover:text-blue-600 font-medium transition-colors py-2">Problem</a>
<a href="#features" class="text-slate-700 hover:text-blue-600 font-medium transition-colors py-2">Features</a>
<a href="#examples" class="text-slate-700 hover:text-blue-600 font-medium transition-colors py-2">Examples</a>
<a href="#comparison" class="text-slate-700 hover:text-blue-600 font-medium transition-colors py-2">Compare</a>
<a href="#installation" class="text-slate-700 hover:text-blue-600 font-medium transition-colors py-2">Install</a>
</div>
</div>
<!-- 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="mb-8 inline-block fade-in">
<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">
<h1 class="text-5xl md:text-6xl font-extrabold text-slate-900 mb-6 leading-tight fade-in delay-100">
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">
<p class="text-xl md:text-2xl text-slate-600 mb-10 max-w-3xl mx-auto leading-relaxed fade-in delay-200">
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">
<div class="flex flex-col sm:flex-row justify-center gap-6 fade-in delay-300">
<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 glow-on-hover">
<i class="fas fa-download mr-2"></i>
Get Started
</a>
@@ -98,8 +180,8 @@
<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">
<h2 class="text-4xl md:text-5xl font-extrabold text-slate-900 mb-6">The Problem</h2>
<p class="text-xl md: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>
@@ -136,13 +218,13 @@
</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="bg-gradient-to-br from-green-50 to-emerald-100 border-l-4 border-green-500 p-10 rounded-xl shadow-xl hover-lift">
<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.
<h3 class="font-bold text-2xl md:text-3xl text-slate-900 mb-4">KubeMirror's Solution</h3>
<p class="text-slate-700 text-lg md:text-xl leading-relaxed">
Define your resource once in a source namespace. KubeMirror automatically copies it to target namespaces (specific list, patterns like <code class="bg-white px-3 py-1 rounded font-mono text-purple-600 font-semibold">app-*</code>, or <code class="bg-white px-3 py-1 rounded font-mono text-purple-600 font-semibold">all</code>) 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>
@@ -156,17 +238,17 @@
<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>
<h2 class="text-4xl md: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-white p-8 md: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>
<h3 class="text-2xl md: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>
@@ -184,15 +266,15 @@
</div>
<!-- Transformation Rules -->
<div class="bg-white p-10 rounded-2xl shadow-xl hover-lift border border-purple-100">
<div class="bg-white p-8 md: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>
<h3 class="text-2xl md: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">
<div class="code-block text-gray-100 p-5 rounded-xl font-mono text-xs md: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>
@@ -211,11 +293,11 @@
</div>
<!-- Automatic Sync -->
<div class="bg-white p-10 rounded-2xl shadow-xl hover-lift border border-green-100">
<div class="bg-white p-8 md: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>
<h3 class="text-2xl md: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>
@@ -233,26 +315,30 @@
</div>
<!-- Smart Targeting -->
<div class="bg-white p-10 rounded-2xl shadow-xl hover-lift border border-orange-100">
<div class="bg-white p-8 md: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>
<h3 class="text-2xl md: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>
<div class="space-y-4 text-slate-700 text-base md:text-lg">
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-3">
<code class="bg-slate-100 px-4 py-2 rounded-lg font-mono text-purple-700 font-semibold text-sm">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>
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-3">
<code class="bg-slate-100 px-4 py-2 rounded-lg font-mono text-purple-700 font-semibold text-sm">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 class="flex flex-col sm:flex-row items-start sm:items-center gap-3">
<code class="bg-slate-100 px-4 py-2 rounded-lg font-mono text-purple-700 font-semibold text-sm">all</code>
<span>All namespaces (no labels required)</span>
</div>
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-3">
<code class="bg-slate-100 px-4 py-2 rounded-lg font-mono text-purple-700 font-semibold text-sm">all-labeled</code>
<span>Only namespaces with opt-in label</span>
</div>
</div>
<div class="mt-6 p-4 bg-orange-50 rounded-lg border border-orange-200">
@@ -269,26 +355,26 @@
<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>
<h2 class="text-4xl md: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-gradient-to-br from-blue-50 to-indigo-50 p-8 md:p-10 rounded-2xl shadow-xl border border-blue-200 hover-lift">
<div class="flex flex-col sm:flex-row 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">
<h3 class="text-2xl md: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">
<div class="code-block text-gray-100 p-4 md:p-6 rounded-xl font-mono text-xs md: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>
@@ -307,20 +393,20 @@
</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-gradient-to-br from-purple-50 to-pink-50 p-8 md:p-10 rounded-2xl shadow-xl border border-purple-200 hover-lift">
<div class="flex flex-col sm:flex-row 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">
<h3 class="text-2xl md: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">
<div class="code-block text-gray-100 p-4 md:p-6 rounded-xl font-mono text-xs md: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>
@@ -337,28 +423,28 @@
<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">
<p class="text-slate-700 text-base md: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-"
<strong>Result:</strong> This ConfigMap will be automatically copied to <code class="bg-white px-2 py-1 rounded font-mono text-purple-700 text-sm">app-frontend</code>, <code class="bg-white px-2 py-1 rounded font-mono text-purple-700 text-sm">app-backend</code>, <code class="bg-white px-2 py-1 rounded font-mono text-purple-700 text-sm">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-gradient-to-br from-green-50 to-teal-50 p-8 md:p-10 rounded-2xl shadow-xl border border-green-200 hover-lift">
<div class="flex flex-col sm:flex-row 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">
<h3 class="text-2xl md: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">
<div class="code-block text-gray-100 p-4 md:p-6 rounded-xl font-mono text-xs md: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>
@@ -376,130 +462,12 @@
- 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">
<p class="text-slate-700 text-base md: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>
@@ -508,102 +476,74 @@
<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>
<h2 class="text-4xl md:text-5xl font-extrabold text-slate-900 mb-6">How KubeMirror Compares</h2>
<p class="text-xl md: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>
<th class="px-6 md:px-8 py-6 text-left font-bold text-base md:text-lg">Capability</th>
<th class="px-6 md:px-8 py-6 text-center font-bold text-base md:text-lg">KubeMirror</th>
<th class="px-6 md:px-8 py-6 text-center font-bold text-base md: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>
<td class="px-6 md:px-8 py-6">
<div class="font-semibold text-base md: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 class="px-6 md:px-8 py-6 text-center">
<div><i class="fas fa-check-circle text-green-500 text-2xl md:text-3xl"></i></div>
<div class="text-xs md: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 class="px-6 md:px-8 py-6 text-center">
<div><i class="fas fa-minus-circle text-yellow-500 text-2xl md:text-3xl"></i></div>
<div class="text-xs md: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>
<td class="px-6 md:px-8 py-6">
<div class="font-semibold text-base md: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 class="px-6 md:px-8 py-6 text-center">
<div><i class="fas fa-check-circle text-green-500 text-2xl md:text-3xl"></i></div>
<div class="text-xs md: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 class="px-6 md:px-8 py-6 text-center">
<div><i class="fas fa-times-circle text-red-500 text-2xl md:text-3xl"></i></div>
<div class="text-xs md: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>
<td class="px-6 md:px-8 py-6">
<div class="font-semibold text-base md: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 class="px-6 md:px-8 py-6 text-center">
<div><i class="fas fa-check-circle text-green-500 text-2xl md:text-3xl"></i></div>
<div class="text-xs md: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 class="px-6 md:px-8 py-6 text-center">
<div><i class="fas fa-times-circle text-red-500 text-2xl md:text-3xl"></i></div>
<div class="text-xs md: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>
<td class="px-6 md:px-8 py-6">
<div class="font-semibold text-base md: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 class="px-6 md:px-8 py-6 text-center">
<div><i class="fas fa-check-circle text-green-500 text-2xl md:text-3xl"></i></div>
<div class="text-xs md: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 class="px-6 md:px-8 py-6 text-center">
<div><i class="fas fa-check-circle text-green-500 text-2xl md:text-3xl"></i></div>
<div class="text-xs md:text-sm font-semibold text-green-700 mt-2">Recently resumed (2025)</div>
</td>
</tr>
</tbody>
@@ -611,11 +551,11 @@
</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 class="flex flex-col sm:flex-row items-start gap-6">
<i class="fas fa-info-circle text-blue-600 text-3xl md: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">
<h4 class="text-xl md:text-2xl font-bold text-slate-900 mb-4">Why We Built KubeMirror</h4>
<p class="text-slate-700 text-base md: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.
@@ -630,20 +570,20 @@
<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>
<h2 class="text-4xl md:text-5xl font-extrabold text-slate-900 mb-4">Installation</h2>
<p class="text-xl md: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="bg-gradient-to-br from-blue-50 to-indigo-50 p-8 md: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>
<h3 class="text-2xl md: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">
<div class="code-block text-gray-100 p-4 md:p-6 rounded-xl font-mono text-xs md:text-sm overflow-x-auto shadow-lg">
<pre><span class="text-green-400">helm repo add kubemirror \</span>
https://lukaszraczylo.github.io/helm-charts
@@ -655,14 +595,14 @@
</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="bg-gradient-to-br from-purple-50 to-pink-50 p-8 md: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>
<h3 class="text-2xl md: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">
<div class="code-block text-gray-100 p-4 md:p-6 rounded-xl font-mono text-xs md:text-sm overflow-x-auto shadow-lg">
<pre><span class="text-green-400">kubectl apply -k \</span>
github.com/lukaszraczylo/kubemirror/deploy
@@ -674,8 +614,8 @@
</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">
<div class="bg-gradient-to-br from-green-50 to-teal-50 p-8 md:p-12 rounded-2xl shadow-2xl border border-green-200">
<h3 class="text-3xl md: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>
@@ -684,9 +624,9 @@
<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>
<h4 class="font-bold text-xl md: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">
<div class="code-block text-gray-100 p-4 md:p-6 rounded-xl font-mono text-xs md: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>
@@ -707,15 +647,15 @@
<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>
<h4 class="font-bold text-xl md:text-2xl text-slate-900">That's it!</h4>
</div>
<p class="text-slate-700 mb-6 text-lg">
<p class="text-slate-700 mb-6 text-base md:text-lg">
KubeMirror automatically:
</p>
<ul class="text-slate-700 space-y-4 text-lg">
<ul class="text-slate-700 space-y-4 text-base md: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>
<span>Creates copies in <code class="bg-white px-2 py-1 rounded font-mono text-green-700 text-sm">app-1</code> and <code class="bg-white px-2 py-1 rounded font-mono text-green-700 text-sm">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>
@@ -732,8 +672,8 @@
</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.
<strong class="text-green-800">Required:</strong> Both the label <code class="bg-white px-2 py-1 rounded font-mono text-green-700 text-xs">kubemirror.raczylo.com/enabled</code>
and annotation <code class="bg-white px-2 py-1 rounded font-mono text-green-700 text-xs">kubemirror.raczylo.com/sync</code> are needed.
</p>
</div>
</div>
@@ -742,42 +682,6 @@
</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">
@@ -789,25 +693,90 @@
</div>
<span class="text-2xl font-bold text-white">KubeMirror</span>
</div>
<p class="text-gray-400 text-lg leading-relaxed">
<p class="text-gray-400 text-base md: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">
<h4 class="text-lg md:text-xl font-bold text-white mb-6">Links</h4>
<ul class="space-y-3 text-base md: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>
<h4 class="text-lg md:text-xl font-bold text-white mb-6">License</h4>
<p class="text-gray-400 text-base md:text-lg">MIT License</p>
<p class="text-gray-400 mt-4 text-base md:text-lg">© 2024 Lukasz Raczylo</p>
</div>
</div>
</div>
</footer>
<!-- Back to Top Button -->
<button id="backToTop" class="fixed bottom-8 right-8 bg-gradient-to-r from-blue-600 to-purple-600 text-white p-4 rounded-full shadow-2xl opacity-0 pointer-events-none transition-opacity duration-300 hover:scale-110 z-50">
<i class="fas fa-arrow-up text-xl"></i>
</button>
<script>
// Scroll progress bar
window.addEventListener('scroll', () => {
const winScroll = document.body.scrollTop || document.documentElement.scrollTop;
const height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
const scrolled = (winScroll / height) * 100;
document.getElementById('progressBar').style.width = scrolled + '%';
});
// Mobile menu toggle
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
const mobileMenu = document.getElementById('mobileMenu');
mobileMenuBtn.addEventListener('click', () => {
mobileMenu.classList.toggle('active');
});
// Close mobile menu when clicking a link
document.querySelectorAll('#mobileMenu a').forEach(link => {
link.addEventListener('click', () => {
mobileMenu.classList.remove('active');
});
});
// Close mobile menu when clicking outside
document.addEventListener('click', (e) => {
if (!mobileMenu.contains(e.target) && !mobileMenuBtn.contains(e.target)) {
mobileMenu.classList.remove('active');
}
});
// Back to top button
const backToTop = document.getElementById('backToTop');
window.addEventListener('scroll', () => {
if (window.pageYOffset > 300) {
backToTop.style.opacity = '1';
backToTop.style.pointerEvents = 'auto';
} else {
backToTop.style.opacity = '0';
backToTop.style.pointerEvents = 'none';
}
});
backToTop.addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
// Smooth scroll for anchor links
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const target = document.querySelector(this.getAttribute('href'));
if (target) {
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
});
});
</script>
</body>
</html>
+131
View File
@@ -249,6 +249,137 @@ data:
config: "value"
```
## ExternalSecrets Integration
KubeMirror integrates seamlessly with the [ExternalSecrets Operator](https://external-secrets.io/) to distribute secrets from external stores (1Password, Vault, AWS Secrets Manager, etc.) across multiple namespaces.
### Overview
The `externalsecret-dockerconfig.yaml` example demonstrates how to:
1. Sync secrets from 1Password/Vault/etc using ExternalSecrets
2. Mirror those secrets to multiple namespaces using KubeMirror
3. Avoid race conditions between the two controllers
### Prerequisites
```bash
# Install ExternalSecrets Operator
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets -n external-secrets-system --create-namespace
# Configure your ClusterSecretStore (example for 1Password)
kubectl apply -f your-clustersecretstore.yaml
```
### Quick Start
```bash
# Apply the ExternalSecret example
kubectl apply -f examples/externalsecret-dockerconfig.yaml
# Verify the source secret was created
kubectl get secret multi-registry-secret -n default
# Verify mirrors were created by KubeMirror
kubectl get secrets --all-namespaces -l kubemirror.raczylo.com/mirror=true
# Check sync status
kubectl get secret multi-registry-secret -n default \
-o jsonpath='{.metadata.annotations.kubemirror\.raczylo\.com/sync-status}'
```
### Key Configuration
**Ownership Model**
KubeMirror uses **labels and annotations** to manage mirrors, not ownerReferences. This allows it to work with sources managed by any controller (ExternalSecrets, ArgoCD, etc.):
```yaml
target:
creationPolicy: Owner # Source can be owned by ExternalSecrets
name: multi-registry-secret
template:
metadata:
labels:
kubemirror.raczylo.com/enabled: "true" # KubeMirror detection
annotations:
kubemirror.raczylo.com/sync: "true"
kubemirror.raczylo.com/target-namespaces: "all"
```
**Separation of Concerns:**
- **Source Secret**: Owned by ExternalSecrets (or any other controller) via `ownerReferences`
- **Mirror Secrets**: Managed by KubeMirror via labels + annotations (no ownerReferences copied)
- **Relationship**: Mirrors link to source via annotations (`source-namespace`, `source-name`, `source-uid`)
- **Result**: Each controller manages its own resources independently
### Examples in externalsecret-dockerconfig.yaml
The example file contains three complete examples:
1. **Docker Registry Credentials** - Mirrors Docker config to all namespaces using `target-namespaces: "all"`
2. **Database Credentials** - Uses `all-labeled` for opt-in mirroring with namespace labels
3. **Namespace with Opt-In Label** - Shows how to label namespaces to receive mirrors
### Verification
```bash
# Check ExternalSecret status
kubectl get externalsecret 1p-docker-config -n default
# View the created secret
kubectl get secret multi-registry-secret -n default -o yaml
# Verify KubeMirror picked it up
kubectl logs -n kubemirror-system -l app.kubernetes.io/name=kubemirror | grep multi-registry-secret
# Count how many namespaces received the mirror
kubectl get secrets --all-namespaces -l kubemirror.raczylo.com/mirror=true \
--field-selector metadata.name=multi-registry-secret | wc -l
# Check a specific mirror
kubectl get secret multi-registry-secret -n production-app -o yaml
```
### Testing Updates
Test that ExternalSecrets refreshes propagate to all mirrors:
```bash
# Force ExternalSecret refresh
kubectl annotate externalsecret 1p-docker-config -n default \
force-sync="$(date +%s)" --overwrite
# Wait for ExternalSecret to sync (check status)
kubectl get externalsecret 1p-docker-config -n default -w
# Verify mirrors are updated (check generation or content hash)
kubectl get secret multi-registry-secret -n production-app \
-o jsonpath='{.metadata.annotations.kubemirror\.raczylo\.com/source-content-hash}'
```
### Common Issues
1. **Mirrors Not Created**
- Verify the secret has `kubemirror.raczylo.com/enabled: "true"` label
- Check that KubeMirror annotations are in the ExternalSecret template
- View controller logs: `kubectl logs -n kubemirror-system -l app.kubernetes.io/name=kubemirror`
2. **ExternalSecret Not Syncing**
- Check ExternalSecret status: `kubectl describe externalsecret <name> -n <namespace>`
- Verify ClusterSecretStore is configured: `kubectl get clustersecretstore`
- Check external-secrets-operator logs
### Alternative Backends
The example includes commented configurations for:
- AWS Secrets Manager
- HashiCorp Vault
- Google Secret Manager
Uncomment and configure the ClusterSecretStore for your backend.
## Transformation Rules
KubeMirror supports transformation rules that modify resources during mirroring. This enables environment-specific configurations, security hardening, and dynamic value generation.
+227
View File
@@ -0,0 +1,227 @@
# Example: Using KubeMirror with ExternalSecrets Operator
#
# This example demonstrates how to use KubeMirror to distribute secrets from
# external secret stores (1Password, Vault, AWS Secrets Manager, etc.) across
# multiple namespaces.
#
# KubeMirror automatically strips ownerReferences from mirrors, so you can use
# the standard ExternalSecrets creationPolicy: Owner without conflicts.
#
# Prerequisites:
# 1. ExternalSecrets Operator installed: https://external-secrets.io/
# 2. ClusterSecretStore configured for your secret backend
# 3. KubeMirror installed and running
#
# Apply this manifest:
# kubectl apply -f externalsecret-dockerconfig.yaml
#
# Verify:
# kubectl get externalsecret 1p-docker-config -n default
# kubectl get secret multi-registry-secret -n default
# kubectl get secrets --all-namespaces -l kubemirror.raczylo.com/mirror=true
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: 1p-docker-config
namespace: default
annotations:
description: "Pulls Docker registry credentials from 1Password and mirrors to all namespaces"
spec:
# Secret backend configuration
secretStoreRef:
kind: ClusterSecretStore
name: 1password-homecluster # Replace with your ClusterSecretStore name
# Refresh interval - how often to sync from external store
refreshInterval: 24h
# Target secret configuration
target:
# Standard ExternalSecrets setting - KubeMirror automatically strips ownerReferences from mirrors
creationPolicy: Owner
# Deletion policy - what happens when ExternalSecret is deleted
# - Retain: Keep the secret (recommended with KubeMirror)
# - Delete: Remove the secret
deletionPolicy: Retain
# Name of the Kubernetes secret to create
name: multi-registry-secret
# Template for the secret - includes KubeMirror annotations
template:
type: kubernetes.io/dockerconfigjson
# Metadata to include in the created secret
metadata:
labels:
# REQUIRED: Server-side filtering label for KubeMirror
kubemirror.raczylo.com/enabled: "true"
# Optional: Additional labels
app: registry-credentials
managed-by: external-secrets
annotations:
# REQUIRED: Enable mirroring
kubemirror.raczylo.com/sync: "true"
# Target namespaces - choose one of:
# - Specific namespaces: "app1,app2,app3"
# - Pattern matching: "app-*,prod-*"
# - All namespaces: "all"
# - Labeled namespaces only: "all-labeled"
kubemirror.raczylo.com/target-namespaces: "all"
# Optional: Add description
description: "Docker registry credentials synced from 1Password"
# Docker config JSON template using ExternalSecrets templating
data:
.dockerconfigjson: |
{
"auths": {
"ghcr.io": {
"username": "{{ .ghcrUsername | toString }}",
"auth": "{{ printf "%s:%s" .ghcrUsername .ghcrPassword | b64enc }}"
},
"https://index.docker.io/v1/": {
"username": "{{ .dockerUsername | toString }}",
"auth": "{{ printf "%s:%s" .dockerUsername .dockerPassword | b64enc }}"
}
}
}
# Data mappings - what to fetch from external secret store
data:
# GitHub Container Registry credentials
- remoteRef:
key: DockerAuth/ghcrio_username
property: username # Optional: if secret has multiple properties
secretKey: ghcrUsername
- remoteRef:
key: DockerAuth/ghcrio_password
secretKey: ghcrPassword
# Docker Hub credentials
- remoteRef:
key: DockerAuth/dockerio_username
secretKey: dockerUsername
- remoteRef:
key: DockerAuth/dockerio_password
secretKey: dockerPassword
---
# Example: Using with all-labeled for opt-in mirroring
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: 1p-database-credentials
namespace: shared-resources
spec:
secretStoreRef:
kind: ClusterSecretStore
name: 1password-homecluster
refreshInterval: 24h
target:
creationPolicy: Owner # Standard setting - KubeMirror strips ownerReferences
deletionPolicy: Retain
name: postgres-credentials
template:
type: Opaque
metadata:
labels:
kubemirror.raczylo.com/enabled: "true"
annotations:
kubemirror.raczylo.com/sync: "true"
# Only mirror to namespaces with allow-mirrors label
kubemirror.raczylo.com/target-namespaces: "all-labeled"
stringData:
# ExternalSecrets templating for connection strings
DATABASE_URL: "postgres://{{ .username }}:{{ .password }}@postgres.shared-resources.svc:5432/mydb"
DB_USER: "{{ .username }}"
DB_PASSWORD: "{{ .password }}"
data:
- remoteRef:
key: Database/postgres_username
secretKey: username
- remoteRef:
key: Database/postgres_password
secretKey: password
---
# Example namespace with allow-mirrors label (for all-labeled targeting)
apiVersion: v1
kind: Namespace
metadata:
name: production-app
labels:
# Opt-in to receive mirrored secrets
kubemirror.raczylo.com/allow-mirrors: "true"
environment: production
---
# Alternative ClusterSecretStore examples for different backends
# Uncomment and configure based on your secret backend
# # AWS Secrets Manager
# apiVersion: external-secrets.io/v1beta1
# kind: ClusterSecretStore
# metadata:
# name: aws-secrets-manager
# spec:
# provider:
# aws:
# service: SecretsManager
# region: us-west-2
# auth:
# jwt:
# serviceAccountRef:
# name: external-secrets
# namespace: external-secrets
# # HashiCorp Vault
# apiVersion: external-secrets.io/v1beta1
# kind: ClusterSecretStore
# metadata:
# name: vault-backend
# spec:
# provider:
# vault:
# server: "https://vault.example.com"
# path: "secret"
# version: "v2"
# auth:
# kubernetes:
# mountPath: "kubernetes"
# role: "external-secrets"
# serviceAccountRef:
# name: external-secrets
# namespace: external-secrets
# # Google Secret Manager
# apiVersion: external-secrets.io/v1beta1
# kind: ClusterSecretStore
# metadata:
# name: google-secret-manager
# spec:
# provider:
# gcpsm:
# projectID: "my-project-id"
# auth:
# workloadIdentity:
# clusterLocation: us-central1
# clusterName: my-cluster
# serviceAccountRef:
# name: external-secrets
# namespace: external-secrets
+9
View File
@@ -150,6 +150,11 @@ func createUnstructuredMirror(source runtime.Object, targetNamespace, sourceHash
mirror.SetGeneration(0)
mirror.SetCreationTimestamp(metav1.Time{})
mirror.SetFinalizers(nil) // Mirrors should not have finalizers
// IMPORTANT: Mirrors should never have ownerReferences from source.
// KubeMirror manages mirrors via labels/annotations, not ownership.
// This allows sources to be owned by other controllers (ExternalSecrets, ArgoCD, etc.)
// while KubeMirror independently manages the mirrors.
mirror.SetOwnerReferences(nil)
return mirror, nil
}
@@ -318,6 +323,10 @@ func updateUnstructuredMirror(mirror, source runtime.Object, sourceHash string)
// Ensure mirrors never have finalizers (even if they were added before this fix)
m.SetFinalizers(nil)
// Ensure mirrors never have ownerReferences (clean up mirrors from before this fix)
// KubeMirror uses labels/annotations for management, not ownerReferences
m.SetOwnerReferences(nil)
return nil
}
+134
View File
@@ -155,6 +155,140 @@ func TestCreateMirror_Unstructured(t *testing.T) {
assert.Equal(t, "3", annotations[constants.AnnotationSourceGeneration])
}
func TestCreateMirror_Unstructured_StripsOwnerReferences(t *testing.T) {
// Create source with ownerReferences (e.g., managed by ExternalSecrets)
source := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "external-secret",
"namespace": "default",
"uid": "secret-uid-123",
"resourceVersion": "100",
"generation": int64(1),
// Source has ownerReferences (e.g., set by ExternalSecrets operator)
"ownerReferences": []interface{}{
map[string]interface{}{
"apiVersion": "external-secrets.io/v1",
"kind": "ExternalSecret",
"name": "1p-docker-config",
"uid": "externalsecret-uid-456",
"controller": true,
},
},
// Source has finalizers
"finalizers": []interface{}{
"externalsecrets.external-secrets.io/externalsecret-cleanup",
},
},
"data": map[string]interface{}{
"password": "c2VjcmV0",
},
},
}
mirror, err := CreateMirror(source, "target-ns")
require.NoError(t, err)
require.NotNil(t, mirror)
uMirror, ok := mirror.(*unstructured.Unstructured)
require.True(t, ok, "mirror should be Unstructured")
// CRITICAL: Verify ownerReferences are NOT copied to mirror
ownerRefs := uMirror.GetOwnerReferences()
assert.Nil(t, ownerRefs, "mirror should not have ownerReferences from source")
// CRITICAL: Verify finalizers are NOT copied to mirror
finalizers := uMirror.GetFinalizers()
assert.Nil(t, finalizers, "mirror should not have finalizers from source")
// Verify mirror is properly managed by KubeMirror via labels/annotations
assert.Equal(t, constants.ControllerName, uMirror.GetLabels()[constants.LabelManagedBy])
assert.Equal(t, "true", uMirror.GetLabels()[constants.LabelMirror])
assert.Equal(t, "default", uMirror.GetAnnotations()[constants.AnnotationSourceNamespace])
assert.Equal(t, "external-secret", uMirror.GetAnnotations()[constants.AnnotationSourceName])
assert.Equal(t, "secret-uid-123", uMirror.GetAnnotations()[constants.AnnotationSourceUID])
}
func TestUpdateMirror_Unstructured_ClearsOwnerReferences(t *testing.T) {
// Create mirror that somehow has ownerReferences (e.g., from before the fix)
mirror := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "traefik.io/v1alpha1",
"kind": "Middleware",
"metadata": map[string]interface{}{
"name": "test-middleware",
"namespace": "target-ns",
"labels": map[string]interface{}{
constants.LabelManagedBy: constants.ControllerName,
constants.LabelMirror: "true",
},
"annotations": map[string]interface{}{
constants.AnnotationSourceNamespace: "default",
constants.AnnotationSourceName: "test-middleware",
constants.AnnotationSourceContentHash: "oldhash",
},
// Mirror has ownerReferences (from before fix or external modification)
"ownerReferences": []interface{}{
map[string]interface{}{
"apiVersion": "external-secrets.io/v1",
"kind": "ExternalSecret",
"name": "1p-docker-config",
"uid": "externalsecret-uid-456",
},
},
// Mirror has finalizers (from before fix or external modification)
"finalizers": []interface{}{
"some-finalizer",
},
},
"spec": map[string]interface{}{
"basicAuth": map[string]interface{}{
"secret": "old-secret",
},
},
},
}
source := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "traefik.io/v1alpha1",
"kind": "Middleware",
"metadata": map[string]interface{}{
"name": "test-middleware",
"namespace": "default",
"generation": int64(2),
},
"spec": map[string]interface{}{
"basicAuth": map[string]interface{}{
"secret": "new-secret",
},
},
},
}
err := UpdateMirror(mirror, source)
require.NoError(t, err)
// CRITICAL: Verify ownerReferences are cleared from mirror
ownerRefs := mirror.GetOwnerReferences()
assert.Nil(t, ownerRefs, "mirror should not have ownerReferences after update")
// CRITICAL: Verify finalizers are cleared from mirror
finalizers := mirror.GetFinalizers()
assert.Nil(t, finalizers, "mirror should not have finalizers after update")
// Verify spec was updated
secret, found, err := unstructured.NestedString(mirror.Object, "spec", "basicAuth", "secret")
require.NoError(t, err)
require.True(t, found)
assert.Equal(t, "new-secret", secret)
// Verify hash was updated
assert.NotEqual(t, "oldhash", mirror.GetAnnotations()[constants.AnnotationSourceContentHash])
}
func TestUpdateMirror_Secret(t *testing.T) {
mirror := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
+86 -2
View File
@@ -120,6 +120,15 @@ func (r *SourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
}
}
// Clean up orphaned mirrors (namespaces that no longer match the target criteria)
orphanedCount, err := r.cleanupOrphanedMirrors(ctx, sourceObj, targetNamespaces)
if err != nil {
logger.Error(err, "failed to cleanup orphaned mirrors")
// Don't fail reconciliation for cleanup errors, just log them
} else if orphanedCount > 0 {
logger.Info("cleaned up orphaned mirrors", "count", orphanedCount)
}
// Update status annotation with last sync info
if err := r.updateLastSyncStatus(ctx, source, sourceObj, reconciledCount, errorCount); err != nil {
logger.Error(err, "failed to update sync status")
@@ -131,9 +140,9 @@ func (r *SourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
"errors", errorCount,
"total", len(targetNamespaces))
// Requeue if there were errors
// Return error if there were errors (controller-runtime will automatically requeue with exponential backoff)
if errorCount > 0 {
return ctrl.Result{Requeue: true}, fmt.Errorf("failed to reconcile %d/%d mirrors", errorCount, len(targetNamespaces))
return ctrl.Result{}, fmt.Errorf("failed to reconcile %d/%d mirrors", errorCount, len(targetNamespaces))
}
return ctrl.Result{}, nil
@@ -293,6 +302,81 @@ func (r *SourceReconciler) deleteAllMirrors(ctx context.Context, sourceObj metav
return nil
}
// cleanupOrphanedMirrors removes mirrors that exist but are no longer in the target list.
// This handles cases where target-namespaces annotation changes (e.g., "all" → "all-labeled" or "app-*" → "prod-*").
func (r *SourceReconciler) cleanupOrphanedMirrors(ctx context.Context, sourceObj metav1.Object, targetNamespaces []string) (int, error) {
logger := log.FromContext(ctx)
// List all namespaces
allNamespaces, err := r.NamespaceLister.ListNamespaces(ctx)
if err != nil {
return 0, fmt.Errorf("failed to list namespaces: %w", err)
}
// Get GVK from source object
sourceUnstructured, ok := sourceObj.(*unstructured.Unstructured)
if !ok {
return 0, fmt.Errorf("source object is not unstructured")
}
// Create a set of target namespaces for quick lookup
targetSet := make(map[string]bool)
for _, ns := range targetNamespaces {
targetSet[ns] = true
}
var deletedCount int
for _, ns := range allNamespaces {
// Skip source namespace
if ns == sourceObj.GetNamespace() {
continue
}
// Skip if this namespace IS in the current target list
if targetSet[ns] {
continue
}
// Check if a mirror exists in this namespace
mirror := &unstructured.Unstructured{}
mirror.SetGroupVersionKind(sourceUnstructured.GroupVersionKind())
mirror.SetNamespace(ns)
mirror.SetName(sourceObj.GetName())
err := r.Get(ctx, client.ObjectKey{Namespace: ns, Name: sourceObj.GetName()}, mirror)
if errors.IsNotFound(err) {
// No mirror exists, nothing to clean up
continue
}
if err != nil {
logger.Error(err, "failed to check for mirror", "namespace", ns)
continue
}
// Verify this is actually our mirror (not someone else's resource with the same name)
if !IsManagedByUs(mirror) {
continue
}
// Verify this mirror points to our source
srcNs, srcName, _, found := GetSourceReference(mirror)
if !found || srcNs != sourceObj.GetNamespace() || srcName != sourceObj.GetName() {
continue
}
// This is an orphaned mirror - delete it
if err := r.Delete(ctx, mirror); err != nil {
logger.Error(err, "failed to delete orphaned mirror", "namespace", ns)
continue
}
deletedCount++
logger.V(1).Info("deleted orphaned mirror", "namespace", ns)
}
return deletedCount, nil
}
// resolveTargetNamespaces determines which namespaces should receive mirrors.
func (r *SourceReconciler) resolveTargetNamespaces(ctx context.Context, sourceObj metav1.Object) ([]string, error) {
annotations := sourceObj.GetAnnotations()
+325
View File
@@ -466,3 +466,328 @@ func BenchmarkResolveTargetNamespaces(b *testing.B) {
_, _ = r.resolveTargetNamespaces(ctx, sourceObj)
}
}
func TestSourceReconciler_cleanupOrphanedMirrors(t *testing.T) {
// Setup: Source in default namespace with mirrors in app-1, app-2, app-3
// Then target-namespaces changes to only app-1, app-2
// Expect: app-3 mirror is deleted (orphaned)
sourceObj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "test-secret",
"namespace": "default",
"uid": "source-uid-123",
},
},
}
// Mock existing mirror in app-3 (will be orphaned)
orphanedMirror := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "test-secret",
"namespace": "app-3",
"labels": map[string]interface{}{
constants.LabelManagedBy: constants.ControllerName,
constants.LabelMirror: "true",
},
"annotations": map[string]interface{}{
constants.AnnotationSourceNamespace: "default",
constants.AnnotationSourceName: "test-secret",
constants.AnnotationSourceUID: "source-uid-123",
},
},
},
}
mockClient := new(MockClient)
mockLister := new(MockNamespaceLister)
// Setup: all namespaces in cluster
allNamespaces := []string{"default", "app-1", "app-2", "app-3", "prod-1"}
mockLister.On("ListNamespaces", mock.Anything).Return(allNamespaces, nil)
// Current target list (after annotation change): only app-1 and app-2
targetNamespaces := []string{"app-1", "app-2"}
// The function will iterate through all namespaces and:
// - Skip "default" (source namespace)
// - Skip "app-1" and "app-2" (in target list)
// - Check "app-3" (not in target list) → will find orphaned mirror
// - Check "prod-1" (not in target list) → no mirror exists
notFoundErr := errors.NewNotFound(schema.GroupResource{Group: "", Resource: "secrets"}, "test-secret")
// app-3: orphaned mirror exists
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "app-3", Name: "test-secret"}, mock.Anything).
Return(nil, orphanedMirror)
// prod-1: no mirror exists
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "prod-1", Name: "test-secret"}, mock.Anything).
Return(notFoundErr, nil)
// Expect delete call for app-3 mirror
mockClient.On("Delete", mock.Anything, mock.MatchedBy(func(obj client.Object) bool {
u, ok := obj.(*unstructured.Unstructured)
return ok && u.GetNamespace() == "app-3" && u.GetName() == "test-secret"
}), mock.Anything).Return(nil)
r := &SourceReconciler{
Client: mockClient,
NamespaceLister: mockLister,
}
ctx := context.Background()
deletedCount, err := r.cleanupOrphanedMirrors(ctx, sourceObj, targetNamespaces)
require.NoError(t, err)
assert.Equal(t, 1, deletedCount, "should have deleted 1 orphaned mirror")
mockClient.AssertExpectations(t)
mockLister.AssertExpectations(t)
}
func TestSourceReconciler_Reconcile_AnnotationChange_AllToAllLabeled(t *testing.T) {
// Scenario: annotation changes from "all" → "all-labeled"
// Before: mirrors in all 5 namespaces
// After: mirrors only in labeled namespaces (app-1, app-2)
// Expected: 3 orphaned mirrors deleted (app-3, prod-1, prod-2)
source := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "test-secret",
"namespace": "default",
"uid": "source-uid-123",
"labels": map[string]interface{}{
constants.LabelEnabled: "true",
},
"annotations": map[string]interface{}{
constants.AnnotationSync: "true",
constants.AnnotationTargetNamespaces: "all-labeled", // Changed from "all"
},
},
"data": map[string]interface{}{
"password": "c2VjcmV0",
},
},
}
// Setup: 5 total namespaces, only 2 have allow-mirrors label
allNamespaces := []string{"default", "app-1", "app-2", "app-3", "prod-1", "prod-2"}
allowMirrorsNamespaces := []string{"app-1", "app-2"}
// Mock existing orphaned mirrors in app-3, prod-1, prod-2
createOrphanedMirror := func(ns string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Secret",
"metadata": map[string]interface{}{
"name": "test-secret",
"namespace": ns,
"labels": map[string]interface{}{
constants.LabelManagedBy: constants.ControllerName,
constants.LabelMirror: "true",
},
"annotations": map[string]interface{}{
constants.AnnotationSourceNamespace: "default",
constants.AnnotationSourceName: "test-secret",
constants.AnnotationSourceUID: "source-uid-123",
},
},
"data": map[string]interface{}{
"password": "c2VjcmV0",
},
},
}
}
mockClient := new(MockClient)
mockLister := new(MockNamespaceLister)
mockFilter := filter.NewNamespaceFilter(nil, nil)
mockLister.On("ListNamespaces", mock.Anything).Return(allNamespaces, nil)
mockLister.On("ListAllowMirrorsNamespaces", mock.Anything).Return(allowMirrorsNamespaces, nil)
// Mock Get for source
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "default", Name: "test-secret"}, mock.Anything).
Return(nil, source)
// Mock reconcileMirror calls for app-1 and app-2 (current targets)
notFoundErr := errors.NewNotFound(schema.GroupResource{Group: "", Resource: "secrets"}, "test-secret")
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "app-1", Name: "test-secret"}, mock.Anything).
Return(notFoundErr, nil).Once()
mockClient.On("Create", mock.Anything, mock.MatchedBy(func(obj client.Object) bool {
return obj.GetNamespace() == "app-1"
}), mock.Anything).Return(nil).Once()
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "app-2", Name: "test-secret"}, mock.Anything).
Return(notFoundErr, nil).Once()
mockClient.On("Create", mock.Anything, mock.MatchedBy(func(obj client.Object) bool {
return obj.GetNamespace() == "app-2"
}), mock.Anything).Return(nil).Once()
// Mock cleanup: check orphaned namespaces app-3, prod-1, prod-2
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "app-3", Name: "test-secret"}, mock.Anything).
Return(nil, createOrphanedMirror("app-3")).Once()
mockClient.On("Delete", mock.Anything, mock.MatchedBy(func(obj client.Object) bool {
return obj.GetNamespace() == "app-3"
}), mock.Anything).Return(nil).Once()
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "prod-1", Name: "test-secret"}, mock.Anything).
Return(nil, createOrphanedMirror("prod-1")).Once()
mockClient.On("Delete", mock.Anything, mock.MatchedBy(func(obj client.Object) bool {
return obj.GetNamespace() == "prod-1"
}), mock.Anything).Return(nil).Once()
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "prod-2", Name: "test-secret"}, mock.Anything).
Return(nil, createOrphanedMirror("prod-2")).Once()
mockClient.On("Delete", mock.Anything, mock.MatchedBy(func(obj client.Object) bool {
return obj.GetNamespace() == "prod-2"
}), mock.Anything).Return(nil).Once()
// Mock Update for status annotation
mockClient.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil)
r := &SourceReconciler{
Client: mockClient,
Scheme: runtime.NewScheme(),
Config: &config.Config{},
Filter: mockFilter,
NamespaceLister: mockLister,
GVK: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Secret"},
}
ctx := context.Background()
req := ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "test-secret"}}
result, err := r.Reconcile(ctx, req)
require.NoError(t, err)
assert.Equal(t, ctrl.Result{}, result)
mockClient.AssertExpectations(t)
mockLister.AssertExpectations(t)
}
func TestSourceReconciler_Reconcile_AnnotationChange_PatternChange(t *testing.T) {
// Scenario: annotation changes from "app-*" → "prod-*"
// Before: mirrors in app-1, app-2, app-3
// After: mirrors in prod-1, prod-2
// Expected: app-1, app-2, app-3 deleted; prod-1, prod-2 created
source := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "app-config",
"namespace": "default",
"uid": "config-uid-456",
"labels": map[string]interface{}{
constants.LabelEnabled: "true",
},
"annotations": map[string]interface{}{
constants.AnnotationSync: "true",
constants.AnnotationTargetNamespaces: "prod-*", // Changed from "app-*"
},
},
"data": map[string]interface{}{
"key": "value",
},
},
}
allNamespaces := []string{"default", "app-1", "app-2", "app-3", "prod-1", "prod-2"}
createOrphanedMirror := func(ns string) *unstructured.Unstructured {
return &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": map[string]interface{}{
"name": "app-config",
"namespace": ns,
"labels": map[string]interface{}{
constants.LabelManagedBy: constants.ControllerName,
constants.LabelMirror: "true",
},
"annotations": map[string]interface{}{
constants.AnnotationSourceNamespace: "default",
constants.AnnotationSourceName: "app-config",
constants.AnnotationSourceUID: "config-uid-456",
},
},
"data": map[string]interface{}{
"key": "value",
},
},
}
}
mockClient := new(MockClient)
mockLister := new(MockNamespaceLister)
mockFilter := filter.NewNamespaceFilter(nil, nil)
mockLister.On("ListNamespaces", mock.Anything).Return(allNamespaces, nil)
mockLister.On("ListAllowMirrorsNamespaces", mock.Anything).Return([]string{}, nil)
// Mock Get for source
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "default", Name: "app-config"}, mock.Anything).
Return(nil, source)
notFoundErr := errors.NewNotFound(schema.GroupResource{Group: "", Resource: "configmaps"}, "app-config")
// Mock reconcileMirror for prod-1 and prod-2 (new targets)
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "prod-1", Name: "app-config"}, mock.Anything).
Return(notFoundErr, nil).Once()
mockClient.On("Create", mock.Anything, mock.MatchedBy(func(obj client.Object) bool {
return obj.GetNamespace() == "prod-1"
}), mock.Anything).Return(nil).Once()
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "prod-2", Name: "app-config"}, mock.Anything).
Return(notFoundErr, nil).Once()
mockClient.On("Create", mock.Anything, mock.MatchedBy(func(obj client.Object) bool {
return obj.GetNamespace() == "prod-2"
}), mock.Anything).Return(nil).Once()
// Mock cleanup: delete orphaned mirrors in app-1, app-2, app-3
for _, ns := range []string{"app-1", "app-2", "app-3"} {
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: ns, Name: "app-config"}, mock.Anything).
Return(nil, createOrphanedMirror(ns)).Once()
mockClient.On("Delete", mock.Anything, mock.MatchedBy(func(obj client.Object) bool {
return obj.GetNamespace() == ns
}), mock.Anything).Return(nil).Once()
}
// Mock Update for status
mockClient.On("Update", mock.Anything, mock.Anything, mock.Anything).Return(nil)
r := &SourceReconciler{
Client: mockClient,
Scheme: runtime.NewScheme(),
Config: &config.Config{},
Filter: mockFilter,
NamespaceLister: mockLister,
GVK: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "ConfigMap"},
}
ctx := context.Background()
req := ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "app-config"}}
result, err := r.Reconcile(ctx, req)
require.NoError(t, err)
assert.Equal(t, ctrl.Result{}, result)
mockClient.AssertExpectations(t)
mockLister.AssertExpectations(t)
}