mirror of
https://github.com/lukaszraczylo/kubemirror.git
synced 2026-07-22 19:29:36 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6bdc1f559 | ||
|
|
54f4f9306c | ||
|
|
2dd34bf39e | ||
|
|
cdae3f7fd7 | ||
|
|
22572aed75 |
@@ -1,70 +1,75 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths-ignore:
|
||||
- "**.md"
|
||||
- "docs/**"
|
||||
- "examples/**"
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
packages: write
|
||||
id-token: write
|
||||
contents: write
|
||||
packages: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
uses: lukaszraczylo/shared-actions/.github/workflows/go-release.yaml@main
|
||||
with:
|
||||
go-version: ">=1.25"
|
||||
secrets: inherit
|
||||
|
||||
publish-helm-chart:
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
release:
|
||||
uses: lukaszraczylo/shared-actions/.github/workflows/go-release.yaml@main
|
||||
with:
|
||||
fetch-depth: 0
|
||||
go-version: ">=1.25"
|
||||
docker-enabled: true
|
||||
secrets: inherit
|
||||
|
||||
- name: Get release version
|
||||
id: version
|
||||
run: |
|
||||
VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0")
|
||||
VERSION=${VERSION#v}
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
publish-helm-chart:
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Trigger helm-charts release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
RELEASE_VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
gh api repos/lukaszraczylo/helm-charts/dispatches \
|
||||
-f event_type=release-chart \
|
||||
-f client_payload[chart_name]=kubemirror \
|
||||
-f client_payload[version]="$RELEASE_VERSION" \
|
||||
-f client_payload[source_repo]=lukaszraczylo/kubemirror \
|
||||
-f client_payload[chart_path]=charts/kubemirror
|
||||
- name: Get release version
|
||||
id: version
|
||||
run: |
|
||||
VERSION=$(git describe --tags --abbrev=0 2>/dev/null || echo "0.0.0")
|
||||
VERSION=${VERSION#v}
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
|
||||
publish-website:
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Trigger helm-charts release
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
|
||||
RELEASE_VERSION: ${{ steps.version.outputs.version }}
|
||||
run: |
|
||||
gh api repos/lukaszraczylo/helm-charts/dispatches \
|
||||
-f event_type=release-chart \
|
||||
-f client_payload[chart_name]=kubemirror \
|
||||
-f client_payload[version]="$RELEASE_VERSION" \
|
||||
-f client_payload[source_repo]=lukaszraczylo/kubemirror \
|
||||
-f client_payload[chart_path]=charts/kubemirror
|
||||
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
publish-website:
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: './docs'
|
||||
- name: Setup Pages
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: "./docs"
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
# Runtime stage - using distroless for minimal attack surface
|
||||
# Dockerfile for GoReleaser dockers_v2
|
||||
# GoReleaser organizes binaries by platform: linux/amd64/kubemirror, linux/arm64/kubemirror
|
||||
FROM gcr.io/distroless/static:nonroot
|
||||
|
||||
ARG TARGETPLATFORM
|
||||
WORKDIR /
|
||||
|
||||
# Copy the binary from goreleaser build
|
||||
COPY kubemirror /kubemirror
|
||||
|
||||
# Use nonroot user (65532)
|
||||
COPY ${TARGETPLATFORM}/kubemirror /kubemirror
|
||||
USER 65532:65532
|
||||
|
||||
ENTRYPOINT ["/kubemirror"]
|
||||
|
||||
@@ -17,8 +17,10 @@ Tested in production environments managing 1000+ mirrors across 200+ namespaces
|
||||
- [Usage Examples](#usage-examples)
|
||||
- [Mirror a Secret to Specific Namespaces](#mirror-a-secret-to-specific-namespaces)
|
||||
- [Mirror to Pattern-Matched Namespaces](#mirror-to-pattern-matched-namespaces)
|
||||
- [Mirror to All Namespaces](#mirror-to-all-namespaces)
|
||||
- [Mirror to All Labeled Namespaces](#mirror-to-all-labeled-namespaces)
|
||||
- [Mirror Custom Resources (CRDs)](#mirror-custom-resources-crds)
|
||||
- [Using with ExternalSecrets Operator](#using-with-externalsecrets-operator)
|
||||
- [Configuration](#configuration)
|
||||
- [Helm Chart Values](#helm-chart-values)
|
||||
- [Command-line Flags](#command-line-flags)
|
||||
@@ -65,9 +67,9 @@ KubeMirror solves this with:
|
||||
| **Resources** | Mirror any Kubernetes resource type - Secrets, ConfigMaps, Ingresses, Services, CRDs, and more |
|
||||
| **Resources** | Auto-discovery of all mirrorable resources with periodic refresh |
|
||||
| **Resources** | Safety deny list prevents mirroring dangerous resources (Pods, Events, Nodes) |
|
||||
| **Targeting** | Mirror to specific namespaces, pattern-matched namespaces (`app-*`), or all labeled namespaces |
|
||||
| **Targeting** | Mirror to specific namespaces, patterns (`app-*`), `all` namespaces, or `all-labeled` (opt-in) |
|
||||
| **Targeting** | Configurable maximum targets per source (default: 100) |
|
||||
| **Targeting** | Namespace opt-in required for "all-labeled" mirrors |
|
||||
| **Targeting** | `all-labeled` requires namespace opt-in via `kubemirror.raczylo.com/allow-mirrors` label |
|
||||
| **Sync** | Multi-layer change detection: generation field + SHA256 content hash |
|
||||
| **Sync** | Automatic drift detection and correction for manually modified mirrors |
|
||||
| **Sync** | Finalizer-based cleanup ensures mirrors are deleted with source |
|
||||
@@ -207,8 +209,33 @@ data:
|
||||
api_url: "https://api.example.com"
|
||||
```
|
||||
|
||||
### Mirror to All Namespaces
|
||||
|
||||
Use the `all` keyword to mirror to every namespace in the cluster (except the source):
|
||||
|
||||
**Source Resource:**
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: global-config
|
||||
namespace: default
|
||||
labels:
|
||||
kubemirror.raczylo.com/enabled: "true"
|
||||
annotations:
|
||||
kubemirror.raczylo.com/sync: "true"
|
||||
kubemirror.raczylo.com/target-namespaces: "all"
|
||||
data:
|
||||
cluster_name: "production"
|
||||
region: "us-west-2"
|
||||
```
|
||||
|
||||
> **⚠️ Use with caution:** The `all` keyword mirrors to ALL namespaces (including kube-system, kube-public, etc.) except the source namespace. Consider using `all-labeled` for safer opt-in behavior.
|
||||
|
||||
### Mirror to All Labeled Namespaces
|
||||
|
||||
Use `all-labeled` for opt-in mirroring where target namespaces must explicitly allow mirrors:
|
||||
|
||||
**Source Resource:**
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
@@ -265,6 +292,88 @@ spec:
|
||||
- text/event-stream
|
||||
```
|
||||
|
||||
### Using with ExternalSecrets Operator
|
||||
|
||||
KubeMirror works seamlessly with the [ExternalSecrets Operator](https://external-secrets.io/) to distribute secrets from external stores (like 1Password, Vault, AWS Secrets Manager) across multiple namespaces.
|
||||
|
||||
**Example - Distribute Docker Registry Credentials from 1Password:**
|
||||
|
||||
```yaml
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: 1p-docker-config
|
||||
namespace: default
|
||||
spec:
|
||||
# Pull secrets from 1Password/Vault/etc
|
||||
secretStoreRef:
|
||||
kind: ClusterSecretStore
|
||||
name: 1password-homecluster
|
||||
|
||||
target:
|
||||
creationPolicy: Owner # Standard ExternalSecrets setting - KubeMirror strips ownerReferences from mirrors
|
||||
deletionPolicy: Retain
|
||||
name: multi-registry-secret
|
||||
|
||||
# Include KubeMirror annotations in the secret template
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
kubemirror.raczylo.com/enabled: "true"
|
||||
annotations:
|
||||
kubemirror.raczylo.com/sync: "true"
|
||||
kubemirror.raczylo.com/target-namespaces: "all" # or specific namespaces
|
||||
|
||||
type: kubernetes.io/dockerconfigjson
|
||||
data:
|
||||
.dockerconfigjson: |
|
||||
{
|
||||
"auths": {
|
||||
"ghcr.io": {
|
||||
"username": "{{ .ghcrUsername | toString }}",
|
||||
"auth": "{{ printf "%s:%s" .ghcrUsername .ghcrPassword | b64enc }}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data:
|
||||
- remoteRef:
|
||||
key: DockerAuth/ghcrio_username
|
||||
secretKey: ghcrUsername
|
||||
- remoteRef:
|
||||
key: DockerAuth/ghcrio_password
|
||||
secretKey: ghcrPassword
|
||||
|
||||
refreshInterval: 24h
|
||||
```
|
||||
|
||||
**How it Works:**
|
||||
|
||||
1. **ExternalSecrets creates the source secret** with KubeMirror labels/annotations (source can be owned by any controller)
|
||||
2. **KubeMirror detects the source** via the `kubemirror.raczylo.com/enabled` label
|
||||
3. **KubeMirror creates mirrors** in target namespaces with:
|
||||
- Labels identifying them as KubeMirror-managed mirrors
|
||||
- Annotations linking back to the source (namespace, name, UID, content hash)
|
||||
- **No ownerReferences** - preventing conflicts with source controllers
|
||||
4. **ExternalSecrets refreshes the source** every 24h, updating only the source secret
|
||||
5. **KubeMirror detects content changes** via hash comparison and updates all mirrors
|
||||
6. Each controller manages its own resources independently - no conflicts
|
||||
|
||||
**Verification:**
|
||||
|
||||
```bash
|
||||
# Check source secret was created by ExternalSecrets
|
||||
kubectl get secret multi-registry-secret -n default -o jsonpath='{.metadata.annotations}'
|
||||
|
||||
# Verify mirrors were created by KubeMirror
|
||||
kubectl get secrets --all-namespaces -l kubemirror.raczylo.com/mirror=true
|
||||
|
||||
# Check sync status on source
|
||||
kubectl get secret multi-registry-secret -n default -o jsonpath='{.metadata.annotations.kubemirror\.raczylo\.com/sync-status}'
|
||||
```
|
||||
|
||||
See [examples/externalsecret-dockerconfig.yaml](examples/externalsecret-dockerconfig.yaml) for a complete working example.
|
||||
|
||||
### Transformation Rules
|
||||
|
||||
KubeMirror supports powerful transformation rules that modify resources during mirroring. This enables environment-specific configurations, security hardening, and dynamic value generation.
|
||||
|
||||
+270
-301
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="scroll-smooth">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
@@ -20,34 +20,100 @@
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
}
|
||||
.glass {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
.hover-lift {
|
||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
||||
}
|
||||
.hover-lift:hover {
|
||||
transform: translateY(-5px);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.code-block {
|
||||
background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
|
||||
}
|
||||
|
||||
/* Fade-in animation */
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeInUp 0.6s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.delay-100 { animation-delay: 0.1s; }
|
||||
.delay-200 { animation-delay: 0.2s; }
|
||||
.delay-300 { animation-delay: 0.3s; }
|
||||
|
||||
/* Scroll progress bar */
|
||||
.progress-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 4px;
|
||||
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||||
z-index: 9999;
|
||||
transition: width 0.1s ease-out;
|
||||
}
|
||||
|
||||
/* Mobile menu animation */
|
||||
.mobile-menu {
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.mobile-menu.active {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* Smooth hover glow */
|
||||
.glow-on-hover {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.glow-on-hover::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
transform: translate(-50%, -50%);
|
||||
transition: width 0.6s, height 0.6s;
|
||||
}
|
||||
|
||||
.glow-on-hover:hover::before {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gradient-to-br from-slate-50 to-blue-50">
|
||||
<!-- Scroll Progress Bar -->
|
||||
<div class="progress-bar" id="progressBar"></div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="bg-white/80 backdrop-blur-md shadow-lg sticky top-0 z-50 border-b border-blue-100">
|
||||
<nav class="bg-white/90 backdrop-blur-lg shadow-lg sticky top-0 z-50 border-b border-blue-100">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between h-16 items-center">
|
||||
<a href="#" class="flex items-center gap-3 group">
|
||||
<div class="bg-gradient-to-br from-blue-500 to-purple-600 p-2 rounded-lg group-hover:scale-110 transition-transform">
|
||||
<div class="bg-gradient-to-br from-blue-500 to-purple-600 p-2 rounded-lg group-hover:scale-110 transition-transform duration-300">
|
||||
<i class="fas fa-copy text-2xl text-white"></i>
|
||||
</div>
|
||||
<span class="text-2xl font-bold gradient-text">KubeMirror</span>
|
||||
</a>
|
||||
|
||||
<!-- Desktop Menu -->
|
||||
<div class="hidden md:flex space-x-8">
|
||||
<a href="#problem" class="text-slate-700 hover:text-blue-600 font-medium transition-colors">Problem</a>
|
||||
<a href="#features" class="text-slate-700 hover:text-blue-600 font-medium transition-colors">Features</a>
|
||||
@@ -55,34 +121,50 @@
|
||||
<a href="#comparison" class="text-slate-700 hover:text-blue-600 font-medium transition-colors">Compare</a>
|
||||
<a href="#installation" class="text-slate-700 hover:text-blue-600 font-medium transition-colors">Install</a>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-4">
|
||||
<a href="https://github.com/lukaszraczylo/kubemirror" target="_blank" class="text-slate-700 hover:text-blue-600 transition-colors">
|
||||
<i class="fab fa-github text-2xl"></i>
|
||||
</a>
|
||||
<!-- Mobile Menu Button -->
|
||||
<button id="mobileMenuBtn" class="md:hidden text-slate-700 hover:text-blue-600">
|
||||
<i class="fas fa-bars text-2xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Mobile Menu -->
|
||||
<div id="mobileMenu" class="mobile-menu fixed top-16 right-0 w-64 h-full bg-white shadow-2xl z-40 md:hidden">
|
||||
<div class="flex flex-col p-6 space-y-4">
|
||||
<a href="#problem" class="text-slate-700 hover:text-blue-600 font-medium transition-colors py-2">Problem</a>
|
||||
<a href="#features" class="text-slate-700 hover:text-blue-600 font-medium transition-colors py-2">Features</a>
|
||||
<a href="#examples" class="text-slate-700 hover:text-blue-600 font-medium transition-colors py-2">Examples</a>
|
||||
<a href="#comparison" class="text-slate-700 hover:text-blue-600 font-medium transition-colors py-2">Compare</a>
|
||||
<a href="#installation" class="text-slate-700 hover:text-blue-600 font-medium transition-colors py-2">Install</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<section class="relative overflow-hidden py-24">
|
||||
<div class="absolute inset-0 bg-gradient-to-br from-blue-50 via-purple-50 to-pink-50 opacity-70"></div>
|
||||
<div class="relative max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
|
||||
<div class="mb-8 inline-block">
|
||||
<div class="mb-8 inline-block fade-in">
|
||||
<div class="bg-gradient-to-br from-blue-500 to-purple-600 p-6 rounded-2xl shadow-2xl">
|
||||
<i class="fas fa-copy text-7xl text-white"></i>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="text-6xl font-extrabold text-slate-900 mb-6 leading-tight">
|
||||
<h1 class="text-5xl md:text-6xl font-extrabold text-slate-900 mb-6 leading-tight fade-in delay-100">
|
||||
Copy Kubernetes Resources<br/>
|
||||
<span class="gradient-text">Across Namespaces</span>
|
||||
</h1>
|
||||
<p class="text-2xl text-slate-600 mb-10 max-w-3xl mx-auto leading-relaxed">
|
||||
<p class="text-xl md:text-2xl text-slate-600 mb-10 max-w-3xl mx-auto leading-relaxed fade-in delay-200">
|
||||
Share Secrets, ConfigMaps, and any Custom Resource (like Traefik Middleware, Cert-Manager Certificates) across multiple namespaces.
|
||||
<strong>Automatically keep them in sync.</strong> Transform values per environment.
|
||||
</p>
|
||||
<div class="flex justify-center gap-6">
|
||||
<a href="#installation" class="bg-gradient-to-r from-blue-600 to-purple-600 text-white px-10 py-4 rounded-xl font-bold text-lg hover:from-blue-700 hover:to-purple-700 transition-all shadow-lg hover:shadow-xl hover:scale-105">
|
||||
<div class="flex flex-col sm:flex-row justify-center gap-6 fade-in delay-300">
|
||||
<a href="#installation" class="bg-gradient-to-r from-blue-600 to-purple-600 text-white px-10 py-4 rounded-xl font-bold text-lg hover:from-blue-700 hover:to-purple-700 transition-all shadow-lg hover:shadow-xl hover:scale-105 glow-on-hover">
|
||||
<i class="fas fa-download mr-2"></i>
|
||||
Get Started
|
||||
</a>
|
||||
@@ -98,8 +180,8 @@
|
||||
<section id="problem" class="py-24 bg-white">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-5xl font-extrabold text-slate-900 mb-6">The Problem</h2>
|
||||
<p class="text-2xl text-slate-600 max-w-4xl mx-auto">
|
||||
<h2 class="text-4xl md:text-5xl font-extrabold text-slate-900 mb-6">The Problem</h2>
|
||||
<p class="text-xl md:text-2xl text-slate-600 max-w-4xl mx-auto">
|
||||
Kubernetes doesn't let you share resources across namespaces. You need the same Secret or ConfigMap in 10 namespaces? You have to duplicate it manually and keep them all in sync.
|
||||
</p>
|
||||
</div>
|
||||
@@ -136,13 +218,13 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gradient-to-br from-green-50 to-emerald-100 border-l-4 border-green-500 p-10 rounded-xl shadow-xl">
|
||||
<div class="bg-gradient-to-br from-green-50 to-emerald-100 border-l-4 border-green-500 p-10 rounded-xl shadow-xl hover-lift">
|
||||
<div class="flex items-start gap-4">
|
||||
<i class="fas fa-check-circle text-green-500 text-4xl mt-1"></i>
|
||||
<div>
|
||||
<h3 class="font-bold text-3xl text-slate-900 mb-4">KubeMirror's Solution</h3>
|
||||
<p class="text-slate-700 text-xl leading-relaxed">
|
||||
Define your resource once in a source namespace. KubeMirror automatically copies it to all target namespaces and keeps them synchronized.
|
||||
<h3 class="font-bold text-2xl md:text-3xl text-slate-900 mb-4">KubeMirror's Solution</h3>
|
||||
<p class="text-slate-700 text-lg md:text-xl leading-relaxed">
|
||||
Define your resource once in a source namespace. KubeMirror automatically copies it to target namespaces (specific list, patterns like <code class="bg-white px-3 py-1 rounded font-mono text-purple-600 font-semibold">app-*</code>, or <code class="bg-white px-3 py-1 rounded font-mono text-purple-600 font-semibold">all</code>) and keeps them synchronized.
|
||||
Transform values per environment (e.g., <code class="bg-white px-3 py-1 rounded font-mono text-purple-600 font-semibold">preprod-*</code> namespaces get preprod API URLs, <code class="bg-white px-3 py-1 rounded font-mono text-purple-600 font-semibold">prod-*</code> get production URLs).
|
||||
Works with any Kubernetes resource type.
|
||||
</p>
|
||||
@@ -156,17 +238,17 @@
|
||||
<section id="features" class="py-24 bg-gradient-to-br from-slate-50 to-blue-50">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-20">
|
||||
<h2 class="text-5xl font-extrabold text-slate-900 mb-4">Key Features</h2>
|
||||
<h2 class="text-4xl md:text-5xl font-extrabold text-slate-900 mb-4">Key Features</h2>
|
||||
<p class="text-xl text-slate-600">Everything you need for resource mirroring and synchronization</p>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-10">
|
||||
<!-- Any Resource Type -->
|
||||
<div class="bg-white p-10 rounded-2xl shadow-xl hover-lift border border-blue-100">
|
||||
<div class="bg-white p-8 md:p-10 rounded-2xl shadow-xl hover-lift border border-blue-100">
|
||||
<div class="bg-gradient-to-br from-blue-500 to-purple-600 w-16 h-16 rounded-xl flex items-center justify-center mb-6">
|
||||
<i class="fas fa-layer-group text-3xl text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-3xl font-bold text-slate-900 mb-4">Mirror Any Resource Type</h3>
|
||||
<h3 class="text-2xl md:text-3xl font-bold text-slate-900 mb-4">Mirror Any Resource Type</h3>
|
||||
<p class="text-slate-600 mb-6 text-lg">
|
||||
Not just Secrets and ConfigMaps. Mirror any namespaced Kubernetes resource:
|
||||
</p>
|
||||
@@ -184,15 +266,15 @@
|
||||
</div>
|
||||
|
||||
<!-- Transformation Rules -->
|
||||
<div class="bg-white p-10 rounded-2xl shadow-xl hover-lift border border-purple-100">
|
||||
<div class="bg-white p-8 md:p-10 rounded-2xl shadow-xl hover-lift border border-purple-100">
|
||||
<div class="bg-gradient-to-br from-purple-500 to-pink-600 w-16 h-16 rounded-xl flex items-center justify-center mb-6">
|
||||
<i class="fas fa-magic text-3xl text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-3xl font-bold text-slate-900 mb-4">Transform Per Environment</h3>
|
||||
<h3 class="text-2xl md:text-3xl font-bold text-slate-900 mb-4">Transform Per Environment</h3>
|
||||
<p class="text-slate-600 mb-6 text-lg">
|
||||
Change values automatically based on target namespace:
|
||||
</p>
|
||||
<div class="code-block text-gray-100 p-5 rounded-xl font-mono text-sm overflow-x-auto mb-4 shadow-lg">
|
||||
<div class="code-block text-gray-100 p-5 rounded-xl font-mono text-xs md:text-sm overflow-x-auto mb-4 shadow-lg">
|
||||
<pre class="text-green-400"># Preprod namespaces get preprod API</pre>
|
||||
<pre><span class="text-yellow-400">- path:</span> data.API_URL
|
||||
<span class="text-yellow-400">value:</span> <span class="text-blue-400">"https://preprod.api.com"</span>
|
||||
@@ -211,11 +293,11 @@
|
||||
</div>
|
||||
|
||||
<!-- Automatic Sync -->
|
||||
<div class="bg-white p-10 rounded-2xl shadow-xl hover-lift border border-green-100">
|
||||
<div class="bg-white p-8 md:p-10 rounded-2xl shadow-xl hover-lift border border-green-100">
|
||||
<div class="bg-gradient-to-br from-green-500 to-teal-600 w-16 h-16 rounded-xl flex items-center justify-center mb-6">
|
||||
<i class="fas fa-sync-alt text-3xl text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-3xl font-bold text-slate-900 mb-4">Automatic Synchronization</h3>
|
||||
<h3 class="text-2xl md:text-3xl font-bold text-slate-900 mb-4">Automatic Synchronization</h3>
|
||||
<p class="text-slate-600 mb-6 text-lg">
|
||||
Update the source once. All copies update automatically:
|
||||
</p>
|
||||
@@ -233,26 +315,30 @@
|
||||
</div>
|
||||
|
||||
<!-- Smart Targeting -->
|
||||
<div class="bg-white p-10 rounded-2xl shadow-xl hover-lift border border-orange-100">
|
||||
<div class="bg-white p-8 md:p-10 rounded-2xl shadow-xl hover-lift border border-orange-100">
|
||||
<div class="bg-gradient-to-br from-orange-500 to-red-600 w-16 h-16 rounded-xl flex items-center justify-center mb-6">
|
||||
<i class="fas fa-bullseye text-3xl text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-3xl font-bold text-slate-900 mb-4">Flexible Targeting</h3>
|
||||
<h3 class="text-2xl md:text-3xl font-bold text-slate-900 mb-4">Flexible Targeting</h3>
|
||||
<p class="text-slate-600 mb-6 text-lg">
|
||||
Choose which namespaces receive the copy:
|
||||
</p>
|
||||
<div class="space-y-4 text-slate-700 text-lg">
|
||||
<div class="flex items-center gap-3">
|
||||
<code class="bg-slate-100 px-4 py-2 rounded-lg font-mono text-purple-700 font-semibold">namespace-1,namespace-2</code>
|
||||
<div class="space-y-4 text-slate-700 text-base md:text-lg">
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-3">
|
||||
<code class="bg-slate-100 px-4 py-2 rounded-lg font-mono text-purple-700 font-semibold text-sm">namespace-1,namespace-2</code>
|
||||
<span>Specific namespaces</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<code class="bg-slate-100 px-4 py-2 rounded-lg font-mono text-purple-700 font-semibold">app-*,prod-*</code>
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-3">
|
||||
<code class="bg-slate-100 px-4 py-2 rounded-lg font-mono text-purple-700 font-semibold text-sm">app-*,prod-*</code>
|
||||
<span>Pattern matching</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<code class="bg-slate-100 px-4 py-2 rounded-lg font-mono text-purple-700 font-semibold">all-labeled</code>
|
||||
<span>All labeled namespaces</span>
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-3">
|
||||
<code class="bg-slate-100 px-4 py-2 rounded-lg font-mono text-purple-700 font-semibold text-sm">all</code>
|
||||
<span>All namespaces (no labels required)</span>
|
||||
</div>
|
||||
<div class="flex flex-col sm:flex-row items-start sm:items-center gap-3">
|
||||
<code class="bg-slate-100 px-4 py-2 rounded-lg font-mono text-purple-700 font-semibold text-sm">all-labeled</code>
|
||||
<span>Only namespaces with opt-in label</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 p-4 bg-orange-50 rounded-lg border border-orange-200">
|
||||
@@ -269,26 +355,26 @@
|
||||
<section id="examples" class="py-24 bg-white">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-20">
|
||||
<h2 class="text-5xl font-extrabold text-slate-900 mb-4">Real-World Examples</h2>
|
||||
<h2 class="text-4xl md:text-5xl font-extrabold text-slate-900 mb-4">Real-World Examples</h2>
|
||||
<p class="text-xl text-slate-600">See how easy it is to get started with KubeMirror</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-12">
|
||||
<!-- Example 1: Basic Secret -->
|
||||
<div class="bg-gradient-to-br from-blue-50 to-indigo-50 p-10 rounded-2xl shadow-xl border border-blue-200">
|
||||
<div class="flex items-start gap-6 mb-6">
|
||||
<div class="bg-gradient-to-br from-blue-50 to-indigo-50 p-8 md:p-10 rounded-2xl shadow-xl border border-blue-200 hover-lift">
|
||||
<div class="flex flex-col sm:flex-row items-start gap-6 mb-6">
|
||||
<div class="bg-blue-600 w-14 h-14 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<span class="text-white font-bold text-2xl">1</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-3xl font-bold text-slate-900 mb-3">
|
||||
<h3 class="text-2xl md:text-3xl font-bold text-slate-900 mb-3">
|
||||
<i class="fas fa-lock text-blue-600 mr-3"></i>
|
||||
Basic: Mirror a TLS Secret
|
||||
</h3>
|
||||
<p class="text-slate-600 text-lg">Share your TLS certificate across multiple application namespaces</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-block text-gray-100 p-6 rounded-xl font-mono text-sm overflow-x-auto shadow-lg">
|
||||
<div class="code-block text-gray-100 p-4 md:p-6 rounded-xl font-mono text-xs md:text-sm overflow-x-auto shadow-lg">
|
||||
<pre><span class="text-blue-400">apiVersion:</span> v1
|
||||
<span class="text-blue-400">kind:</span> Secret
|
||||
<span class="text-blue-400">metadata:</span>
|
||||
@@ -307,20 +393,20 @@
|
||||
</div>
|
||||
|
||||
<!-- Example 2: Pattern Matching -->
|
||||
<div class="bg-gradient-to-br from-purple-50 to-pink-50 p-10 rounded-2xl shadow-xl border border-purple-200">
|
||||
<div class="flex items-start gap-6 mb-6">
|
||||
<div class="bg-gradient-to-br from-purple-50 to-pink-50 p-8 md:p-10 rounded-2xl shadow-xl border border-purple-200 hover-lift">
|
||||
<div class="flex flex-col sm:flex-row items-start gap-6 mb-6">
|
||||
<div class="bg-purple-600 w-14 h-14 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<span class="text-white font-bold text-2xl">2</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-3xl font-bold text-slate-900 mb-3">
|
||||
<h3 class="text-2xl md:text-3xl font-bold text-slate-900 mb-3">
|
||||
<i class="fas fa-asterisk text-purple-600 mr-3"></i>
|
||||
Pattern Matching: Mirror to All App Namespaces
|
||||
</h3>
|
||||
<p class="text-slate-600 text-lg">Use wildcards to mirror to all namespaces matching a pattern</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-block text-gray-100 p-6 rounded-xl font-mono text-sm overflow-x-auto shadow-lg">
|
||||
<div class="code-block text-gray-100 p-4 md:p-6 rounded-xl font-mono text-xs md:text-sm overflow-x-auto shadow-lg">
|
||||
<pre><span class="text-blue-400">apiVersion:</span> v1
|
||||
<span class="text-blue-400">kind:</span> ConfigMap
|
||||
<span class="text-blue-400">metadata:</span>
|
||||
@@ -337,28 +423,28 @@
|
||||
<span class="text-yellow-400">api_url:</span> <span class="text-purple-400">"https://api.example.com"</span></pre>
|
||||
</div>
|
||||
<div class="mt-6 p-5 bg-purple-100 rounded-lg border border-purple-300">
|
||||
<p class="text-slate-700 text-lg">
|
||||
<p class="text-slate-700 text-base md:text-lg">
|
||||
<i class="fas fa-info-circle text-purple-600 mr-2"></i>
|
||||
<strong>Result:</strong> This ConfigMap will be automatically copied to <code class="bg-white px-2 py-1 rounded font-mono text-purple-700">app-frontend</code>, <code class="bg-white px-2 py-1 rounded font-mono text-purple-700">app-backend</code>, <code class="bg-white px-2 py-1 rounded font-mono text-purple-700">app-worker</code>, and any other namespace starting with "app-"
|
||||
<strong>Result:</strong> This ConfigMap will be automatically copied to <code class="bg-white px-2 py-1 rounded font-mono text-purple-700 text-sm">app-frontend</code>, <code class="bg-white px-2 py-1 rounded font-mono text-purple-700 text-sm">app-backend</code>, <code class="bg-white px-2 py-1 rounded font-mono text-purple-700 text-sm">app-worker</code>, and any other namespace starting with "app-"
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Example 3: Custom Resource (Traefik) -->
|
||||
<div class="bg-gradient-to-br from-green-50 to-teal-50 p-10 rounded-2xl shadow-xl border border-green-200">
|
||||
<div class="flex items-start gap-6 mb-6">
|
||||
<div class="bg-gradient-to-br from-green-50 to-teal-50 p-8 md:p-10 rounded-2xl shadow-xl border border-green-200 hover-lift">
|
||||
<div class="flex flex-col sm:flex-row items-start gap-6 mb-6">
|
||||
<div class="bg-green-600 w-14 h-14 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<span class="text-white font-bold text-2xl">3</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-3xl font-bold text-slate-900 mb-3">
|
||||
<h3 class="text-2xl md:text-3xl font-bold text-slate-900 mb-3">
|
||||
<i class="fas fa-cubes text-green-600 mr-3"></i>
|
||||
Custom Resource: Share Traefik Middleware
|
||||
</h3>
|
||||
<p class="text-slate-600 text-lg">Mirror any CRD like Traefik Middleware across your cluster</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-block text-gray-100 p-6 rounded-xl font-mono text-sm overflow-x-auto shadow-lg">
|
||||
<div class="code-block text-gray-100 p-4 md:p-6 rounded-xl font-mono text-xs md:text-sm overflow-x-auto shadow-lg">
|
||||
<pre><span class="text-blue-400">apiVersion:</span> traefik.io/v1alpha1
|
||||
<span class="text-blue-400">kind:</span> Middleware
|
||||
<span class="text-blue-400">metadata:</span>
|
||||
@@ -376,130 +462,12 @@
|
||||
- text/event-stream</pre>
|
||||
</div>
|
||||
<div class="mt-6 p-5 bg-green-100 rounded-lg border border-green-300">
|
||||
<p class="text-slate-700 text-lg">
|
||||
<p class="text-slate-700 text-base md:text-lg">
|
||||
<i class="fas fa-lightbulb text-green-600 mr-2"></i>
|
||||
<strong>Works with any CRD:</strong> Cert-Manager Certificates, Gateway API resources, or your own custom resources!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Example 4: Transformation -->
|
||||
<div class="bg-gradient-to-br from-orange-50 to-red-50 p-10 rounded-2xl shadow-xl border border-orange-200">
|
||||
<div class="flex items-start gap-6 mb-6">
|
||||
<div class="bg-orange-600 w-14 h-14 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<span class="text-white font-bold text-2xl">4</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-3xl font-bold text-slate-900 mb-3">
|
||||
<i class="fas fa-code-branch text-orange-600 mr-3"></i>
|
||||
Advanced: Environment-Specific Configuration
|
||||
</h3>
|
||||
<p class="text-slate-600 text-lg">Transform values based on target environment using namespace patterns</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-block text-gray-100 p-6 rounded-xl font-mono text-sm overflow-x-auto shadow-lg">
|
||||
<pre><span class="text-blue-400">apiVersion:</span> v1
|
||||
<span class="text-blue-400">kind:</span> ConfigMap
|
||||
<span class="text-blue-400">metadata:</span>
|
||||
<span class="text-yellow-400">name:</span> app-config
|
||||
<span class="text-yellow-400">namespace:</span> default
|
||||
<span class="text-yellow-400">labels:</span>
|
||||
<span class="text-green-400">kubemirror.raczylo.com/enabled:</span> <span class="text-purple-400">"true"</span>
|
||||
<span class="text-yellow-400">annotations:</span>
|
||||
<span class="text-green-400">kubemirror.raczylo.com/sync:</span> <span class="text-purple-400">"true"</span>
|
||||
<span class="text-green-400">kubemirror.raczylo.com/target-namespaces:</span> <span class="text-purple-400">"preprod-api,prod-api,staging-api"</span>
|
||||
<span class="text-green-400">kubemirror.raczylo.com/transform:</span> |
|
||||
<span class="text-yellow-400">rules:</span>
|
||||
<span class="text-pink-400"># Preprod gets preprod GraphQL endpoint</span>
|
||||
- <span class="text-yellow-400">path:</span> data.GRAPHQL_HOST
|
||||
<span class="text-yellow-400">value:</span> <span class="text-purple-400">"https://preprod.example.com/v1/graphql"</span>
|
||||
<span class="text-yellow-400">namespacePattern:</span> <span class="text-purple-400">"preprod-*"</span>
|
||||
|
||||
<span class="text-pink-400"># Production gets production endpoint</span>
|
||||
- <span class="text-yellow-400">path:</span> data.GRAPHQL_HOST
|
||||
<span class="text-yellow-400">value:</span> <span class="text-purple-400">"https://api.example.com/v1/graphql"</span>
|
||||
<span class="text-yellow-400">namespacePattern:</span> <span class="text-purple-400">"prod-*"</span>
|
||||
|
||||
<span class="text-pink-400"># Set log level to error in production</span>
|
||||
- <span class="text-yellow-400">path:</span> data.LOG_LEVEL
|
||||
<span class="text-yellow-400">value:</span> <span class="text-purple-400">"error"</span>
|
||||
<span class="text-yellow-400">namespacePattern:</span> <span class="text-purple-400">"prod-*"</span>
|
||||
<span class="text-blue-400">data:</span>
|
||||
<span class="text-yellow-400">GRAPHQL_HOST:</span> <span class="text-purple-400">"https://localhost/v1/graphql"</span>
|
||||
<span class="text-yellow-400">LOG_LEVEL:</span> <span class="text-purple-400">"debug"</span></pre>
|
||||
</div>
|
||||
<div class="mt-6 p-5 bg-orange-100 rounded-lg border border-orange-300">
|
||||
<p class="text-slate-700 text-lg">
|
||||
<i class="fas fa-magic text-orange-600 mr-2"></i>
|
||||
<strong>Result:</strong> Each environment gets the right configuration automatically. One source, multiple environments, zero manual work!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Example 5: Go Templates -->
|
||||
<div class="bg-gradient-to-br from-pink-50 to-rose-50 p-10 rounded-2xl shadow-xl border border-pink-200">
|
||||
<div class="flex items-start gap-6 mb-6">
|
||||
<div class="bg-pink-600 w-14 h-14 rounded-xl flex items-center justify-center flex-shrink-0">
|
||||
<span class="text-white font-bold text-2xl">5</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-3xl font-bold text-slate-900 mb-3">
|
||||
<i class="fas fa-code text-pink-600 mr-3"></i>
|
||||
Go Templates: Dynamic Value Generation
|
||||
</h3>
|
||||
<p class="text-slate-600 text-lg">Use Go template functions to generate namespace-specific values dynamically</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="code-block text-gray-100 p-6 rounded-xl font-mono text-sm overflow-x-auto shadow-lg">
|
||||
<pre><span class="text-blue-400">apiVersion:</span> v1
|
||||
<span class="text-blue-400">kind:</span> ConfigMap
|
||||
<span class="text-blue-400">metadata:</span>
|
||||
<span class="text-yellow-400">name:</span> dynamic-config
|
||||
<span class="text-yellow-400">namespace:</span> default
|
||||
<span class="text-yellow-400">labels:</span>
|
||||
<span class="text-green-400">kubemirror.raczylo.com/enabled:</span> <span class="text-purple-400">"true"</span>
|
||||
<span class="text-yellow-400">annotations:</span>
|
||||
<span class="text-green-400">kubemirror.raczylo.com/sync:</span> <span class="text-purple-400">"true"</span>
|
||||
<span class="text-green-400">kubemirror.raczylo.com/target-namespaces:</span> <span class="text-purple-400">"app-frontend,app-backend,app-worker"</span>
|
||||
<span class="text-green-400">kubemirror.raczylo.com/transform:</span> |
|
||||
<span class="text-yellow-400">rules:</span>
|
||||
<span class="text-pink-400"># Generate namespace-specific API URL</span>
|
||||
- <span class="text-yellow-400">path:</span> data.API_URL
|
||||
<span class="text-yellow-400">template:</span> <span class="text-purple-400">"https://{{.TargetNamespace}}.api.example.com"</span>
|
||||
|
||||
<span class="text-pink-400"># Generate database connection string</span>
|
||||
- <span class="text-yellow-400">path:</span> data.DATABASE_URL
|
||||
<span class="text-yellow-400">template:</span> <span class="text-purple-400">"postgres://{{.TargetNamespace}}.db.svc:5432/app"</span>
|
||||
|
||||
<span class="text-pink-400"># Use string manipulation functions</span>
|
||||
- <span class="text-yellow-400">path:</span> data.CACHE_PREFIX
|
||||
<span class="text-yellow-400">template:</span> <span class="text-purple-400">"{{replace .TargetNamespace \"-\" \"_\"}}"</span>
|
||||
|
||||
<span class="text-pink-400"># Uppercase namespace for environment variable</span>
|
||||
- <span class="text-yellow-400">path:</span> data.NAMESPACE_UPPER
|
||||
<span class="text-yellow-400">template:</span> <span class="text-purple-400">"{{upper .TargetNamespace}}"</span>
|
||||
<span class="text-blue-400">data:</span>
|
||||
<span class="text-yellow-400">API_URL:</span> <span class="text-purple-400">"https://localhost:8080"</span>
|
||||
<span class="text-yellow-400">DATABASE_URL:</span> <span class="text-purple-400">"postgres://localhost:5432/app"</span>
|
||||
<span class="text-yellow-400">CACHE_PREFIX:</span> <span class="text-purple-400">"default"</span>
|
||||
<span class="text-yellow-400">NAMESPACE_UPPER:</span> <span class="text-purple-400">"DEFAULT"</span></pre>
|
||||
</div>
|
||||
<div class="mt-6 p-5 bg-pink-100 rounded-lg border border-pink-300">
|
||||
<p class="text-slate-700 text-lg mb-4">
|
||||
<i class="fas fa-info-circle text-pink-600 mr-2"></i>
|
||||
<strong>Available Template Variables:</strong>
|
||||
</p>
|
||||
<div class="grid md:grid-cols-2 gap-3 text-sm font-mono">
|
||||
<div><code class="bg-white px-2 py-1 rounded text-pink-700">.TargetNamespace</code> - Target namespace name</div>
|
||||
<div><code class="bg-white px-2 py-1 rounded text-pink-700">.SourceNamespace</code> - Source namespace name</div>
|
||||
<div><code class="bg-white px-2 py-1 rounded text-pink-700">.SourceName</code> - Source resource name</div>
|
||||
<div><code class="bg-white px-2 py-1 rounded text-pink-700">.TargetName</code> - Mirror resource name</div>
|
||||
</div>
|
||||
<p class="text-slate-700 text-lg mt-4">
|
||||
<strong>Template Functions:</strong> <code class="bg-white px-2 py-1 rounded font-mono text-pink-700">upper</code>, <code class="bg-white px-2 py-1 rounded font-mono text-pink-700">lower</code>, <code class="bg-white px-2 py-1 rounded font-mono text-pink-700">replace</code>, <code class="bg-white px-2 py-1 rounded font-mono text-pink-700">trimPrefix</code>, <code class="bg-white px-2 py-1 rounded font-mono text-pink-700">trimSuffix</code>, <code class="bg-white px-2 py-1 rounded font-mono text-pink-700">default</code>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -508,102 +476,74 @@
|
||||
<section id="comparison" class="py-24 bg-gradient-to-br from-slate-50 to-blue-50">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-16">
|
||||
<h2 class="text-5xl font-extrabold text-slate-900 mb-6">How KubeMirror Compares</h2>
|
||||
<p class="text-2xl text-slate-600">We built KubeMirror to replace <a href="https://github.com/emberstack/kubernetes-reflector" class="text-blue-600 hover:underline font-semibold" target="_blank">emberstack/reflector</a></p>
|
||||
<h2 class="text-4xl md:text-5xl font-extrabold text-slate-900 mb-6">How KubeMirror Compares</h2>
|
||||
<p class="text-xl md:text-2xl text-slate-600">We built KubeMirror to replace <a href="https://github.com/emberstack/kubernetes-reflector" class="text-blue-600 hover:underline font-semibold" target="_blank">emberstack/reflector</a></p>
|
||||
</div>
|
||||
|
||||
<div class="overflow-x-auto rounded-2xl shadow-2xl">
|
||||
<table class="w-full bg-white">
|
||||
<thead class="bg-gradient-to-r from-slate-800 to-slate-900 text-white">
|
||||
<tr>
|
||||
<th class="px-8 py-6 text-left font-bold text-lg">Capability</th>
|
||||
<th class="px-8 py-6 text-center font-bold text-lg">KubeMirror</th>
|
||||
<th class="px-8 py-6 text-center font-bold text-lg">Reflector</th>
|
||||
<th class="px-6 md:px-8 py-6 text-left font-bold text-base md:text-lg">Capability</th>
|
||||
<th class="px-6 md:px-8 py-6 text-center font-bold text-base md:text-lg">KubeMirror</th>
|
||||
<th class="px-6 md:px-8 py-6 text-center font-bold text-base md:text-lg">Reflector</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-200">
|
||||
<tr class="hover:bg-blue-50 transition-colors">
|
||||
<td class="px-8 py-6">
|
||||
<div class="font-semibold text-lg text-slate-900">Supported Resources</div>
|
||||
<td class="px-6 md:px-8 py-6">
|
||||
<div class="font-semibold text-base md:text-lg text-slate-900">Supported Resources</div>
|
||||
<div class="text-sm text-slate-600 mt-1">What resource types can be mirrored</div>
|
||||
</td>
|
||||
<td class="px-8 py-6 text-center">
|
||||
<div><i class="fas fa-check-circle text-green-500 text-3xl"></i></div>
|
||||
<div class="text-sm font-semibold text-green-700 mt-2">Secrets, ConfigMaps, CRDs, etc.</div>
|
||||
<td class="px-6 md:px-8 py-6 text-center">
|
||||
<div><i class="fas fa-check-circle text-green-500 text-2xl md:text-3xl"></i></div>
|
||||
<div class="text-xs md:text-sm font-semibold text-green-700 mt-2">Secrets, ConfigMaps, CRDs, etc.</div>
|
||||
</td>
|
||||
<td class="px-8 py-6 text-center">
|
||||
<div><i class="fas fa-minus-circle text-yellow-500 text-3xl"></i></div>
|
||||
<div class="text-sm font-semibold text-yellow-700 mt-2">Secrets, ConfigMaps only</div>
|
||||
<td class="px-6 md:px-8 py-6 text-center">
|
||||
<div><i class="fas fa-minus-circle text-yellow-500 text-2xl md:text-3xl"></i></div>
|
||||
<div class="text-xs md:text-sm font-semibold text-yellow-700 mt-2">Secrets, ConfigMaps only</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-blue-50 transition-colors bg-slate-50">
|
||||
<td class="px-8 py-6">
|
||||
<div class="font-semibold text-lg text-slate-900">Auto-Discovery</div>
|
||||
<td class="px-6 md:px-8 py-6">
|
||||
<div class="font-semibold text-base md:text-lg text-slate-900">Auto-Discovery</div>
|
||||
<div class="text-sm text-slate-600 mt-1">Finds all resource types automatically</div>
|
||||
</td>
|
||||
<td class="px-8 py-6 text-center">
|
||||
<div><i class="fas fa-check-circle text-green-500 text-3xl"></i></div>
|
||||
<div class="text-sm font-semibold text-green-700 mt-2">Yes</div>
|
||||
<td class="px-6 md:px-8 py-6 text-center">
|
||||
<div><i class="fas fa-check-circle text-green-500 text-2xl md:text-3xl"></i></div>
|
||||
<div class="text-xs md:text-sm font-semibold text-green-700 mt-2">Yes</div>
|
||||
</td>
|
||||
<td class="px-8 py-6 text-center">
|
||||
<div><i class="fas fa-times-circle text-red-500 text-3xl"></i></div>
|
||||
<div class="text-sm font-semibold text-red-700 mt-2">Hardcoded</div>
|
||||
<td class="px-6 md:px-8 py-6 text-center">
|
||||
<div><i class="fas fa-times-circle text-red-500 text-2xl md:text-3xl"></i></div>
|
||||
<div class="text-xs md:text-sm font-semibold text-red-700 mt-2">Hardcoded</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-blue-50 transition-colors">
|
||||
<td class="px-8 py-6">
|
||||
<div class="font-semibold text-lg text-slate-900">Value Transformation</div>
|
||||
<td class="px-6 md:px-8 py-6">
|
||||
<div class="font-semibold text-base md:text-lg text-slate-900">Value Transformation</div>
|
||||
<div class="text-sm text-slate-600 mt-1">Change values per target namespace</div>
|
||||
</td>
|
||||
<td class="px-8 py-6 text-center">
|
||||
<div><i class="fas fa-check-circle text-green-500 text-3xl"></i></div>
|
||||
<div class="text-sm font-semibold text-green-700 mt-2">Full support</div>
|
||||
<td class="px-6 md:px-8 py-6 text-center">
|
||||
<div><i class="fas fa-check-circle text-green-500 text-2xl md:text-3xl"></i></div>
|
||||
<div class="text-xs md:text-sm font-semibold text-green-700 mt-2">Full support</div>
|
||||
</td>
|
||||
<td class="px-8 py-6 text-center">
|
||||
<div><i class="fas fa-times-circle text-red-500 text-3xl"></i></div>
|
||||
<div class="text-sm font-semibold text-red-700 mt-2">Not available</div>
|
||||
<td class="px-6 md:px-8 py-6 text-center">
|
||||
<div><i class="fas fa-times-circle text-red-500 text-2xl md:text-3xl"></i></div>
|
||||
<div class="text-xs md:text-sm font-semibold text-red-700 mt-2">Not available</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-blue-50 transition-colors bg-slate-50">
|
||||
<td class="px-8 py-6">
|
||||
<div class="font-semibold text-lg text-slate-900">Environment-Specific Rules</div>
|
||||
<div class="text-sm text-slate-600 mt-1">Different transforms for dev/staging/prod</div>
|
||||
</td>
|
||||
<td class="px-8 py-6 text-center">
|
||||
<div><i class="fas fa-check-circle text-green-500 text-3xl"></i></div>
|
||||
<div class="text-sm font-semibold text-green-700 mt-2">Namespace patterns</div>
|
||||
</td>
|
||||
<td class="px-8 py-6 text-center">
|
||||
<div><i class="fas fa-times-circle text-red-500 text-3xl"></i></div>
|
||||
<div class="text-sm font-semibold text-red-700 mt-2">Not available</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-blue-50 transition-colors">
|
||||
<td class="px-8 py-6">
|
||||
<div class="font-semibold text-lg text-slate-900">API Efficiency</div>
|
||||
<div class="text-sm text-slate-600 mt-1">Impact on Kubernetes API server</div>
|
||||
</td>
|
||||
<td class="px-8 py-6 text-center">
|
||||
<div><i class="fas fa-check-circle text-green-500 text-3xl"></i></div>
|
||||
<div class="text-sm font-semibold text-green-700 mt-2">90% less load</div>
|
||||
</td>
|
||||
<td class="px-8 py-6 text-center">
|
||||
<div><i class="fas fa-minus-circle text-yellow-500 text-3xl"></i></div>
|
||||
<div class="text-sm font-semibold text-yellow-700 mt-2">Higher load</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="hover:bg-blue-50 transition-colors bg-slate-50">
|
||||
<td class="px-8 py-6">
|
||||
<div class="font-semibold text-lg text-slate-900">Active Development</div>
|
||||
<td class="px-6 md:px-8 py-6">
|
||||
<div class="font-semibold text-base md:text-lg text-slate-900">Active Development</div>
|
||||
<div class="text-sm text-slate-600 mt-1">Regular updates and bug fixes</div>
|
||||
</td>
|
||||
<td class="px-8 py-6 text-center">
|
||||
<div><i class="fas fa-check-circle text-green-500 text-3xl"></i></div>
|
||||
<div class="text-sm font-semibold text-green-700 mt-2">Active</div>
|
||||
<td class="px-6 md:px-8 py-6 text-center">
|
||||
<div><i class="fas fa-check-circle text-green-500 text-2xl md:text-3xl"></i></div>
|
||||
<div class="text-xs md:text-sm font-semibold text-green-700 mt-2">Active</div>
|
||||
</td>
|
||||
<td class="px-8 py-6 text-center">
|
||||
<div><i class="fas fa-check-circle text-green-500 text-3xl"></i></div>
|
||||
<div class="text-sm font-semibold text-green-700 mt-2">Recently resumed (2025)</div>
|
||||
<td class="px-6 md:px-8 py-6 text-center">
|
||||
<div><i class="fas fa-check-circle text-green-500 text-2xl md:text-3xl"></i></div>
|
||||
<div class="text-xs md:text-sm font-semibold text-green-700 mt-2">Recently resumed (2025)</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -611,11 +551,11 @@
|
||||
</div>
|
||||
|
||||
<div class="mt-16 bg-gradient-to-br from-blue-50 to-indigo-100 border-l-4 border-blue-600 p-8 rounded-xl shadow-xl">
|
||||
<div class="flex items-start gap-6">
|
||||
<i class="fas fa-info-circle text-blue-600 text-4xl mt-1"></i>
|
||||
<div class="flex flex-col sm:flex-row items-start gap-6">
|
||||
<i class="fas fa-info-circle text-blue-600 text-3xl md:text-4xl mt-1"></i>
|
||||
<div>
|
||||
<h4 class="text-2xl font-bold text-slate-900 mb-4">Why We Built KubeMirror</h4>
|
||||
<p class="text-slate-700 text-lg leading-relaxed">
|
||||
<h4 class="text-xl md:text-2xl font-bold text-slate-900 mb-4">Why We Built KubeMirror</h4>
|
||||
<p class="text-slate-700 text-base md:text-lg leading-relaxed">
|
||||
We needed to share Traefik Middleware across 200+ namespaces with environment-specific configurations.
|
||||
Reflector couldn't do it (Secrets/ConfigMaps only, no transformations). So we built KubeMirror with modern
|
||||
Kubernetes best practices and all the features we wished Reflector had.
|
||||
@@ -630,20 +570,20 @@
|
||||
<section id="installation" class="py-24 bg-white">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-20">
|
||||
<h2 class="text-5xl font-extrabold text-slate-900 mb-4">Installation</h2>
|
||||
<p class="text-2xl text-slate-600">Get started in under 2 minutes</p>
|
||||
<h2 class="text-4xl md:text-5xl font-extrabold text-slate-900 mb-4">Installation</h2>
|
||||
<p class="text-xl md:text-2xl text-slate-600">Get started in under 2 minutes</p>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-10 mb-16">
|
||||
<!-- Helm Installation -->
|
||||
<div class="bg-gradient-to-br from-blue-50 to-indigo-50 p-10 rounded-2xl shadow-xl border border-blue-200 hover-lift">
|
||||
<div class="bg-gradient-to-br from-blue-50 to-indigo-50 p-8 md:p-10 rounded-2xl shadow-xl border border-blue-200 hover-lift">
|
||||
<div class="flex items-center mb-6">
|
||||
<div class="bg-blue-600 w-14 h-14 rounded-xl flex items-center justify-center mr-4">
|
||||
<i class="fas fa-ship text-2xl text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-3xl font-bold text-slate-900">Helm <span class="text-blue-600">(Recommended)</span></h3>
|
||||
<h3 class="text-2xl md:text-3xl font-bold text-slate-900">Helm <span class="text-blue-600">(Recommended)</span></h3>
|
||||
</div>
|
||||
<div class="code-block text-gray-100 p-6 rounded-xl font-mono text-sm overflow-x-auto shadow-lg">
|
||||
<div class="code-block text-gray-100 p-4 md:p-6 rounded-xl font-mono text-xs md:text-sm overflow-x-auto shadow-lg">
|
||||
<pre><span class="text-green-400">helm repo add kubemirror \</span>
|
||||
https://lukaszraczylo.github.io/helm-charts
|
||||
|
||||
@@ -655,14 +595,14 @@
|
||||
</div>
|
||||
|
||||
<!-- kubectl Installation -->
|
||||
<div class="bg-gradient-to-br from-purple-50 to-pink-50 p-10 rounded-2xl shadow-xl border border-purple-200 hover-lift">
|
||||
<div class="bg-gradient-to-br from-purple-50 to-pink-50 p-8 md:p-10 rounded-2xl shadow-xl border border-purple-200 hover-lift">
|
||||
<div class="flex items-center mb-6">
|
||||
<div class="bg-purple-600 w-14 h-14 rounded-xl flex items-center justify-center mr-4">
|
||||
<i class="fas fa-terminal text-2xl text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-3xl font-bold text-slate-900">kubectl</h3>
|
||||
<h3 class="text-2xl md:text-3xl font-bold text-slate-900">kubectl</h3>
|
||||
</div>
|
||||
<div class="code-block text-gray-100 p-6 rounded-xl font-mono text-sm overflow-x-auto shadow-lg">
|
||||
<div class="code-block text-gray-100 p-4 md:p-6 rounded-xl font-mono text-xs md:text-sm overflow-x-auto shadow-lg">
|
||||
<pre><span class="text-green-400">kubectl apply -k \</span>
|
||||
github.com/lukaszraczylo/kubemirror/deploy
|
||||
|
||||
@@ -674,8 +614,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Quick Start Example -->
|
||||
<div class="bg-gradient-to-br from-green-50 to-teal-50 p-12 rounded-2xl shadow-2xl border border-green-200">
|
||||
<h3 class="text-4xl font-bold text-slate-900 mb-8 text-center">
|
||||
<div class="bg-gradient-to-br from-green-50 to-teal-50 p-8 md:p-12 rounded-2xl shadow-2xl border border-green-200">
|
||||
<h3 class="text-3xl md:text-4xl font-bold text-slate-900 mb-8 text-center">
|
||||
<i class="fas fa-rocket text-green-600 mr-3"></i>
|
||||
Quick Start: Mirror a Secret in 30 Seconds
|
||||
</h3>
|
||||
@@ -684,9 +624,9 @@
|
||||
<div>
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="bg-green-600 w-10 h-10 rounded-full flex items-center justify-center text-white font-bold">1</div>
|
||||
<h4 class="font-bold text-2xl text-slate-900">Create your source Secret</h4>
|
||||
<h4 class="font-bold text-xl md:text-2xl text-slate-900">Create your source Secret</h4>
|
||||
</div>
|
||||
<div class="code-block text-gray-100 p-6 rounded-xl font-mono text-sm overflow-x-auto shadow-lg">
|
||||
<div class="code-block text-gray-100 p-4 md:p-6 rounded-xl font-mono text-xs md:text-sm overflow-x-auto shadow-lg">
|
||||
<pre><span class="text-blue-400">apiVersion:</span> v1
|
||||
<span class="text-blue-400">kind:</span> Secret
|
||||
<span class="text-blue-400">metadata:</span>
|
||||
@@ -707,15 +647,15 @@
|
||||
<div>
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<div class="bg-green-600 w-10 h-10 rounded-full flex items-center justify-center text-white font-bold">2</div>
|
||||
<h4 class="font-bold text-2xl text-slate-900">That's it!</h4>
|
||||
<h4 class="font-bold text-xl md:text-2xl text-slate-900">That's it!</h4>
|
||||
</div>
|
||||
<p class="text-slate-700 mb-6 text-lg">
|
||||
<p class="text-slate-700 mb-6 text-base md:text-lg">
|
||||
KubeMirror automatically:
|
||||
</p>
|
||||
<ul class="text-slate-700 space-y-4 text-lg">
|
||||
<ul class="text-slate-700 space-y-4 text-base md:text-lg">
|
||||
<li class="flex items-start gap-3">
|
||||
<i class="fas fa-check-circle text-green-500 text-xl mt-1"></i>
|
||||
<span>Creates copies in <code class="bg-white px-2 py-1 rounded font-mono text-green-700">app-1</code> and <code class="bg-white px-2 py-1 rounded font-mono text-green-700">app-2</code></span>
|
||||
<span>Creates copies in <code class="bg-white px-2 py-1 rounded font-mono text-green-700 text-sm">app-1</code> and <code class="bg-white px-2 py-1 rounded font-mono text-green-700 text-sm">app-2</code></span>
|
||||
</li>
|
||||
<li class="flex items-start gap-3">
|
||||
<i class="fas fa-check-circle text-green-500 text-xl mt-1"></i>
|
||||
@@ -732,8 +672,8 @@
|
||||
</ul>
|
||||
<div class="mt-8 p-5 bg-green-100 rounded-lg border border-green-300">
|
||||
<p class="text-sm text-slate-700">
|
||||
<strong class="text-green-800">Required:</strong> Both the label <code class="bg-white px-2 py-1 rounded font-mono text-green-700">kubemirror.raczylo.com/enabled</code>
|
||||
and annotation <code class="bg-white px-2 py-1 rounded font-mono text-green-700">kubemirror.raczylo.com/sync</code> are needed.
|
||||
<strong class="text-green-800">Required:</strong> Both the label <code class="bg-white px-2 py-1 rounded font-mono text-green-700 text-xs">kubemirror.raczylo.com/enabled</code>
|
||||
and annotation <code class="bg-white px-2 py-1 rounded font-mono text-green-700 text-xs">kubemirror.raczylo.com/sync</code> are needed.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -742,42 +682,6 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Documentation Section -->
|
||||
<section class="py-24 bg-gradient-to-br from-slate-50 to-blue-50">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="text-center mb-20">
|
||||
<h2 class="text-5xl font-extrabold text-slate-900 mb-4">Documentation</h2>
|
||||
<p class="text-xl text-slate-600">Everything you need to know</p>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-3 gap-10">
|
||||
<a href="https://github.com/lukaszraczylo/kubemirror#usage-examples" target="_blank" class="bg-white p-10 rounded-2xl shadow-xl hover-lift border border-blue-100 group">
|
||||
<div class="bg-gradient-to-br from-blue-500 to-purple-600 w-16 h-16 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
|
||||
<i class="fas fa-book-open text-3xl text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-slate-900 mb-3 group-hover:text-blue-600 transition-colors">Usage Examples</h3>
|
||||
<p class="text-slate-600 text-lg">Real-world examples for common use cases</p>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/lukaszraczylo/kubemirror/tree/main/examples#transformation-rules" target="_blank" class="bg-white p-10 rounded-2xl shadow-xl hover-lift border border-purple-100 group">
|
||||
<div class="bg-gradient-to-br from-purple-500 to-pink-600 w-16 h-16 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
|
||||
<i class="fas fa-magic text-3xl text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-slate-900 mb-3 group-hover:text-purple-600 transition-colors">Transformation Rules</h3>
|
||||
<p class="text-slate-600 text-lg">Change values based on target environment</p>
|
||||
</a>
|
||||
|
||||
<a href="https://github.com/lukaszraczylo/kubemirror#configuration" target="_blank" class="bg-white p-10 rounded-2xl shadow-xl hover-lift border border-green-100 group">
|
||||
<div class="bg-gradient-to-br from-green-500 to-teal-600 w-16 h-16 rounded-xl flex items-center justify-center mb-6 group-hover:scale-110 transition-transform">
|
||||
<i class="fas fa-cog text-3xl text-white"></i>
|
||||
</div>
|
||||
<h3 class="text-2xl font-bold text-slate-900 mb-3 group-hover:text-green-600 transition-colors">Configuration</h3>
|
||||
<p class="text-slate-600 text-lg">Helm values and advanced settings</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-gradient-to-br from-slate-900 to-slate-800 text-gray-300 py-16">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
@@ -789,25 +693,90 @@
|
||||
</div>
|
||||
<span class="text-2xl font-bold text-white">KubeMirror</span>
|
||||
</div>
|
||||
<p class="text-gray-400 text-lg leading-relaxed">
|
||||
<p class="text-gray-400 text-base md:text-lg leading-relaxed">
|
||||
Copy Kubernetes resources across namespaces. Modern replacement for Reflector.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-xl font-bold text-white mb-6">Links</h4>
|
||||
<ul class="space-y-3 text-lg">
|
||||
<h4 class="text-lg md:text-xl font-bold text-white mb-6">Links</h4>
|
||||
<ul class="space-y-3 text-base md:text-lg">
|
||||
<li><a href="https://github.com/lukaszraczylo/kubemirror" target="_blank" class="hover:text-white transition-colors"><i class="fab fa-github mr-2"></i>GitHub</a></li>
|
||||
<li><a href="https://github.com/lukaszraczylo/kubemirror/issues" target="_blank" class="hover:text-white transition-colors"><i class="fas fa-bug mr-2"></i>Report Issue</a></li>
|
||||
<li><a href="https://github.com/lukaszraczylo/kubemirror/releases" target="_blank" class="hover:text-white transition-colors"><i class="fas fa-tag mr-2"></i>Releases</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="text-xl font-bold text-white mb-6">License</h4>
|
||||
<p class="text-gray-400 text-lg">MIT License</p>
|
||||
<p class="text-gray-400 mt-4 text-lg">© 2024 Lukasz Raczylo</p>
|
||||
<h4 class="text-lg md:text-xl font-bold text-white mb-6">License</h4>
|
||||
<p class="text-gray-400 text-base md:text-lg">MIT License</p>
|
||||
<p class="text-gray-400 mt-4 text-base md:text-lg">© 2024 Lukasz Raczylo</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Back to Top Button -->
|
||||
<button id="backToTop" class="fixed bottom-8 right-8 bg-gradient-to-r from-blue-600 to-purple-600 text-white p-4 rounded-full shadow-2xl opacity-0 pointer-events-none transition-opacity duration-300 hover:scale-110 z-50">
|
||||
<i class="fas fa-arrow-up text-xl"></i>
|
||||
</button>
|
||||
|
||||
<script>
|
||||
// Scroll progress bar
|
||||
window.addEventListener('scroll', () => {
|
||||
const winScroll = document.body.scrollTop || document.documentElement.scrollTop;
|
||||
const height = document.documentElement.scrollHeight - document.documentElement.clientHeight;
|
||||
const scrolled = (winScroll / height) * 100;
|
||||
document.getElementById('progressBar').style.width = scrolled + '%';
|
||||
});
|
||||
|
||||
// Mobile menu toggle
|
||||
const mobileMenuBtn = document.getElementById('mobileMenuBtn');
|
||||
const mobileMenu = document.getElementById('mobileMenu');
|
||||
|
||||
mobileMenuBtn.addEventListener('click', () => {
|
||||
mobileMenu.classList.toggle('active');
|
||||
});
|
||||
|
||||
// Close mobile menu when clicking a link
|
||||
document.querySelectorAll('#mobileMenu a').forEach(link => {
|
||||
link.addEventListener('click', () => {
|
||||
mobileMenu.classList.remove('active');
|
||||
});
|
||||
});
|
||||
|
||||
// Close mobile menu when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!mobileMenu.contains(e.target) && !mobileMenuBtn.contains(e.target)) {
|
||||
mobileMenu.classList.remove('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Back to top button
|
||||
const backToTop = document.getElementById('backToTop');
|
||||
|
||||
window.addEventListener('scroll', () => {
|
||||
if (window.pageYOffset > 300) {
|
||||
backToTop.style.opacity = '1';
|
||||
backToTop.style.pointerEvents = 'auto';
|
||||
} else {
|
||||
backToTop.style.opacity = '0';
|
||||
backToTop.style.pointerEvents = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
backToTop.addEventListener('click', () => {
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
});
|
||||
|
||||
// Smooth scroll for anchor links
|
||||
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||||
anchor.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
const target = document.querySelector(this.getAttribute('href'));
|
||||
if (target) {
|
||||
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -249,6 +249,137 @@ data:
|
||||
config: "value"
|
||||
```
|
||||
|
||||
## ExternalSecrets Integration
|
||||
|
||||
KubeMirror integrates seamlessly with the [ExternalSecrets Operator](https://external-secrets.io/) to distribute secrets from external stores (1Password, Vault, AWS Secrets Manager, etc.) across multiple namespaces.
|
||||
|
||||
### Overview
|
||||
|
||||
The `externalsecret-dockerconfig.yaml` example demonstrates how to:
|
||||
1. Sync secrets from 1Password/Vault/etc using ExternalSecrets
|
||||
2. Mirror those secrets to multiple namespaces using KubeMirror
|
||||
3. Avoid race conditions between the two controllers
|
||||
|
||||
### Prerequisites
|
||||
|
||||
```bash
|
||||
# Install ExternalSecrets Operator
|
||||
helm repo add external-secrets https://charts.external-secrets.io
|
||||
helm install external-secrets external-secrets/external-secrets -n external-secrets-system --create-namespace
|
||||
|
||||
# Configure your ClusterSecretStore (example for 1Password)
|
||||
kubectl apply -f your-clustersecretstore.yaml
|
||||
```
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# Apply the ExternalSecret example
|
||||
kubectl apply -f examples/externalsecret-dockerconfig.yaml
|
||||
|
||||
# Verify the source secret was created
|
||||
kubectl get secret multi-registry-secret -n default
|
||||
|
||||
# Verify mirrors were created by KubeMirror
|
||||
kubectl get secrets --all-namespaces -l kubemirror.raczylo.com/mirror=true
|
||||
|
||||
# Check sync status
|
||||
kubectl get secret multi-registry-secret -n default \
|
||||
-o jsonpath='{.metadata.annotations.kubemirror\.raczylo\.com/sync-status}'
|
||||
```
|
||||
|
||||
### Key Configuration
|
||||
|
||||
**Ownership Model**
|
||||
|
||||
KubeMirror uses **labels and annotations** to manage mirrors, not ownerReferences. This allows it to work with sources managed by any controller (ExternalSecrets, ArgoCD, etc.):
|
||||
|
||||
```yaml
|
||||
target:
|
||||
creationPolicy: Owner # Source can be owned by ExternalSecrets
|
||||
name: multi-registry-secret
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
kubemirror.raczylo.com/enabled: "true" # KubeMirror detection
|
||||
annotations:
|
||||
kubemirror.raczylo.com/sync: "true"
|
||||
kubemirror.raczylo.com/target-namespaces: "all"
|
||||
```
|
||||
|
||||
**Separation of Concerns:**
|
||||
|
||||
- **Source Secret**: Owned by ExternalSecrets (or any other controller) via `ownerReferences`
|
||||
- **Mirror Secrets**: Managed by KubeMirror via labels + annotations (no ownerReferences copied)
|
||||
- **Relationship**: Mirrors link to source via annotations (`source-namespace`, `source-name`, `source-uid`)
|
||||
- **Result**: Each controller manages its own resources independently
|
||||
|
||||
### Examples in externalsecret-dockerconfig.yaml
|
||||
|
||||
The example file contains three complete examples:
|
||||
|
||||
1. **Docker Registry Credentials** - Mirrors Docker config to all namespaces using `target-namespaces: "all"`
|
||||
2. **Database Credentials** - Uses `all-labeled` for opt-in mirroring with namespace labels
|
||||
3. **Namespace with Opt-In Label** - Shows how to label namespaces to receive mirrors
|
||||
|
||||
### Verification
|
||||
|
||||
```bash
|
||||
# Check ExternalSecret status
|
||||
kubectl get externalsecret 1p-docker-config -n default
|
||||
|
||||
# View the created secret
|
||||
kubectl get secret multi-registry-secret -n default -o yaml
|
||||
|
||||
# Verify KubeMirror picked it up
|
||||
kubectl logs -n kubemirror-system -l app.kubernetes.io/name=kubemirror | grep multi-registry-secret
|
||||
|
||||
# Count how many namespaces received the mirror
|
||||
kubectl get secrets --all-namespaces -l kubemirror.raczylo.com/mirror=true \
|
||||
--field-selector metadata.name=multi-registry-secret | wc -l
|
||||
|
||||
# Check a specific mirror
|
||||
kubectl get secret multi-registry-secret -n production-app -o yaml
|
||||
```
|
||||
|
||||
### Testing Updates
|
||||
|
||||
Test that ExternalSecrets refreshes propagate to all mirrors:
|
||||
|
||||
```bash
|
||||
# Force ExternalSecret refresh
|
||||
kubectl annotate externalsecret 1p-docker-config -n default \
|
||||
force-sync="$(date +%s)" --overwrite
|
||||
|
||||
# Wait for ExternalSecret to sync (check status)
|
||||
kubectl get externalsecret 1p-docker-config -n default -w
|
||||
|
||||
# Verify mirrors are updated (check generation or content hash)
|
||||
kubectl get secret multi-registry-secret -n production-app \
|
||||
-o jsonpath='{.metadata.annotations.kubemirror\.raczylo\.com/source-content-hash}'
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Mirrors Not Created**
|
||||
- Verify the secret has `kubemirror.raczylo.com/enabled: "true"` label
|
||||
- Check that KubeMirror annotations are in the ExternalSecret template
|
||||
- View controller logs: `kubectl logs -n kubemirror-system -l app.kubernetes.io/name=kubemirror`
|
||||
|
||||
2. **ExternalSecret Not Syncing**
|
||||
- Check ExternalSecret status: `kubectl describe externalsecret <name> -n <namespace>`
|
||||
- Verify ClusterSecretStore is configured: `kubectl get clustersecretstore`
|
||||
- Check external-secrets-operator logs
|
||||
|
||||
### Alternative Backends
|
||||
|
||||
The example includes commented configurations for:
|
||||
- AWS Secrets Manager
|
||||
- HashiCorp Vault
|
||||
- Google Secret Manager
|
||||
|
||||
Uncomment and configure the ClusterSecretStore for your backend.
|
||||
|
||||
## Transformation Rules
|
||||
|
||||
KubeMirror supports transformation rules that modify resources during mirroring. This enables environment-specific configurations, security hardening, and dynamic value generation.
|
||||
|
||||
@@ -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
|
||||
@@ -150,6 +150,11 @@ func createUnstructuredMirror(source runtime.Object, targetNamespace, sourceHash
|
||||
mirror.SetGeneration(0)
|
||||
mirror.SetCreationTimestamp(metav1.Time{})
|
||||
mirror.SetFinalizers(nil) // Mirrors should not have finalizers
|
||||
// IMPORTANT: Mirrors should never have ownerReferences from source.
|
||||
// KubeMirror manages mirrors via labels/annotations, not ownership.
|
||||
// This allows sources to be owned by other controllers (ExternalSecrets, ArgoCD, etc.)
|
||||
// while KubeMirror independently manages the mirrors.
|
||||
mirror.SetOwnerReferences(nil)
|
||||
|
||||
return mirror, nil
|
||||
}
|
||||
@@ -318,6 +323,10 @@ func updateUnstructuredMirror(mirror, source runtime.Object, sourceHash string)
|
||||
// Ensure mirrors never have finalizers (even if they were added before this fix)
|
||||
m.SetFinalizers(nil)
|
||||
|
||||
// Ensure mirrors never have ownerReferences (clean up mirrors from before this fix)
|
||||
// KubeMirror uses labels/annotations for management, not ownerReferences
|
||||
m.SetOwnerReferences(nil)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -155,6 +155,140 @@ func TestCreateMirror_Unstructured(t *testing.T) {
|
||||
assert.Equal(t, "3", annotations[constants.AnnotationSourceGeneration])
|
||||
}
|
||||
|
||||
func TestCreateMirror_Unstructured_StripsOwnerReferences(t *testing.T) {
|
||||
// Create source with ownerReferences (e.g., managed by ExternalSecrets)
|
||||
source := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Secret",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "external-secret",
|
||||
"namespace": "default",
|
||||
"uid": "secret-uid-123",
|
||||
"resourceVersion": "100",
|
||||
"generation": int64(1),
|
||||
// Source has ownerReferences (e.g., set by ExternalSecrets operator)
|
||||
"ownerReferences": []interface{}{
|
||||
map[string]interface{}{
|
||||
"apiVersion": "external-secrets.io/v1",
|
||||
"kind": "ExternalSecret",
|
||||
"name": "1p-docker-config",
|
||||
"uid": "externalsecret-uid-456",
|
||||
"controller": true,
|
||||
},
|
||||
},
|
||||
// Source has finalizers
|
||||
"finalizers": []interface{}{
|
||||
"externalsecrets.external-secrets.io/externalsecret-cleanup",
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"password": "c2VjcmV0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mirror, err := CreateMirror(source, "target-ns")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, mirror)
|
||||
|
||||
uMirror, ok := mirror.(*unstructured.Unstructured)
|
||||
require.True(t, ok, "mirror should be Unstructured")
|
||||
|
||||
// CRITICAL: Verify ownerReferences are NOT copied to mirror
|
||||
ownerRefs := uMirror.GetOwnerReferences()
|
||||
assert.Nil(t, ownerRefs, "mirror should not have ownerReferences from source")
|
||||
|
||||
// CRITICAL: Verify finalizers are NOT copied to mirror
|
||||
finalizers := uMirror.GetFinalizers()
|
||||
assert.Nil(t, finalizers, "mirror should not have finalizers from source")
|
||||
|
||||
// Verify mirror is properly managed by KubeMirror via labels/annotations
|
||||
assert.Equal(t, constants.ControllerName, uMirror.GetLabels()[constants.LabelManagedBy])
|
||||
assert.Equal(t, "true", uMirror.GetLabels()[constants.LabelMirror])
|
||||
assert.Equal(t, "default", uMirror.GetAnnotations()[constants.AnnotationSourceNamespace])
|
||||
assert.Equal(t, "external-secret", uMirror.GetAnnotations()[constants.AnnotationSourceName])
|
||||
assert.Equal(t, "secret-uid-123", uMirror.GetAnnotations()[constants.AnnotationSourceUID])
|
||||
}
|
||||
|
||||
func TestUpdateMirror_Unstructured_ClearsOwnerReferences(t *testing.T) {
|
||||
// Create mirror that somehow has ownerReferences (e.g., from before the fix)
|
||||
mirror := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "traefik.io/v1alpha1",
|
||||
"kind": "Middleware",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test-middleware",
|
||||
"namespace": "target-ns",
|
||||
"labels": map[string]interface{}{
|
||||
constants.LabelManagedBy: constants.ControllerName,
|
||||
constants.LabelMirror: "true",
|
||||
},
|
||||
"annotations": map[string]interface{}{
|
||||
constants.AnnotationSourceNamespace: "default",
|
||||
constants.AnnotationSourceName: "test-middleware",
|
||||
constants.AnnotationSourceContentHash: "oldhash",
|
||||
},
|
||||
// Mirror has ownerReferences (from before fix or external modification)
|
||||
"ownerReferences": []interface{}{
|
||||
map[string]interface{}{
|
||||
"apiVersion": "external-secrets.io/v1",
|
||||
"kind": "ExternalSecret",
|
||||
"name": "1p-docker-config",
|
||||
"uid": "externalsecret-uid-456",
|
||||
},
|
||||
},
|
||||
// Mirror has finalizers (from before fix or external modification)
|
||||
"finalizers": []interface{}{
|
||||
"some-finalizer",
|
||||
},
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"basicAuth": map[string]interface{}{
|
||||
"secret": "old-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
source := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "traefik.io/v1alpha1",
|
||||
"kind": "Middleware",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test-middleware",
|
||||
"namespace": "default",
|
||||
"generation": int64(2),
|
||||
},
|
||||
"spec": map[string]interface{}{
|
||||
"basicAuth": map[string]interface{}{
|
||||
"secret": "new-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err := UpdateMirror(mirror, source)
|
||||
require.NoError(t, err)
|
||||
|
||||
// CRITICAL: Verify ownerReferences are cleared from mirror
|
||||
ownerRefs := mirror.GetOwnerReferences()
|
||||
assert.Nil(t, ownerRefs, "mirror should not have ownerReferences after update")
|
||||
|
||||
// CRITICAL: Verify finalizers are cleared from mirror
|
||||
finalizers := mirror.GetFinalizers()
|
||||
assert.Nil(t, finalizers, "mirror should not have finalizers after update")
|
||||
|
||||
// Verify spec was updated
|
||||
secret, found, err := unstructured.NestedString(mirror.Object, "spec", "basicAuth", "secret")
|
||||
require.NoError(t, err)
|
||||
require.True(t, found)
|
||||
assert.Equal(t, "new-secret", secret)
|
||||
|
||||
// Verify hash was updated
|
||||
assert.NotEqual(t, "oldhash", mirror.GetAnnotations()[constants.AnnotationSourceContentHash])
|
||||
}
|
||||
|
||||
func TestUpdateMirror_Secret(t *testing.T) {
|
||||
mirror := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
|
||||
@@ -120,6 +120,15 @@ func (r *SourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up orphaned mirrors (namespaces that no longer match the target criteria)
|
||||
orphanedCount, err := r.cleanupOrphanedMirrors(ctx, sourceObj, targetNamespaces)
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to cleanup orphaned mirrors")
|
||||
// Don't fail reconciliation for cleanup errors, just log them
|
||||
} else if orphanedCount > 0 {
|
||||
logger.Info("cleaned up orphaned mirrors", "count", orphanedCount)
|
||||
}
|
||||
|
||||
// Update status annotation with last sync info
|
||||
if err := r.updateLastSyncStatus(ctx, source, sourceObj, reconciledCount, errorCount); err != nil {
|
||||
logger.Error(err, "failed to update sync status")
|
||||
@@ -131,9 +140,9 @@ func (r *SourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
|
||||
"errors", errorCount,
|
||||
"total", len(targetNamespaces))
|
||||
|
||||
// Requeue if there were errors
|
||||
// Return error if there were errors (controller-runtime will automatically requeue with exponential backoff)
|
||||
if errorCount > 0 {
|
||||
return ctrl.Result{Requeue: true}, fmt.Errorf("failed to reconcile %d/%d mirrors", errorCount, len(targetNamespaces))
|
||||
return ctrl.Result{}, fmt.Errorf("failed to reconcile %d/%d mirrors", errorCount, len(targetNamespaces))
|
||||
}
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
@@ -293,6 +302,81 @@ func (r *SourceReconciler) deleteAllMirrors(ctx context.Context, sourceObj metav
|
||||
return nil
|
||||
}
|
||||
|
||||
// cleanupOrphanedMirrors removes mirrors that exist but are no longer in the target list.
|
||||
// This handles cases where target-namespaces annotation changes (e.g., "all" → "all-labeled" or "app-*" → "prod-*").
|
||||
func (r *SourceReconciler) cleanupOrphanedMirrors(ctx context.Context, sourceObj metav1.Object, targetNamespaces []string) (int, error) {
|
||||
logger := log.FromContext(ctx)
|
||||
|
||||
// List all namespaces
|
||||
allNamespaces, err := r.NamespaceLister.ListNamespaces(ctx)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to list namespaces: %w", err)
|
||||
}
|
||||
|
||||
// Get GVK from source object
|
||||
sourceUnstructured, ok := sourceObj.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("source object is not unstructured")
|
||||
}
|
||||
|
||||
// Create a set of target namespaces for quick lookup
|
||||
targetSet := make(map[string]bool)
|
||||
for _, ns := range targetNamespaces {
|
||||
targetSet[ns] = true
|
||||
}
|
||||
|
||||
var deletedCount int
|
||||
for _, ns := range allNamespaces {
|
||||
// Skip source namespace
|
||||
if ns == sourceObj.GetNamespace() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip if this namespace IS in the current target list
|
||||
if targetSet[ns] {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if a mirror exists in this namespace
|
||||
mirror := &unstructured.Unstructured{}
|
||||
mirror.SetGroupVersionKind(sourceUnstructured.GroupVersionKind())
|
||||
mirror.SetNamespace(ns)
|
||||
mirror.SetName(sourceObj.GetName())
|
||||
|
||||
err := r.Get(ctx, client.ObjectKey{Namespace: ns, Name: sourceObj.GetName()}, mirror)
|
||||
if errors.IsNotFound(err) {
|
||||
// No mirror exists, nothing to clean up
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to check for mirror", "namespace", ns)
|
||||
continue
|
||||
}
|
||||
|
||||
// Verify this is actually our mirror (not someone else's resource with the same name)
|
||||
if !IsManagedByUs(mirror) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Verify this mirror points to our source
|
||||
srcNs, srcName, _, found := GetSourceReference(mirror)
|
||||
if !found || srcNs != sourceObj.GetNamespace() || srcName != sourceObj.GetName() {
|
||||
continue
|
||||
}
|
||||
|
||||
// This is an orphaned mirror - delete it
|
||||
if err := r.Delete(ctx, mirror); err != nil {
|
||||
logger.Error(err, "failed to delete orphaned mirror", "namespace", ns)
|
||||
continue
|
||||
}
|
||||
|
||||
deletedCount++
|
||||
logger.V(1).Info("deleted orphaned mirror", "namespace", ns)
|
||||
}
|
||||
|
||||
return deletedCount, nil
|
||||
}
|
||||
|
||||
// resolveTargetNamespaces determines which namespaces should receive mirrors.
|
||||
func (r *SourceReconciler) resolveTargetNamespaces(ctx context.Context, sourceObj metav1.Object) ([]string, error) {
|
||||
annotations := sourceObj.GetAnnotations()
|
||||
|
||||
@@ -466,3 +466,88 @@ func BenchmarkResolveTargetNamespaces(b *testing.B) {
|
||||
_, _ = r.resolveTargetNamespaces(ctx, sourceObj)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceReconciler_cleanupOrphanedMirrors(t *testing.T) {
|
||||
// Setup: Source in default namespace with mirrors in app-1, app-2, app-3
|
||||
// Then target-namespaces changes to only app-1, app-2
|
||||
// Expect: app-3 mirror is deleted (orphaned)
|
||||
|
||||
sourceObj := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Secret",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test-secret",
|
||||
"namespace": "default",
|
||||
"uid": "source-uid-123",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Mock existing mirror in app-3 (will be orphaned)
|
||||
orphanedMirror := &unstructured.Unstructured{
|
||||
Object: map[string]interface{}{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Secret",
|
||||
"metadata": map[string]interface{}{
|
||||
"name": "test-secret",
|
||||
"namespace": "app-3",
|
||||
"labels": map[string]interface{}{
|
||||
constants.LabelManagedBy: constants.ControllerName,
|
||||
constants.LabelMirror: "true",
|
||||
},
|
||||
"annotations": map[string]interface{}{
|
||||
constants.AnnotationSourceNamespace: "default",
|
||||
constants.AnnotationSourceName: "test-secret",
|
||||
constants.AnnotationSourceUID: "source-uid-123",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
mockClient := new(MockClient)
|
||||
mockLister := new(MockNamespaceLister)
|
||||
|
||||
// Setup: all namespaces in cluster
|
||||
allNamespaces := []string{"default", "app-1", "app-2", "app-3", "prod-1"}
|
||||
mockLister.On("ListNamespaces", mock.Anything).Return(allNamespaces, nil)
|
||||
|
||||
// Current target list (after annotation change): only app-1 and app-2
|
||||
targetNamespaces := []string{"app-1", "app-2"}
|
||||
|
||||
// The function will iterate through all namespaces and:
|
||||
// - Skip "default" (source namespace)
|
||||
// - Skip "app-1" and "app-2" (in target list)
|
||||
// - Check "app-3" (not in target list) → will find orphaned mirror
|
||||
// - Check "prod-1" (not in target list) → no mirror exists
|
||||
|
||||
notFoundErr := errors.NewNotFound(schema.GroupResource{Group: "", Resource: "secrets"}, "test-secret")
|
||||
|
||||
// app-3: orphaned mirror exists
|
||||
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "app-3", Name: "test-secret"}, mock.Anything).
|
||||
Return(nil, orphanedMirror)
|
||||
|
||||
// prod-1: no mirror exists
|
||||
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "prod-1", Name: "test-secret"}, mock.Anything).
|
||||
Return(notFoundErr, nil)
|
||||
|
||||
// Expect delete call for app-3 mirror
|
||||
mockClient.On("Delete", mock.Anything, mock.MatchedBy(func(obj client.Object) bool {
|
||||
u, ok := obj.(*unstructured.Unstructured)
|
||||
return ok && u.GetNamespace() == "app-3" && u.GetName() == "test-secret"
|
||||
}), mock.Anything).Return(nil)
|
||||
|
||||
r := &SourceReconciler{
|
||||
Client: mockClient,
|
||||
NamespaceLister: mockLister,
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
deletedCount, err := r.cleanupOrphanedMirrors(ctx, sourceObj, targetNamespaces)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, deletedCount, "should have deleted 1 orphaned mirror")
|
||||
|
||||
mockClient.AssertExpectations(t)
|
||||
mockLister.AssertExpectations(t)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user