Compare commits

..
6 Commits
10 changed files with 1358 additions and 369 deletions
+61 -56
View File
@@ -1,70 +1,75 @@
name: Release name: Release
on: on:
workflow_dispatch: workflow_dispatch:
push: push:
tags: paths-ignore:
- 'v*' - "**.md"
- "docs/**"
- "examples/**"
branches:
- main
permissions: permissions:
id-token: write id-token: write
contents: write contents: write
packages: write packages: write
jobs: jobs:
release: release:
uses: lukaszraczylo/shared-actions/.github/workflows/go-release.yaml@main 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
with: with:
fetch-depth: 0 go-version: ">=1.25"
docker-enabled: true
secrets: inherit
- name: Get release version publish-helm-chart:
id: version needs: release
run: | runs-on: ubuntu-latest
VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0") steps:
VERSION=${VERSION#v} - name: Checkout
echo "version=$VERSION" >> $GITHUB_OUTPUT uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Trigger helm-charts release - name: Get release version
env: id: version
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} run: |
RELEASE_VERSION: ${{ steps.version.outputs.version }} VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0")
run: | VERSION=${VERSION#v}
gh api repos/lukaszraczylo/helm-charts/dispatches \ echo "version=$VERSION" >> $GITHUB_OUTPUT
-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: - name: Trigger helm-charts release
needs: release env:
runs-on: ubuntu-latest GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
permissions: RELEASE_VERSION: ${{ steps.version.outputs.version }}
contents: read run: |
pages: write gh api repos/lukaszraczylo/helm-charts/dispatches \
id-token: write -f event_type=release-chart \
steps: -f client_payload[chart_name]=kubemirror \
- name: Checkout -f client_payload[version]="$RELEASE_VERSION" \
uses: actions/checkout@v4 -f client_payload[source_repo]=lukaszraczylo/kubemirror \
-f client_payload[chart_path]=charts/kubemirror
- name: Setup Pages publish-website:
uses: actions/configure-pages@v5 needs: release
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Upload artifact - name: Setup Pages
uses: actions/upload-pages-artifact@v3 uses: actions/configure-pages@v5
with:
path: './docs'
- name: Deploy to GitHub Pages - name: Upload artifact
id: deployment uses: actions/upload-pages-artifact@v3
uses: actions/deploy-pages@v4 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 FROM gcr.io/distroless/static:nonroot
ARG TARGETPLATFORM
WORKDIR / WORKDIR /
COPY ${TARGETPLATFORM}/kubemirror /kubemirror
# Copy the binary from goreleaser build
COPY kubemirror /kubemirror
# Use nonroot user (65532)
USER 65532:65532 USER 65532:65532
ENTRYPOINT ["/kubemirror"] ENTRYPOINT ["/kubemirror"]
+111 -2
View File
@@ -17,8 +17,10 @@ Tested in production environments managing 1000+ mirrors across 200+ namespaces
- [Usage Examples](#usage-examples) - [Usage Examples](#usage-examples)
- [Mirror a Secret to Specific Namespaces](#mirror-a-secret-to-specific-namespaces) - [Mirror a Secret to Specific Namespaces](#mirror-a-secret-to-specific-namespaces)
- [Mirror to Pattern-Matched Namespaces](#mirror-to-pattern-matched-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 to All Labeled Namespaces](#mirror-to-all-labeled-namespaces)
- [Mirror Custom Resources (CRDs)](#mirror-custom-resources-crds) - [Mirror Custom Resources (CRDs)](#mirror-custom-resources-crds)
- [Using with ExternalSecrets Operator](#using-with-externalsecrets-operator)
- [Configuration](#configuration) - [Configuration](#configuration)
- [Helm Chart Values](#helm-chart-values) - [Helm Chart Values](#helm-chart-values)
- [Command-line Flags](#command-line-flags) - [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** | Mirror any Kubernetes resource type - Secrets, ConfigMaps, Ingresses, Services, CRDs, and more |
| **Resources** | Auto-discovery of all mirrorable resources with periodic refresh | | **Resources** | Auto-discovery of all mirrorable resources with periodic refresh |
| **Resources** | Safety deny list prevents mirroring dangerous resources (Pods, Events, Nodes) | | **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** | 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** | Multi-layer change detection: generation field + SHA256 content hash |
| **Sync** | Automatic drift detection and correction for manually modified mirrors | | **Sync** | Automatic drift detection and correction for manually modified mirrors |
| **Sync** | Finalizer-based cleanup ensures mirrors are deleted with source | | **Sync** | Finalizer-based cleanup ensures mirrors are deleted with source |
@@ -207,8 +209,33 @@ data:
api_url: "https://api.example.com" 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 ### Mirror to All Labeled Namespaces
Use `all-labeled` for opt-in mirroring where target namespaces must explicitly allow mirrors:
**Source Resource:** **Source Resource:**
```yaml ```yaml
apiVersion: v1 apiVersion: v1
@@ -265,6 +292,88 @@ spec:
- text/event-stream - 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 ### Transformation Rules
KubeMirror supports powerful transformation rules that modify resources during mirroring. This enables environment-specific configurations, security hardening, and dynamic value generation. 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> <!doctype html>
<html lang="en"> <html lang="en" class="scroll-smooth">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -20,34 +20,100 @@
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; 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 { .hover-lift {
transition: transform 0.3s ease, box-shadow 0.3s ease; transition: transform 0.3s ease, box-shadow 0.3s ease;
} }
.hover-lift:hover { .hover-lift:hover {
transform: translateY(-5px); 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 { .code-block {
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%); 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> </style>
</head> </head>
<body class="bg-gradient-to-br from-slate-50 to-blue-50"> <body class="bg-gradient-to-br from-slate-50 to-blue-50">
<!-- Scroll Progress Bar -->
<div class="progress-bar" id="progressBar"></div>
<!-- Navigation --> <!-- 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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16 items-center"> <div class="flex justify-between h-16 items-center">
<a href="#" class="flex items-center gap-3 group"> <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> <i class="fas fa-copy text-2xl text-white"></i>
</div> </div>
<span class="text-2xl font-bold gradient-text">KubeMirror</span> <span class="text-2xl font-bold gradient-text">KubeMirror</span>
</a> </a>
<!-- Desktop Menu -->
<div class="hidden md:flex space-x-8"> <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="#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="#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="#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> <a href="#installation" class="text-slate-700 hover:text-blue-600 font-medium transition-colors">Install</a>
</div> </div>
<div class="flex items-center space-x-4"> <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"> <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> <i class="fab fa-github text-2xl"></i>
</a> </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> </div>
</div> </div>
</nav> </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 --> <!-- Hero Section -->
<section class="relative overflow-hidden py-24"> <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="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="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"> <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> <i class="fas fa-copy text-7xl text-white"></i>
</div> </div>
</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/> Copy Kubernetes Resources<br/>
<span class="gradient-text">Across Namespaces</span> <span class="gradient-text">Across Namespaces</span>
</h1> </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. 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. <strong>Automatically keep them in sync.</strong> Transform values per environment.
</p> </p>
<div class="flex justify-center gap-6"> <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"> <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> <i class="fas fa-download mr-2"></i>
Get Started Get Started
</a> </a>
@@ -98,8 +180,8 @@
<section id="problem" class="py-24 bg-white"> <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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-16"> <div class="text-center mb-16">
<h2 class="text-5xl font-extrabold text-slate-900 mb-6">The Problem</h2> <h2 class="text-4xl md:text-5xl font-extrabold text-slate-900 mb-6">The Problem</h2>
<p class="text-2xl text-slate-600 max-w-4xl mx-auto"> <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. 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> </p>
</div> </div>
@@ -136,13 +218,13 @@
</div> </div>
</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"> <div class="flex items-start gap-4">
<i class="fas fa-check-circle text-green-500 text-4xl mt-1"></i> <i class="fas fa-check-circle text-green-500 text-4xl mt-1"></i>
<div> <div>
<h3 class="font-bold text-3xl text-slate-900 mb-4">KubeMirror's Solution</h3> <h3 class="font-bold text-2xl md:text-3xl text-slate-900 mb-4">KubeMirror's Solution</h3>
<p class="text-slate-700 text-xl leading-relaxed"> <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 all target namespaces and keeps them synchronized. 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). 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. Works with any Kubernetes resource type.
</p> </p>
@@ -156,17 +238,17 @@
<section id="features" class="py-24 bg-gradient-to-br from-slate-50 to-blue-50"> <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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-20"> <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> <p class="text-xl text-slate-600">Everything you need for resource mirroring and synchronization</p>
</div> </div>
<div class="grid md:grid-cols-2 gap-10"> <div class="grid md:grid-cols-2 gap-10">
<!-- Any Resource Type --> <!-- 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"> <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> <i class="fas fa-layer-group text-3xl text-white"></i>
</div> </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"> <p class="text-slate-600 mb-6 text-lg">
Not just Secrets and ConfigMaps. Mirror any namespaced Kubernetes resource: Not just Secrets and ConfigMaps. Mirror any namespaced Kubernetes resource:
</p> </p>
@@ -184,15 +266,15 @@
</div> </div>
<!-- Transformation Rules --> <!-- 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"> <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> <i class="fas fa-magic text-3xl text-white"></i>
</div> </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"> <p class="text-slate-600 mb-6 text-lg">
Change values automatically based on target namespace: Change values automatically based on target namespace:
</p> </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 class="text-green-400"># Preprod namespaces get preprod API</pre>
<pre><span class="text-yellow-400">- path:</span> data.API_URL <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">value:</span> <span class="text-blue-400">"https://preprod.api.com"</span>
@@ -211,11 +293,11 @@
</div> </div>
<!-- Automatic Sync --> <!-- 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"> <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> <i class="fas fa-sync-alt text-3xl text-white"></i>
</div> </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"> <p class="text-slate-600 mb-6 text-lg">
Update the source once. All copies update automatically: Update the source once. All copies update automatically:
</p> </p>
@@ -233,26 +315,30 @@
</div> </div>
<!-- Smart Targeting --> <!-- 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"> <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> <i class="fas fa-bullseye text-3xl text-white"></i>
</div> </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"> <p class="text-slate-600 mb-6 text-lg">
Choose which namespaces receive the copy: Choose which namespaces receive the copy:
</p> </p>
<div class="space-y-4 text-slate-700 text-lg"> <div class="space-y-4 text-slate-700 text-base md:text-lg">
<div class="flex items-center gap-3"> <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">namespace-1,namespace-2</code> <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> <span>Specific namespaces</span>
</div> </div>
<div class="flex items-center gap-3"> <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">app-*,prod-*</code> <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> <span>Pattern matching</span>
</div> </div>
<div class="flex items-center gap-3"> <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">all-labeled</code> <code class="bg-slate-100 px-4 py-2 rounded-lg font-mono text-purple-700 font-semibold text-sm">all</code>
<span>All labeled namespaces</span> <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> </div>
<div class="mt-6 p-4 bg-orange-50 rounded-lg border border-orange-200"> <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"> <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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-20"> <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> <p class="text-xl text-slate-600">See how easy it is to get started with KubeMirror</p>
</div> </div>
<div class="space-y-12"> <div class="space-y-12">
<!-- Example 1: Basic Secret --> <!-- 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="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-start gap-6 mb-6"> <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"> <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> <span class="text-white font-bold text-2xl">1</span>
</div> </div>
<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> <i class="fas fa-lock text-blue-600 mr-3"></i>
Basic: Mirror a TLS Secret Basic: Mirror a TLS Secret
</h3> </h3>
<p class="text-slate-600 text-lg">Share your TLS certificate across multiple application namespaces</p> <p class="text-slate-600 text-lg">Share your TLS certificate across multiple application namespaces</p>
</div> </div>
</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 <pre><span class="text-blue-400">apiVersion:</span> v1
<span class="text-blue-400">kind:</span> Secret <span class="text-blue-400">kind:</span> Secret
<span class="text-blue-400">metadata:</span> <span class="text-blue-400">metadata:</span>
@@ -307,20 +393,20 @@
</div> </div>
<!-- Example 2: Pattern Matching --> <!-- 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="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-start gap-6 mb-6"> <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"> <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> <span class="text-white font-bold text-2xl">2</span>
</div> </div>
<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> <i class="fas fa-asterisk text-purple-600 mr-3"></i>
Pattern Matching: Mirror to All App Namespaces Pattern Matching: Mirror to All App Namespaces
</h3> </h3>
<p class="text-slate-600 text-lg">Use wildcards to mirror to all namespaces matching a pattern</p> <p class="text-slate-600 text-lg">Use wildcards to mirror to all namespaces matching a pattern</p>
</div> </div>
</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 <pre><span class="text-blue-400">apiVersion:</span> v1
<span class="text-blue-400">kind:</span> ConfigMap <span class="text-blue-400">kind:</span> ConfigMap
<span class="text-blue-400">metadata:</span> <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> <span class="text-yellow-400">api_url:</span> <span class="text-purple-400">"https://api.example.com"</span></pre>
</div> </div>
<div class="mt-6 p-5 bg-purple-100 rounded-lg border border-purple-300"> <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> <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> </p>
</div> </div>
</div> </div>
<!-- Example 3: Custom Resource (Traefik) --> <!-- 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="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 items-start gap-6 mb-6"> <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"> <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> <span class="text-white font-bold text-2xl">3</span>
</div> </div>
<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> <i class="fas fa-cubes text-green-600 mr-3"></i>
Custom Resource: Share Traefik Middleware Custom Resource: Share Traefik Middleware
</h3> </h3>
<p class="text-slate-600 text-lg">Mirror any CRD like Traefik Middleware across your cluster</p> <p class="text-slate-600 text-lg">Mirror any CRD like Traefik Middleware across your cluster</p>
</div> </div>
</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 <pre><span class="text-blue-400">apiVersion:</span> traefik.io/v1alpha1
<span class="text-blue-400">kind:</span> Middleware <span class="text-blue-400">kind:</span> Middleware
<span class="text-blue-400">metadata:</span> <span class="text-blue-400">metadata:</span>
@@ -376,130 +462,12 @@
- text/event-stream</pre> - text/event-stream</pre>
</div> </div>
<div class="mt-6 p-5 bg-green-100 rounded-lg border border-green-300"> <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> <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! <strong>Works with any CRD:</strong> Cert-Manager Certificates, Gateway API resources, or your own custom resources!
</p> </p>
</div> </div>
</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>
</div> </div>
</section> </section>
@@ -508,102 +476,74 @@
<section id="comparison" class="py-24 bg-gradient-to-br from-slate-50 to-blue-50"> <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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-16"> <div class="text-center mb-16">
<h2 class="text-5xl font-extrabold text-slate-900 mb-6">How KubeMirror Compares</h2> <h2 class="text-4xl md: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> <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>
<div class="overflow-x-auto rounded-2xl shadow-2xl"> <div class="overflow-x-auto rounded-2xl shadow-2xl">
<table class="w-full bg-white"> <table class="w-full bg-white">
<thead class="bg-gradient-to-r from-slate-800 to-slate-900 text-white"> <thead class="bg-gradient-to-r from-slate-800 to-slate-900 text-white">
<tr> <tr>
<th class="px-8 py-6 text-left font-bold text-lg">Capability</th> <th class="px-6 md:px-8 py-6 text-left font-bold text-base md:text-lg">Capability</th>
<th class="px-8 py-6 text-center font-bold text-lg">KubeMirror</th> <th class="px-6 md:px-8 py-6 text-center font-bold text-base md: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-center font-bold text-base md:text-lg">Reflector</th>
</tr> </tr>
</thead> </thead>
<tbody class="divide-y divide-slate-200"> <tbody class="divide-y divide-slate-200">
<tr class="hover:bg-blue-50 transition-colors"> <tr class="hover:bg-blue-50 transition-colors">
<td class="px-8 py-6"> <td class="px-6 md:px-8 py-6">
<div class="font-semibold text-lg text-slate-900">Supported Resources</div> <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> <div class="text-sm text-slate-600 mt-1">What resource types can be mirrored</div>
</td> </td>
<td class="px-8 py-6 text-center"> <td class="px-6 md:px-8 py-6 text-center">
<div><i class="fas fa-check-circle text-green-500 text-3xl"></i></div> <div><i class="fas fa-check-circle text-green-500 text-2xl md:text-3xl"></i></div>
<div class="text-sm font-semibold text-green-700 mt-2">Secrets, ConfigMaps, CRDs, etc.</div> <div class="text-xs md:text-sm font-semibold text-green-700 mt-2">Secrets, ConfigMaps, CRDs, etc.</div>
</td> </td>
<td class="px-8 py-6 text-center"> <td class="px-6 md:px-8 py-6 text-center">
<div><i class="fas fa-minus-circle text-yellow-500 text-3xl"></i></div> <div><i class="fas fa-minus-circle text-yellow-500 text-2xl md:text-3xl"></i></div>
<div class="text-sm font-semibold text-yellow-700 mt-2">Secrets, ConfigMaps only</div> <div class="text-xs md:text-sm font-semibold text-yellow-700 mt-2">Secrets, ConfigMaps only</div>
</td> </td>
</tr> </tr>
<tr class="hover:bg-blue-50 transition-colors bg-slate-50"> <tr class="hover:bg-blue-50 transition-colors bg-slate-50">
<td class="px-8 py-6"> <td class="px-6 md:px-8 py-6">
<div class="font-semibold text-lg text-slate-900">Auto-Discovery</div> <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> <div class="text-sm text-slate-600 mt-1">Finds all resource types automatically</div>
</td> </td>
<td class="px-8 py-6 text-center"> <td class="px-6 md:px-8 py-6 text-center">
<div><i class="fas fa-check-circle text-green-500 text-3xl"></i></div> <div><i class="fas fa-check-circle text-green-500 text-2xl md:text-3xl"></i></div>
<div class="text-sm font-semibold text-green-700 mt-2">Yes</div> <div class="text-xs md:text-sm font-semibold text-green-700 mt-2">Yes</div>
</td> </td>
<td class="px-8 py-6 text-center"> <td class="px-6 md:px-8 py-6 text-center">
<div><i class="fas fa-times-circle text-red-500 text-3xl"></i></div> <div><i class="fas fa-times-circle text-red-500 text-2xl md:text-3xl"></i></div>
<div class="text-sm font-semibold text-red-700 mt-2">Hardcoded</div> <div class="text-xs md:text-sm font-semibold text-red-700 mt-2">Hardcoded</div>
</td> </td>
</tr> </tr>
<tr class="hover:bg-blue-50 transition-colors"> <tr class="hover:bg-blue-50 transition-colors">
<td class="px-8 py-6"> <td class="px-6 md:px-8 py-6">
<div class="font-semibold text-lg text-slate-900">Value Transformation</div> <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> <div class="text-sm text-slate-600 mt-1">Change values per target namespace</div>
</td> </td>
<td class="px-8 py-6 text-center"> <td class="px-6 md:px-8 py-6 text-center">
<div><i class="fas fa-check-circle text-green-500 text-3xl"></i></div> <div><i class="fas fa-check-circle text-green-500 text-2xl md:text-3xl"></i></div>
<div class="text-sm font-semibold text-green-700 mt-2">Full support</div> <div class="text-xs md:text-sm font-semibold text-green-700 mt-2">Full support</div>
</td> </td>
<td class="px-8 py-6 text-center"> <td class="px-6 md:px-8 py-6 text-center">
<div><i class="fas fa-times-circle text-red-500 text-3xl"></i></div> <div><i class="fas fa-times-circle text-red-500 text-2xl md:text-3xl"></i></div>
<div class="text-sm font-semibold text-red-700 mt-2">Not available</div> <div class="text-xs md:text-sm font-semibold text-red-700 mt-2">Not available</div>
</td> </td>
</tr> </tr>
<tr class="hover:bg-blue-50 transition-colors bg-slate-50"> <tr class="hover:bg-blue-50 transition-colors bg-slate-50">
<td class="px-8 py-6"> <td class="px-6 md:px-8 py-6">
<div class="font-semibold text-lg text-slate-900">Environment-Specific Rules</div> <div class="font-semibold text-base md:text-lg text-slate-900">Active Development</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> <div class="text-sm text-slate-600 mt-1">Regular updates and bug fixes</div>
</td> </td>
<td class="px-8 py-6 text-center"> <td class="px-6 md:px-8 py-6 text-center">
<div><i class="fas fa-check-circle text-green-500 text-3xl"></i></div> <div><i class="fas fa-check-circle text-green-500 text-2xl md:text-3xl"></i></div>
<div class="text-sm font-semibold text-green-700 mt-2">Active</div> <div class="text-xs md:text-sm font-semibold text-green-700 mt-2">Active</div>
</td> </td>
<td class="px-8 py-6 text-center"> <td class="px-6 md:px-8 py-6 text-center">
<div><i class="fas fa-check-circle text-green-500 text-3xl"></i></div> <div><i class="fas fa-check-circle text-green-500 text-2xl md:text-3xl"></i></div>
<div class="text-sm font-semibold text-green-700 mt-2">Recently resumed (2025)</div> <div class="text-xs md:text-sm font-semibold text-green-700 mt-2">Recently resumed (2025)</div>
</td> </td>
</tr> </tr>
</tbody> </tbody>
@@ -611,11 +551,11 @@
</div> </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="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"> <div class="flex flex-col sm:flex-row items-start gap-6">
<i class="fas fa-info-circle text-blue-600 text-4xl mt-1"></i> <i class="fas fa-info-circle text-blue-600 text-3xl md:text-4xl mt-1"></i>
<div> <div>
<h4 class="text-2xl font-bold text-slate-900 mb-4">Why We Built KubeMirror</h4> <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-lg leading-relaxed"> <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. 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 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. Kubernetes best practices and all the features we wished Reflector had.
@@ -630,20 +570,20 @@
<section id="installation" class="py-24 bg-white"> <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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-20"> <div class="text-center mb-20">
<h2 class="text-5xl font-extrabold text-slate-900 mb-4">Installation</h2> <h2 class="text-4xl md: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> <p class="text-xl md:text-2xl text-slate-600">Get started in under 2 minutes</p>
</div> </div>
<div class="grid md:grid-cols-2 gap-10 mb-16"> <div class="grid md:grid-cols-2 gap-10 mb-16">
<!-- Helm Installation --> <!-- 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="flex items-center mb-6">
<div class="bg-blue-600 w-14 h-14 rounded-xl flex items-center justify-center mr-4"> <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> <i class="fas fa-ship text-2xl text-white"></i>
</div> </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>
<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> <pre><span class="text-green-400">helm repo add kubemirror \</span>
https://lukaszraczylo.github.io/helm-charts https://lukaszraczylo.github.io/helm-charts
@@ -655,14 +595,14 @@
</div> </div>
<!-- kubectl Installation --> <!-- 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="flex items-center mb-6">
<div class="bg-purple-600 w-14 h-14 rounded-xl flex items-center justify-center mr-4"> <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> <i class="fas fa-terminal text-2xl text-white"></i>
</div> </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>
<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> <pre><span class="text-green-400">kubectl apply -k \</span>
github.com/lukaszraczylo/kubemirror/deploy github.com/lukaszraczylo/kubemirror/deploy
@@ -674,8 +614,8 @@
</div> </div>
<!-- Quick Start Example --> <!-- 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"> <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-4xl font-bold text-slate-900 mb-8 text-center"> <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> <i class="fas fa-rocket text-green-600 mr-3"></i>
Quick Start: Mirror a Secret in 30 Seconds Quick Start: Mirror a Secret in 30 Seconds
</h3> </h3>
@@ -684,9 +624,9 @@
<div> <div>
<div class="flex items-center gap-3 mb-6"> <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> <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>
<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 <pre><span class="text-blue-400">apiVersion:</span> v1
<span class="text-blue-400">kind:</span> Secret <span class="text-blue-400">kind:</span> Secret
<span class="text-blue-400">metadata:</span> <span class="text-blue-400">metadata:</span>
@@ -707,15 +647,15 @@
<div> <div>
<div class="flex items-center gap-3 mb-6"> <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> <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> </div>
<p class="text-slate-700 mb-6 text-lg"> <p class="text-slate-700 mb-6 text-base md:text-lg">
KubeMirror automatically: KubeMirror automatically:
</p> </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"> <li class="flex items-start gap-3">
<i class="fas fa-check-circle text-green-500 text-xl mt-1"></i> <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>
<li class="flex items-start gap-3"> <li class="flex items-start gap-3">
<i class="fas fa-check-circle text-green-500 text-xl mt-1"></i> <i class="fas fa-check-circle text-green-500 text-xl mt-1"></i>
@@ -732,8 +672,8 @@
</ul> </ul>
<div class="mt-8 p-5 bg-green-100 rounded-lg border border-green-300"> <div class="mt-8 p-5 bg-green-100 rounded-lg border border-green-300">
<p class="text-sm text-slate-700"> <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> <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">kubemirror.raczylo.com/sync</code> are needed. 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> </p>
</div> </div>
</div> </div>
@@ -742,42 +682,6 @@
</div> </div>
</section> </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 -->
<footer class="bg-gradient-to-br from-slate-900 to-slate-800 text-gray-300 py-16"> <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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
@@ -789,25 +693,90 @@
</div> </div>
<span class="text-2xl font-bold text-white">KubeMirror</span> <span class="text-2xl font-bold text-white">KubeMirror</span>
</div> </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. Copy Kubernetes resources across namespaces. Modern replacement for Reflector.
</p> </p>
</div> </div>
<div> <div>
<h4 class="text-xl font-bold text-white mb-6">Links</h4> <h4 class="text-lg md:text-xl font-bold text-white mb-6">Links</h4>
<ul class="space-y-3 text-lg"> <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" 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/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> <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> </ul>
</div> </div>
<div> <div>
<h4 class="text-xl font-bold text-white mb-6">License</h4> <h4 class="text-lg md: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 text-base md:text-lg">MIT License</p>
<p class="text-gray-400 mt-4 text-lg">© 2024 Lukasz Raczylo</p> <p class="text-gray-400 mt-4 text-base md:text-lg">© 2024 Lukasz Raczylo</p>
</div> </div>
</div> </div>
</div> </div>
</footer> </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> </body>
</html> </html>
+131
View File
@@ -249,6 +249,137 @@ data:
config: "value" 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 ## Transformation Rules
KubeMirror supports transformation rules that modify resources during mirroring. This enables environment-specific configurations, security hardening, and dynamic value generation. 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.SetGeneration(0)
mirror.SetCreationTimestamp(metav1.Time{}) mirror.SetCreationTimestamp(metav1.Time{})
mirror.SetFinalizers(nil) // Mirrors should not have finalizers 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 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) // Ensure mirrors never have finalizers (even if they were added before this fix)
m.SetFinalizers(nil) 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 return nil
} }
+134
View File
@@ -155,6 +155,140 @@ func TestCreateMirror_Unstructured(t *testing.T) {
assert.Equal(t, "3", annotations[constants.AnnotationSourceGeneration]) 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) { func TestUpdateMirror_Secret(t *testing.T) {
mirror := &corev1.Secret{ mirror := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{ 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 // Update status annotation with last sync info
if err := r.updateLastSyncStatus(ctx, source, sourceObj, reconciledCount, errorCount); err != nil { if err := r.updateLastSyncStatus(ctx, source, sourceObj, reconciledCount, errorCount); err != nil {
logger.Error(err, "failed to update sync status") 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, "errors", errorCount,
"total", len(targetNamespaces)) "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 { 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 return ctrl.Result{}, nil
@@ -293,6 +302,81 @@ func (r *SourceReconciler) deleteAllMirrors(ctx context.Context, sourceObj metav
return nil 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. // resolveTargetNamespaces determines which namespaces should receive mirrors.
func (r *SourceReconciler) resolveTargetNamespaces(ctx context.Context, sourceObj metav1.Object) ([]string, error) { func (r *SourceReconciler) resolveTargetNamespaces(ctx context.Context, sourceObj metav1.Object) ([]string, error) {
annotations := sourceObj.GetAnnotations() annotations := sourceObj.GetAnnotations()
+325
View File
@@ -466,3 +466,328 @@ func BenchmarkResolveTargetNamespaces(b *testing.B) {
_, _ = r.resolveTargetNamespaces(ctx, sourceObj) _, _ = 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)
}