mirror of
https://github.com/lukaszraczylo/kubemirror.git
synced 2026-07-08 22:31:50 +00:00
fix: Mirrored resources managed by other operators.
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
+6
-2
@@ -224,7 +224,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<h3 class="font-bold text-2xl md: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-lg md: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>
|
||||||
@@ -332,9 +332,13 @@
|
|||||||
<code class="bg-slate-100 px-4 py-2 rounded-lg font-mono text-purple-700 font-semibold text-sm">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 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">
|
<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>
|
<code class="bg-slate-100 px-4 py-2 rounded-lg font-mono text-purple-700 font-semibold text-sm">all-labeled</code>
|
||||||
<span>All labeled namespaces</span>
|
<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">
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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.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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -131,9 +131,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
|
||||||
|
|||||||
Reference in New Issue
Block a user