mirror of
https://github.com/lukaszraczylo/kubemirror.git
synced 2026-07-22 12:49:28 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
441b05def5 | ||
|
|
4c0af68f60 | ||
|
|
a0e37ec5d8 | ||
|
|
e0ff007e2a | ||
|
|
390e199ae0 | ||
|
|
5919e14c18 | ||
|
|
fc97f34cea | ||
|
|
fb1fa9e255 | ||
|
|
ebb2365a88 | ||
|
|
3b9637bd37 | ||
|
|
ae90f09aef | ||
|
|
8bcce1bff8 | ||
|
|
7ce5b71406 | ||
|
|
3a713a6388 | ||
|
|
8adec76ce8 | ||
|
|
6a41222b34 | ||
|
|
28896ab1d6 | ||
|
|
6d27d7dfe7 | ||
|
|
330666336f | ||
|
|
3628929871 | ||
|
|
c8ca8c41b6 | ||
|
|
e5aa084c14 | ||
|
|
417258630a |
@@ -271,6 +271,34 @@ metadata:
|
||||
kubemirror.raczylo.com/allow-mirrors: "true"
|
||||
```
|
||||
|
||||
### Pause or Exclude a Resource
|
||||
|
||||
Two annotations control opting a source out of mirroring:
|
||||
|
||||
- `kubemirror.raczylo.com/exclude: "true"` — opt the resource out entirely. It is
|
||||
not mirrored, and any mirrors it previously created are **deleted**.
|
||||
- `kubemirror.raczylo.com/paused: "true"` — **freeze** the resource. Existing
|
||||
mirrors are left exactly as they are (no updates, no cleanup) until the
|
||||
annotation is removed. Useful during maintenance when you want mirrors to
|
||||
persist but stop tracking source changes.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: shared-credentials
|
||||
namespace: default
|
||||
labels:
|
||||
kubemirror.raczylo.com/enabled: "true"
|
||||
annotations:
|
||||
kubemirror.raczylo.com/sync: "true"
|
||||
kubemirror.raczylo.com/target-namespaces: "all"
|
||||
# Freeze mirrors in place (or use /exclude to remove them):
|
||||
kubemirror.raczylo.com/paused: "true"
|
||||
data:
|
||||
password: c2VjcmV0
|
||||
```
|
||||
|
||||
### Mirror Custom Resources (CRDs)
|
||||
|
||||
KubeMirror works with any custom resource:
|
||||
@@ -555,12 +583,17 @@ Complete configuration reference:
|
||||
| **Resource Discovery** | | | |
|
||||
| `controller.resourceTypes` | Explicit resource type list (empty = auto-discover all) | `[]` | `["Secret.v1", "ConfigMap.v1", "Ingress.v1.networking.k8s.io"]` |
|
||||
| `controller.discoveryInterval` | Rediscovery interval for auto-discovery mode | `5m` | `10m`, `1h` |
|
||||
| `controller.lazyWatcherInit` | Only watch resource types in use; lowers memory | `false` | `true`, `false` |
|
||||
| `controller.watcherScanInterval` | Scan interval for new types in lazy mode | `5m` | `10m` |
|
||||
| **Performance & Limits** | | | |
|
||||
| `controller.leaderElect` | Enable leader election for HA | `true` | `true`, `false` |
|
||||
| `controller.leaderElectionID` | Leader election lease name | `kubemirror-controller-leader` | |
|
||||
| `controller.maxTargets` | Maximum mirrors per source resource | `100` | `50`, `200`, `500` |
|
||||
| `controller.workerThreads` | Concurrent reconciliation workers | `5` | `10`, `20` |
|
||||
| `controller.rateLimitQPS` | API rate limit (queries per second) | `50.0` | `100.0`, `200.0` |
|
||||
| `controller.rateLimitBurst` | API burst allowance | `100` | `200`, `500` |
|
||||
| `controller.resyncPeriod` | Full cache resync period | `10m` | `30m` |
|
||||
| `controller.verifySourceFreshness` | Verify cache against a direct API read before mirroring | `false` | `true`, `false` |
|
||||
| **Namespace Filtering** | | | |
|
||||
| `controller.excludedNamespaces` | Comma-separated namespace exclusion list | `""` | `kube-system,kube-public,kube-node-lease` |
|
||||
| `controller.includedNamespaces` | Comma-separated namespace inclusion list | `""` | `app-*,prod-*` |
|
||||
@@ -579,15 +612,19 @@ When running the binary directly:
|
||||
|
||||
**Resource Discovery:**
|
||||
- `--resource-types string` - Comma-separated list (e.g., `Secret.v1,ConfigMap.v1,Ingress.v1.networking.k8s.io`)
|
||||
- `--discovery-interval duration` - Rediscovery interval (default: 5m)
|
||||
- `--discovery-interval duration` - Rediscovery interval for auto-discovery mode (default: 5m)
|
||||
- `--lazy-watcher-init` - Only create watchers for resource types actually in use; lowers memory (default: false)
|
||||
- `--watcher-scan-interval duration` - Scan interval for new types in lazy mode (default: 5m)
|
||||
|
||||
**Performance & Limits:**
|
||||
- `--leader-elect` - Enable leader election (default: true)
|
||||
- `--leader-elect` - Enable leader election (default: false)
|
||||
- `--leader-election-id string` - Leader election lease name (default: kubemirror-controller-leader)
|
||||
- `--max-targets int` - Max mirrors per source (default: 100)
|
||||
- `--worker-threads int` - Concurrent workers (default: 5)
|
||||
- `--rate-limit-qps float32` - API rate limit (default: 50.0)
|
||||
- `--rate-limit-burst int` - API burst limit (default: 100)
|
||||
- `--verify-source-freshness` - Verify cache freshness before mirroring (default: false)
|
||||
- `--resync-period duration` - Full cache resync period (default: 10m)
|
||||
- `--verify-source-freshness` - Verify cache against a direct API read before mirroring (default: true)
|
||||
|
||||
**Namespace Filtering:**
|
||||
- `--excluded-namespaces string` - Comma-separated exclusion list
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# kubemirror Helm chart
|
||||
|
||||
Deploys the [kubemirror](https://github.com/lukaszraczylo/kubemirror) controller,
|
||||
which mirrors Kubernetes resources (Secrets, ConfigMaps, and other types) across
|
||||
namespaces.
|
||||
|
||||
- Chart version: `0.1.0`
|
||||
- App version: `0.1.0`
|
||||
- Image: `ghcr.io/lukaszraczylo/kubemirror`
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
helm install kubemirror ./charts/kubemirror -n kubemirror --create-namespace
|
||||
```
|
||||
|
||||
With a values override:
|
||||
|
||||
```bash
|
||||
helm install kubemirror ./charts/kubemirror -n kubemirror --create-namespace \
|
||||
-f my-values.yaml
|
||||
```
|
||||
|
||||
## Uninstall
|
||||
|
||||
```bash
|
||||
helm uninstall kubemirror -n kubemirror
|
||||
```
|
||||
|
||||
## What it creates
|
||||
|
||||
A single-replica `Deployment`, a `ServiceAccount`, a cluster-scoped `ClusterRole`
|
||||
+ `ClusterRoleBinding` (the controller watches resources across all namespaces),
|
||||
and a `Service` exposing the metrics (`8080`) and health (`8081`) ports. The
|
||||
container runs as non-root (uid `65532`) with a read-only root filesystem and all
|
||||
Linux capabilities dropped.
|
||||
|
||||
## Values
|
||||
|
||||
Defaults are taken from [`values.yaml`](values.yaml).
|
||||
|
||||
| Key | Default | Description |
|
||||
| --- | --- | --- |
|
||||
| `replicaCount` | `1` | Number of controller replicas. |
|
||||
| `image.repository` | `ghcr.io/lukaszraczylo/kubemirror` | Controller image. |
|
||||
| `image.tag` | `"0.1.0"` | Image tag (falls back to chart `appVersion` if empty). |
|
||||
| `image.pullPolicy` | `IfNotPresent` | Image pull policy. |
|
||||
| `serviceAccount.create` | `true` | Create a ServiceAccount. |
|
||||
| `serviceAccount.name` | `""` | Override the generated ServiceAccount name. |
|
||||
| `controller.metricsBindAddress` | `":8080"` | `--metrics-bind-address`. |
|
||||
| `controller.healthProbeBindAddress` | `":8081"` | `--health-probe-bind-address`. |
|
||||
| `controller.leaderElect` | `true` | Enable leader election (`--leader-elect`). |
|
||||
| `controller.leaderElectionID` | `"kubemirror-controller-leader"` | Lease name (`--leader-election-id`). |
|
||||
| `controller.resourceTypes` | `[]` | Resource types to mirror (`--resource-types`). Empty enables auto-discovery. |
|
||||
| `controller.discoveryInterval` | `"5m"` | Auto-discovery interval (`--discovery-interval`). |
|
||||
| `controller.resyncPeriod` | `"10m"` | Cache resync period (`--resync-period`). |
|
||||
| `controller.maxTargets` | `100` | Max target namespaces per resource (`--max-targets`). |
|
||||
| `controller.workerThreads` | `5` | Concurrent reconcile workers (`--worker-threads`). |
|
||||
| `controller.rateLimitQPS` | `50.0` | API QPS limit (`--rate-limit-qps`). |
|
||||
| `controller.rateLimitBurst` | `100` | API burst limit (`--rate-limit-burst`). |
|
||||
| `controller.verifySourceFreshness` | `false` | Verify cache against a direct API read (`--verify-source-freshness`). |
|
||||
| `controller.lazyWatcherInit` | `false` | Only watch resource types actually in use (`--lazy-watcher-init`). Lowers memory; recommended for production. |
|
||||
| `controller.watcherScanInterval` | `"5m"` | Scan interval for new types in lazy mode (`--watcher-scan-interval`). |
|
||||
| `controller.excludedNamespaces` | `""` | Comma-separated namespaces never mirrored to (`--excluded-namespaces`). |
|
||||
| `controller.includedNamespaces` | `""` | Comma-separated namespace patterns to include (`--included-namespaces`). |
|
||||
| `service.type` | `ClusterIP` | Service type. |
|
||||
| `service.metricsPort` | `8080` | Metrics port. |
|
||||
| `service.healthPort` | `8081` | Health-probe port. |
|
||||
| `resources.requests` | `100m` CPU / `128Mi` | Container requests. |
|
||||
| `resources.limits` | `500m` CPU / `512Mi` | Container limits. |
|
||||
| `nodeSelector` / `tolerations` / `affinity` | `{}` / `[]` / `{}` | Standard scheduling controls. |
|
||||
| `priorityClassName` | `""` | Pod priority class. |
|
||||
|
||||
> **Memory tip:** setting `controller.resourceTypes` to the exact types you mirror
|
||||
> (e.g. `["Secret.v1", "ConfigMap.v1"]`), or enabling `controller.lazyWatcherInit`,
|
||||
> avoids creating watchers for unused resource types and cuts memory use
|
||||
> substantially versus full auto-discovery.
|
||||
|
||||
## Usage
|
||||
|
||||
After install, mark a source resource for mirroring and opt a target namespace
|
||||
in. See the [project README](../../README.md) and [examples](../../examples/) for
|
||||
the full annotation/label reference and worked scenarios.
|
||||
@@ -134,7 +134,7 @@ func main() {
|
||||
ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts)))
|
||||
|
||||
setupLog.Info("starting kubemirror controller",
|
||||
"version", "dev",
|
||||
"version", Version,
|
||||
"maxTargets", maxTargets,
|
||||
"workers", workerThreads,
|
||||
)
|
||||
@@ -207,7 +207,7 @@ func main() {
|
||||
// This can reduce memory consumption by 50-70% by removing:
|
||||
// - managedFields (often several KB per resource)
|
||||
// - large annotations like kubectl.kubernetes.io/last-applied-configuration
|
||||
transformFunc := func(obj interface{}) (interface{}, error) {
|
||||
transformFunc := func(obj any) (any, error) {
|
||||
// Type assert to unstructured
|
||||
u, ok := obj.(*unstructured.Unstructured)
|
||||
if !ok {
|
||||
|
||||
+42
-26
@@ -113,13 +113,15 @@ kubectl get middleware headers -n namespace-3
|
||||
Verify that mirrored resources have the correct ownership labels:
|
||||
|
||||
```bash
|
||||
# Check labels on a mirrored secret
|
||||
kubectl get secret shared-credentials -n namespace-3 -o yaml | grep -A 5 labels
|
||||
# Inspect a mirrored secret's metadata
|
||||
kubectl get secret shared-credentials -n namespace-3 -o yaml
|
||||
|
||||
# Should include:
|
||||
# kubemirror.raczylo.com/mirrored: "true"
|
||||
# kubemirror.raczylo.com/source-namespace: namespace-1
|
||||
# kubemirror.raczylo.com/source-name: shared-credentials
|
||||
# Labels should include:
|
||||
# kubemirror.raczylo.com/managed-by: kubemirror
|
||||
# kubemirror.raczylo.com/mirror: "true"
|
||||
# Annotations should include:
|
||||
# kubemirror.raczylo.com/source-namespace: namespace-1
|
||||
# kubemirror.raczylo.com/source-name: shared-credentials
|
||||
```
|
||||
|
||||
## Testing Update Propagation
|
||||
@@ -213,26 +215,12 @@ kubectl get pods -n kubemirror-system
|
||||
|
||||
## Advanced Examples
|
||||
|
||||
### Mirror to All Except Specific Namespaces
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: almost-all
|
||||
namespace: namespace-1
|
||||
annotations:
|
||||
kubemirror.raczylo.com/sync: "true"
|
||||
kubemirror.raczylo.com/target-namespaces: "all"
|
||||
kubemirror.raczylo.com/excluded-namespaces: "namespace-3"
|
||||
labels:
|
||||
kubemirror.raczylo.com/enabled: "true"
|
||||
data:
|
||||
key: dmFsdWU= # "value" in base64
|
||||
```
|
||||
|
||||
### Pattern-Based Mirroring
|
||||
|
||||
`target-namespaces` accepts glob patterns (`*` and `?`, matched with Go's
|
||||
`filepath.Match`), so you can target a family of namespaces without listing each
|
||||
one. Comma-separate multiple patterns, e.g. `"app-*,prod-*"`.
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
@@ -241,14 +229,42 @@ metadata:
|
||||
namespace: namespace-1
|
||||
annotations:
|
||||
kubemirror.raczylo.com/sync: "true"
|
||||
kubemirror.raczylo.com/target-namespaces: "all"
|
||||
kubemirror.raczylo.com/namespace-pattern: "app-.*"
|
||||
# Mirror to every namespace whose name starts with "app-"
|
||||
kubemirror.raczylo.com/target-namespaces: "app-*"
|
||||
labels:
|
||||
kubemirror.raczylo.com/enabled: "true"
|
||||
data:
|
||||
config: "value"
|
||||
```
|
||||
|
||||
> **Note:** Target patterns are include-only; there is no "all except namespace
|
||||
> X" target syntax. For the two real exclusion mechanisms:
|
||||
>
|
||||
> - To stop a single source resource from being mirrored (and tear down any
|
||||
> mirrors it already created), set `kubemirror.raczylo.com/exclude: "true"` on
|
||||
> that resource.
|
||||
> - To stop specific namespaces from ever receiving mirrors cluster-wide, use the
|
||||
> controller's `--excluded-namespaces` flag.
|
||||
|
||||
### Excluding a Specific Resource
|
||||
|
||||
```yaml
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: do-not-mirror
|
||||
namespace: namespace-1
|
||||
annotations:
|
||||
kubemirror.raczylo.com/sync: "true"
|
||||
kubemirror.raczylo.com/target-namespaces: "all"
|
||||
# Opt this resource out: it will not be mirrored even though sync is set.
|
||||
kubemirror.raczylo.com/exclude: "true"
|
||||
labels:
|
||||
kubemirror.raczylo.com/enabled: "true"
|
||||
data:
|
||||
key: dmFsdWU=
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -3,13 +3,13 @@ module github.com/lukaszraczylo/kubemirror
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
github.com/go-logr/logr v1.4.3
|
||||
github.com/go-logr/logr v1.4.4
|
||||
github.com/lukaszraczylo/oss-telemetry v0.2.3
|
||||
github.com/stretchr/testify v1.11.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
k8s.io/api v0.36.1
|
||||
k8s.io/apimachinery v0.36.1
|
||||
k8s.io/client-go v0.36.1
|
||||
k8s.io/api v0.36.2
|
||||
k8s.io/apimachinery v0.36.2
|
||||
k8s.io/client-go v0.36.2
|
||||
sigs.k8s.io/controller-runtime v0.24.1
|
||||
)
|
||||
|
||||
@@ -22,20 +22,20 @@ require (
|
||||
github.com/fsnotify/fsnotify v1.10.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.2 // indirect
|
||||
github.com/go-logr/zapr v1.3.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.23.1 // indirect
|
||||
github.com/go-openapi/jsonreference v0.21.6 // indirect
|
||||
github.com/go-openapi/swag v0.26.1 // indirect
|
||||
github.com/go-openapi/swag/cmdutils v0.26.1 // indirect
|
||||
github.com/go-openapi/swag/conv v0.26.1 // indirect
|
||||
github.com/go-openapi/swag/fileutils v0.26.1 // indirect
|
||||
github.com/go-openapi/swag/jsonname v0.26.1 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.26.1 // indirect
|
||||
github.com/go-openapi/swag/loading v0.26.1 // indirect
|
||||
github.com/go-openapi/swag/mangling v0.26.1 // indirect
|
||||
github.com/go-openapi/swag/netutils v0.26.1 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.26.1 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.26.1 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.26.1 // indirect
|
||||
github.com/go-openapi/jsonpointer v1.0.0 // indirect
|
||||
github.com/go-openapi/jsonreference v1.0.0 // indirect
|
||||
github.com/go-openapi/swag v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/cmdutils v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/conv v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/fileutils v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/jsonutils v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/loading v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/mangling v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/netutils v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/pools v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/stringutils v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/typeutils v0.27.3 // indirect
|
||||
github.com/go-openapi/swag/yamlutils v0.27.3 // indirect
|
||||
github.com/google/gnostic-models v0.7.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
@@ -43,10 +43,10 @@ require (
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/client_golang v1.23.2 // indirect
|
||||
github.com/prometheus/client_golang v1.24.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.68.1 // indirect
|
||||
github.com/prometheus/procfs v0.20.1 // indirect
|
||||
github.com/prometheus/common v0.70.0 // indirect
|
||||
github.com/prometheus/procfs v0.21.1 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/x448/float16 v0.8.4 // indirect
|
||||
@@ -54,23 +54,23 @@ require (
|
||||
go.uber.org/zap v1.28.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/net v0.56.0 // indirect
|
||||
golang.org/x/net v0.57.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
golang.org/x/sync v0.21.0 // indirect
|
||||
golang.org/x/sys v0.46.0 // indirect
|
||||
golang.org/x/term v0.44.0 // indirect
|
||||
golang.org/x/text v0.38.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/term v0.45.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
golang.org/x/time v0.15.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.36.1 // indirect
|
||||
k8s.io/apiextensions-apiserver v0.36.2 // indirect
|
||||
k8s.io/klog/v2 v2.140.0 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20260603220949-865597e52e25 // indirect
|
||||
k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad // indirect
|
||||
k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
|
||||
sigs.k8s.io/randfill v1.0.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.4.0 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 // indirect
|
||||
sigs.k8s.io/yaml v1.6.0 // indirect
|
||||
)
|
||||
|
||||
@@ -18,44 +18,44 @@ github.com/fsnotify/fsnotify v1.10.1 h1:b0/UzAf9yR5rhf3RPm9gf3ehBPpf0oZKIjtpKrx5
|
||||
github.com/fsnotify/fsnotify v1.10.1/go.mod h1:TLheqan6HD6GBK6PrDWyDPBaEV8LspOxvPSjC+bVfgo=
|
||||
github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78=
|
||||
github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
||||
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ=
|
||||
github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg=
|
||||
github.com/go-openapi/jsonpointer v0.23.1 h1:1HBACs7XIwR2RcmItfdSFlALhGbe6S92p0ry4d1GWg4=
|
||||
github.com/go-openapi/jsonpointer v0.23.1/go.mod h1:iWRmZTrGn7XwYhtPt/fvdSFj1OfNBngqRT2UG3BxSqY=
|
||||
github.com/go-openapi/jsonreference v0.21.6 h1:NZ5nGfnaM1n4I43Xjm1e5/M2GjOwQwndQz22uhxwD+Y=
|
||||
github.com/go-openapi/jsonreference v0.21.6/go.mod h1:xzbgtQ3ZbWxvET3AxdzCJlJt6vkovbf+IfSPJjD0tUY=
|
||||
github.com/go-openapi/swag v0.26.1 h1:l5sVEyVpwj+DDYeZyo7wQI/Ebn/mKYIyGB/pFwAfGoQ=
|
||||
github.com/go-openapi/swag v0.26.1/go.mod h1:yNY38BbIVthxbkDtq1UHBCGasBqjakW3lCR6ANzdBEw=
|
||||
github.com/go-openapi/swag/cmdutils v0.26.1 h1:f2iE1ijYaJ3nuu5PaEMx3zpEhzhZFgivCJObWEObLIQ=
|
||||
github.com/go-openapi/swag/cmdutils v0.26.1/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM=
|
||||
github.com/go-openapi/swag/conv v0.26.1 h1:slr5FVkg9Wc3Y5zcwenD8Sd/PQ94b2I/QJI7N7KTBpg=
|
||||
github.com/go-openapi/swag/conv v0.26.1/go.mod h1:mvQXgPptZk9GTrFgGwWvT4q+dN+zQej9JfmGwnipz1A=
|
||||
github.com/go-openapi/swag/fileutils v0.26.1 h1:K1XCM2CGhfNsc6YDt6v7Q5+1e59rftYWdcu/isZhvFw=
|
||||
github.com/go-openapi/swag/fileutils v0.26.1/go.mod h1:mYUgxQAKX4ShS3qvvySx+/9yrlUnDhjiD1CalaQl8lQ=
|
||||
github.com/go-openapi/swag/jsonname v0.26.1 h1:VReupaV6WxlAsCn0e4DUfgV6bPmINnPpyJDLqSfNPcE=
|
||||
github.com/go-openapi/swag/jsonname v0.26.1/go.mod h1:OvdW6BoWoj33pTfi7x9vFrgmT+fk7aw0BRwvCE0YOuc=
|
||||
github.com/go-openapi/swag/jsonutils v0.26.1 h1:2hdBfFkHg+7Wrz2VsCbeyR6hzkRDs7AztnMR2u84yOY=
|
||||
github.com/go-openapi/swag/jsonutils v0.26.1/go.mod h1:U+RMJH3wa+6BRiphuRtIyI8fW9HPFqFQ4sHk2oRx0UQ=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.1 h1:1CD7NiLLb/TXl3tOnFYU4b+mNfb5rtgHkaA+q7RMYYQ=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.26.1/go.mod h1:ZWafc8nMdYzTE3uYY6W86f0n46+IF0g4uUyRhJw/kXc=
|
||||
github.com/go-openapi/swag/loading v0.26.1 h1:E9K4wqXeROlhjFQ13K9zMz6ojFGXIggGe+ad1odrK9w=
|
||||
github.com/go-openapi/swag/loading v0.26.1/go.mod h1:3qvRIlWzWdq1HvmldwmuJ2ohpcAryN6xVt2OTKd0/7E=
|
||||
github.com/go-openapi/swag/mangling v0.26.1 h1:gpYI4WuPKFJJVjV5cDLGlDVJhFIxYjQc7yN5eEb4CqM=
|
||||
github.com/go-openapi/swag/mangling v0.26.1/go.mod h1:POETDH01hqAdASXfw7ISEd9bCOE6xBHOt8NHmGZRmYM=
|
||||
github.com/go-openapi/swag/netutils v0.26.1 h1:BNctoc39WTAUMxyAs355fExOPzMZtPbZ0ZZ1Am2FR5M=
|
||||
github.com/go-openapi/swag/netutils v0.26.1/go.mod h1:y02vByhZhQPAVwOX+0KipXFZ/hUbk6G/Enhf5rGaOkQ=
|
||||
github.com/go-openapi/swag/stringutils v0.26.1 h1:f88uYyTso7TnHrKM/bUBsQ5e2wKf37cpgo6pvbzd9yU=
|
||||
github.com/go-openapi/swag/stringutils v0.26.1/go.mod h1:Sc6d3bU8fgk5AyZR8/8jEQ+Is/Ald+TD/IIggPN8UJk=
|
||||
github.com/go-openapi/swag/typeutils v0.26.1 h1:yg42FgMzRR6PVQ3M3qHz1s+Y6/P4HoJ3cBarXa3OVnU=
|
||||
github.com/go-openapi/swag/typeutils v0.26.1/go.mod h1:VfnV+oUtSP2vCSCn2aJgnr8OevUYemyIzzS1VOzS10o=
|
||||
github.com/go-openapi/swag/yamlutils v0.26.1 h1:0TSLK+lXs9vfIhAWzBeI/lOzEnIoot6WTCO1aAeWFTk=
|
||||
github.com/go-openapi/swag/yamlutils v0.26.1/go.mod h1:7W5b7PRX9MxwL7TjeG7H8HkyBGRsIDRObhyMWFgBI2M=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.5.1 h1:q9NtHwK4qHF7yZziBPvZyv7zWAIk8ok88Gh2mR6Jpc8=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.5.1/go.mod h1:JW0MXIotCYps/XsgJnG3a8Q7rE5xAiBwoOD5OfaIQBk=
|
||||
github.com/go-openapi/testify/v2 v2.5.1 h1:TMdhCaw8fUNraVSf3Omoob1dO/AzBfhtFAPW0an6sBo=
|
||||
github.com/go-openapi/testify/v2 v2.5.1/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
|
||||
github.com/go-openapi/jsonpointer v1.0.0 h1:kR9tHqY0CtZaOPVFm622dPVNhrvYpwr4uCxgL3h1H8s=
|
||||
github.com/go-openapi/jsonpointer v1.0.0/go.mod h1:Z3rw7dWu1p9IgitXCFamSlA5lmDiklEB6vkaxcNZW5Y=
|
||||
github.com/go-openapi/jsonreference v1.0.0 h1:jlmTr6torcd1YgDQvSfNmRtKzYDO4FGBkrAdlAVWnpY=
|
||||
github.com/go-openapi/jsonreference v1.0.0/go.mod h1:jtwdyGbJk0Xhe5Y+rwtglQP6Sb1WZST4rT32LWB+sv0=
|
||||
github.com/go-openapi/swag v0.27.3 h1:i6oVKkGZeFgETHMiBHGtj9gIQ1aLtWDdJnT/SRZeets=
|
||||
github.com/go-openapi/swag v0.27.3/go.mod h1:qEXs3GcyyQTDCFQ4ykqnLPDh8qT+zBcjbVWdxCAW0Us=
|
||||
github.com/go-openapi/swag/cmdutils v0.27.3 h1:sjuL0TvW81i9R9GRMO/fy+c3mOW+7zxRYwy/7fobZt4=
|
||||
github.com/go-openapi/swag/cmdutils v0.27.3/go.mod h1:Sm1MVFMkF6guJJ+pQqHnQA3N0j9qALV3NxzDSv6bETM=
|
||||
github.com/go-openapi/swag/conv v0.27.3 h1:iqJFmGEjmX3AY0lSszABFqRVqOSt99XS0LzNIMJYuhU=
|
||||
github.com/go-openapi/swag/conv v0.27.3/go.mod h1:nPRmN6jgNme99hpf+nM0auDZGALWIqlwhisKPK/bQhQ=
|
||||
github.com/go-openapi/swag/fileutils v0.27.3 h1:3UVoZ2RLaIs1lt+2jcKzL8RM3Yk0rmsDE9FLA/HGxFE=
|
||||
github.com/go-openapi/swag/fileutils v0.27.3/go.mod h1:VvJFZLTZS0AI854gEQz5tk7dBESdLjiNUMSZ/th2ry8=
|
||||
github.com/go-openapi/swag/jsonutils v0.27.3 h1:1DEz+O82frtSMBcos/7XIn1GnpNTbsD4Bru4Dc/uhRc=
|
||||
github.com/go-openapi/swag/jsonutils v0.27.3/go.mod h1:qiDCoQvzkMxrV3G8FLEdIU5L+EFYc0zcDOHWT3Yofvo=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3 h1:h/eT9kmGCDdFLJF29lOhzLtF0FmP1AX2MhLJWVebsb8=
|
||||
github.com/go-openapi/swag/jsonutils/fixtures_test v0.27.3/go.mod h1:mofwUWx70wvskwESqRJ//k/9kURmCgyJl5m5Ppoh5kY=
|
||||
github.com/go-openapi/swag/loading v0.27.3 h1:L9nQkEgzU7QgFQL+pLEMfGUKxeM4pWwGwbET9Z3weW0=
|
||||
github.com/go-openapi/swag/loading v0.27.3/go.mod h1:rJ0NeaKsF4CVPnMGjPQl7JlSHzvD0bc2DKXLss1hiuE=
|
||||
github.com/go-openapi/swag/mangling v0.27.3 h1:gRzzD1PAUoLTtGMgI3KpBmCSOlTuLTFWnviLxLcTnyg=
|
||||
github.com/go-openapi/swag/mangling v0.27.3/go.mod h1:jtBE2+V+3pILxOR7Vgce+Cwp6A2PgZbvVqfNntbVs0w=
|
||||
github.com/go-openapi/swag/netutils v0.27.3 h1:IoBvfCoprsE6E87kAIm9basnISqDDqB79mJ8MN+f5PU=
|
||||
github.com/go-openapi/swag/netutils v0.27.3/go.mod h1:J+WYyFMLtvtCGqa6jLv+YNUmIKI3ZRQRrvfNDMoQoEQ=
|
||||
github.com/go-openapi/swag/pools v0.27.3 h1:gXjImP3F6/56wRRcFgEPld084Y6u2gs21ikPBt8NKBk=
|
||||
github.com/go-openapi/swag/pools v0.27.3/go.mod h1:kVQefhSK5RWuRe7BXsL8htgBPAMpN7HDGpGEknqugeE=
|
||||
github.com/go-openapi/swag/stringutils v0.27.3 h1:Ru28hnbAvN5wycALQYy8IobHvASq+FUFMlp1QzLM0JI=
|
||||
github.com/go-openapi/swag/stringutils v0.27.3/go.mod h1:lzRN95CxXmA03XcDWHLOb6nOMcxCqR5rGY0lOgsfRoM=
|
||||
github.com/go-openapi/swag/typeutils v0.27.3 h1:l6SSrx5eR5/WVwrGNzN6bQ9WqL04mrxNBl9YgQ3rcJ4=
|
||||
github.com/go-openapi/swag/typeutils v0.27.3/go.mod h1:Srm0xFNRZ1Y+vCxJclo5qzx8aj+1pAKda/YfFPrG0dQ=
|
||||
github.com/go-openapi/swag/yamlutils v0.27.3 h1:cRFCAoYtslYn9L9T0xWryHy1t7c1MACC+DMj3CLvwvs=
|
||||
github.com/go-openapi/swag/yamlutils v0.27.3/go.mod h1:6JYBGj8sw/NawMllyZY+cTA8Mzk2etS3ZBASdcyPsiU=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0 h1:gGHwAJ0R/5jU8BEGDbfRNR3hL68dAVi84WuOApp29B0=
|
||||
github.com/go-openapi/testify/enable/yaml/v2 v2.6.0/go.mod h1:tY+St1SGq4NFl0QIqdTY4aEdbChAHxhyB77XQi9iJCo=
|
||||
github.com/go-openapi/testify/v2 v2.6.0 h1:5PKH2HE7YJ/LuRPQGvSxBRlFXNQhSetBLlGAgUEu3ug=
|
||||
github.com/go-openapi/testify/v2 v2.6.0/go.mod h1:SgsVHtfooshd0tublTtJ50FPKhujf47YRqauXXOUxfw=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c=
|
||||
@@ -71,8 +71,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
|
||||
github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
@@ -98,14 +98,14 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_golang v1.24.0 h1:5XStIklKuAtJSNpdD3s8XJj/Yv78IQmE1kbNk87JrAI=
|
||||
github.com/prometheus/client_golang v1.24.0/go.mod h1:QcsNdotprC2nS4BTM2ucbcqxd2CeXTEa9jW7zHO9iDE=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.68.1 h1:omjRRl4QP4komogpXuhfeOiisQg7xdy8VM1UY+pStaY=
|
||||
github.com/prometheus/common v0.68.1/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/prometheus/common v0.70.0 h1:bcpru3tWPVnxGnETLgOV5jbp/JRXgYEyv65CuBLAMMI=
|
||||
github.com/prometheus/common v0.70.0/go.mod h1:S/SFasQmgGiYH6C81LKCtYa8QACgthGg5zxL2udV7SY=
|
||||
github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
|
||||
github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
@@ -128,24 +128,24 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
|
||||
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
|
||||
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
|
||||
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0=
|
||||
gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY=
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
|
||||
@@ -159,27 +159,27 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
k8s.io/api v0.36.1 h1:XbL/EMj8K2aJpJtePmqUyQMsM0D4QI2pvl7YKJ20FTY=
|
||||
k8s.io/api v0.36.1/go.mod h1:KOWo4ey3TINlXjeHVuwB3i+tXXnu+UcwFBHlI/9dvEo=
|
||||
k8s.io/apiextensions-apiserver v0.36.1 h1:6JfYmPUsuUIHuN+3QxutXYWj492RqF5fBSx67GYK5Ks=
|
||||
k8s.io/apiextensions-apiserver v0.36.1/go.mod h1:pLzZin90riwisdzKwv/GoTwENooytoIx5zWJb4Hkby8=
|
||||
k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA=
|
||||
k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8=
|
||||
k8s.io/client-go v0.36.1 h1:FN/K8QIT2CEDt+2WB2HnWrUANZ50AP5GII43/SP2JR0=
|
||||
k8s.io/client-go v0.36.1/go.mod h1:s6rAnCtTGYDQnpNjEhSaISV+2O8jwruZ6m3QOYBFbtU=
|
||||
k8s.io/api v0.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY=
|
||||
k8s.io/api v0.36.2/go.mod h1:F4LbMO4brjZYh7yFkXWhynSvtB7YauxV4c+HHkNRGNg=
|
||||
k8s.io/apiextensions-apiserver v0.36.2 h1:3O5gqOj/dt2XWWbpMe+TXWpE9yU6pjM/tXxtHHJT/K4=
|
||||
k8s.io/apiextensions-apiserver v0.36.2/go.mod h1:cL1tBWe8XSaP1H30iWKGo7hf6iAUUUJPEU70dskmAnA=
|
||||
k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ=
|
||||
k8s.io/apimachinery v0.36.2/go.mod h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4=
|
||||
k8s.io/client-go v0.36.2 h1:bfgxmFKc9CgqsgX4xKLAAdmTQlWee7Ob/HlDOrJ5TBI=
|
||||
k8s.io/client-go v0.36.2/go.mod h1:1vgO4OAlfPnoLcb+Rze2GF5rAr14w8qjrYMoyXJzQj0=
|
||||
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
|
||||
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
|
||||
k8s.io/kube-openapi v0.0.0-20260603220949-865597e52e25 h1:mPMaPMpBij2V1Wv/fR+HW124vVGXXvOSS9ver/9yjWs=
|
||||
k8s.io/kube-openapi v0.0.0-20260603220949-865597e52e25/go.mod h1:V/QaCUYDa+0QpcHhVVc5l99Uz56wEMEXBSj9oCDkNDY=
|
||||
k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 h1:wU4tMEhLGgIbLvXQb1cfN+EcM0wf7zC6CPF+C79jroc=
|
||||
k8s.io/utils v0.0.0-20260507154919-ff6756f316d2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
|
||||
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad h1:oXImqH8mQNk7PmvzKhmN3ddJoY6OnyM225MXwGHPm0A=
|
||||
k8s.io/kube-openapi v0.0.0-20260721132016-d427ff9ee9ad/go.mod h1:0/mqHCVhlumdJ3BhCfnjSZQE037nAhNodh1/hK0T8/I=
|
||||
k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 h1:jVkFFVfXdXP74B/zbO3hM3hpSFD0xvhQ5U686DPurkE=
|
||||
k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3/go.mod h1:M2s5JB1lIYP3jzZdorPLHXIPJzt9vv2muW5a6L9DtNM=
|
||||
sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4=
|
||||
sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
|
||||
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
|
||||
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
|
||||
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.4.0 h1:qmp2e3ZfFi1/jJbDGpD4mt3wyp6PE1NfKHCYLqgNQJo=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.4.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.4.2 h1:qdOxHwrl2Kaag1aQEarlYcOA9vSyGCp3CIki3aW8c4Q=
|
||||
sigs.k8s.io/structured-merge-diff/v6 v6.4.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
|
||||
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
|
||||
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
|
||||
|
||||
@@ -159,9 +159,14 @@ histogram_quantile(0.99,
|
||||
)
|
||||
|
||||
# Current workqueue depth
|
||||
workqueue_depth{name=~"secret|configmap"}
|
||||
workqueue_depth{name=~"Secret.*|ConfigMap.*"}
|
||||
```
|
||||
|
||||
> **Controller label values:** kubemirror names each controller `<Kind>.<version>.<group>`
|
||||
> (e.g. `Secret.v1.`, `ConfigMap.v1.`) plus a `-mirror` variant for orphan
|
||||
> detection, so PromQL filters use prefixes like `controller=~"Secret.*"` rather
|
||||
> than `controller="secret"`. Adjust the kind for other mirrored resource types.
|
||||
|
||||
### Using kubectl
|
||||
|
||||
```bash
|
||||
|
||||
@@ -134,12 +134,12 @@
|
||||
"pluginVersion": "8.0.0",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(rate(controller_runtime_reconcile_total{controller=\"secret\"}[5m])) by (result)",
|
||||
"expr": "sum(rate(controller_runtime_reconcile_total{controller=~\"Secret.*\"}[5m])) by (result)",
|
||||
"legendFormat": "Secret - {{result}}",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "sum(rate(controller_runtime_reconcile_total{controller=\"configmap\"}[5m])) by (result)",
|
||||
"expr": "sum(rate(controller_runtime_reconcile_total{controller=~\"ConfigMap.*\"}[5m])) by (result)",
|
||||
"legendFormat": "ConfigMap - {{result}}",
|
||||
"refId": "B"
|
||||
}
|
||||
@@ -198,7 +198,7 @@
|
||||
"pluginVersion": "8.0.0",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(workqueue_depth{name=~\"secret|configmap\"})",
|
||||
"expr": "sum(workqueue_depth{name=~\"Secret.*|ConfigMap.*\"})",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
@@ -266,22 +266,22 @@
|
||||
"pluginVersion": "8.0.0",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "histogram_quantile(0.99, sum(rate(controller_runtime_reconcile_time_seconds_bucket{controller=\"secret\"}[5m])) by (le))",
|
||||
"expr": "histogram_quantile(0.99, sum(rate(controller_runtime_reconcile_time_seconds_bucket{controller=~\"Secret.*\"}[5m])) by (le))",
|
||||
"legendFormat": "Secret p99",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "histogram_quantile(0.95, sum(rate(controller_runtime_reconcile_time_seconds_bucket{controller=\"secret\"}[5m])) by (le))",
|
||||
"expr": "histogram_quantile(0.95, sum(rate(controller_runtime_reconcile_time_seconds_bucket{controller=~\"Secret.*\"}[5m])) by (le))",
|
||||
"legendFormat": "Secret p95",
|
||||
"refId": "B"
|
||||
},
|
||||
{
|
||||
"expr": "histogram_quantile(0.99, sum(rate(controller_runtime_reconcile_time_seconds_bucket{controller=\"configmap\"}[5m])) by (le))",
|
||||
"expr": "histogram_quantile(0.99, sum(rate(controller_runtime_reconcile_time_seconds_bucket{controller=~\"ConfigMap.*\"}[5m])) by (le))",
|
||||
"legendFormat": "ConfigMap p99",
|
||||
"refId": "C"
|
||||
},
|
||||
{
|
||||
"expr": "histogram_quantile(0.95, sum(rate(controller_runtime_reconcile_time_seconds_bucket{controller=\"configmap\"}[5m])) by (le))",
|
||||
"expr": "histogram_quantile(0.95, sum(rate(controller_runtime_reconcile_time_seconds_bucket{controller=~\"ConfigMap.*\"}[5m])) by (le))",
|
||||
"legendFormat": "ConfigMap p95",
|
||||
"refId": "D"
|
||||
}
|
||||
@@ -350,12 +350,12 @@
|
||||
"pluginVersion": "8.0.0",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "workqueue_depth{name=\"secret\"}",
|
||||
"expr": "workqueue_depth{name=~\"Secret.*\"}",
|
||||
"legendFormat": "Secret",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "workqueue_depth{name=\"configmap\"}",
|
||||
"expr": "workqueue_depth{name=~\"ConfigMap.*\"}",
|
||||
"legendFormat": "ConfigMap",
|
||||
"refId": "B"
|
||||
}
|
||||
@@ -571,12 +571,12 @@
|
||||
"pluginVersion": "8.0.0",
|
||||
"targets": [
|
||||
{
|
||||
"expr": "sum(rate(controller_runtime_reconcile_errors_total{controller=\"secret\"}[5m])) / sum(rate(controller_runtime_reconcile_total{controller=\"secret\"}[5m]))",
|
||||
"expr": "sum(rate(controller_runtime_reconcile_errors_total{controller=~\"Secret.*\"}[5m])) / sum(rate(controller_runtime_reconcile_total{controller=~\"Secret.*\"}[5m]))",
|
||||
"legendFormat": "Secret Error Rate",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"expr": "sum(rate(controller_runtime_reconcile_errors_total{controller=\"configmap\"}[5m])) / sum(rate(controller_runtime_reconcile_total{controller=\"configmap\"}[5m]))",
|
||||
"expr": "sum(rate(controller_runtime_reconcile_errors_total{controller=~\"ConfigMap.*\"}[5m])) / sum(rate(controller_runtime_reconcile_total{controller=~\"ConfigMap.*\"}[5m]))",
|
||||
"legendFormat": "ConfigMap Error Rate",
|
||||
"refId": "B"
|
||||
}
|
||||
|
||||
@@ -25,9 +25,9 @@ spec:
|
||||
|
||||
- alert: KubeMirrorHighReconcileErrors
|
||||
expr: |
|
||||
rate(controller_runtime_reconcile_errors_total{controller="secret"}[5m]) > 0.1
|
||||
rate(controller_runtime_reconcile_errors_total{controller=~"Secret.*"}[5m]) > 0.1
|
||||
or
|
||||
rate(controller_runtime_reconcile_errors_total{controller="configmap"}[5m]) > 0.1
|
||||
rate(controller_runtime_reconcile_errors_total{controller=~"ConfigMap.*"}[5m]) > 0.1
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
@@ -39,7 +39,7 @@ spec:
|
||||
- alert: KubeMirrorReconcileLatencyHigh
|
||||
expr: |
|
||||
histogram_quantile(0.99,
|
||||
rate(controller_runtime_reconcile_time_seconds_bucket{controller=~"secret|configmap"}[5m])
|
||||
rate(controller_runtime_reconcile_time_seconds_bucket{controller=~"Secret.*|ConfigMap.*"}[5m])
|
||||
) > 5
|
||||
for: 10m
|
||||
labels:
|
||||
@@ -51,7 +51,7 @@ spec:
|
||||
|
||||
- alert: KubeMirrorWorkqueueDepthHigh
|
||||
expr: |
|
||||
workqueue_depth{name=~"secret|configmap"} > 100
|
||||
workqueue_depth{name=~"Secret.*|ConfigMap.*"} > 100
|
||||
for: 15m
|
||||
labels:
|
||||
severity: warning
|
||||
@@ -74,9 +74,9 @@ spec:
|
||||
# Resource mirror alerts
|
||||
- alert: KubeMirrorHighFailureRate
|
||||
expr: |
|
||||
sum(rate(controller_runtime_reconcile_errors_total{controller=~"secret|configmap"}[5m]))
|
||||
sum(rate(controller_runtime_reconcile_errors_total{controller=~"Secret.*|ConfigMap.*"}[5m]))
|
||||
/
|
||||
sum(rate(controller_runtime_reconcile_total{controller=~"secret|configmap"}[5m]))
|
||||
sum(rate(controller_runtime_reconcile_total{controller=~"Secret.*|ConfigMap.*"}[5m]))
|
||||
> 0.05
|
||||
for: 10m
|
||||
labels:
|
||||
@@ -118,29 +118,29 @@ spec:
|
||||
- record: kubemirror:reconcile_duration_seconds:p99
|
||||
expr: |
|
||||
histogram_quantile(0.99,
|
||||
rate(controller_runtime_reconcile_time_seconds_bucket{controller=~"secret|configmap"}[5m])
|
||||
rate(controller_runtime_reconcile_time_seconds_bucket{controller=~"Secret.*|ConfigMap.*"}[5m])
|
||||
)
|
||||
|
||||
- record: kubemirror:reconcile_duration_seconds:p95
|
||||
expr: |
|
||||
histogram_quantile(0.95,
|
||||
rate(controller_runtime_reconcile_time_seconds_bucket{controller=~"secret|configmap"}[5m])
|
||||
rate(controller_runtime_reconcile_time_seconds_bucket{controller=~"Secret.*|ConfigMap.*"}[5m])
|
||||
)
|
||||
|
||||
- record: kubemirror:reconcile_duration_seconds:p50
|
||||
expr: |
|
||||
histogram_quantile(0.50,
|
||||
rate(controller_runtime_reconcile_time_seconds_bucket{controller=~"secret|configmap"}[5m])
|
||||
rate(controller_runtime_reconcile_time_seconds_bucket{controller=~"Secret.*|ConfigMap.*"}[5m])
|
||||
)
|
||||
|
||||
- record: kubemirror:reconcile_rate:5m
|
||||
expr: |
|
||||
sum(rate(controller_runtime_reconcile_total{controller=~"secret|configmap"}[5m])) by (controller, result)
|
||||
sum(rate(controller_runtime_reconcile_total{controller=~"Secret.*|ConfigMap.*"}[5m])) by (controller, result)
|
||||
|
||||
- record: kubemirror:reconcile_errors:rate5m
|
||||
expr: |
|
||||
sum(rate(controller_runtime_reconcile_errors_total{controller=~"secret|configmap"}[5m])) by (controller)
|
||||
sum(rate(controller_runtime_reconcile_errors_total{controller=~"Secret.*|ConfigMap.*"}[5m])) by (controller)
|
||||
|
||||
- record: kubemirror:workqueue_depth:max
|
||||
expr: |
|
||||
max(workqueue_depth{name=~"secret|configmap"}) by (name)
|
||||
max(workqueue_depth{name=~"Secret.*|ConfigMap.*"}) by (name)
|
||||
|
||||
@@ -71,31 +71,18 @@ const (
|
||||
// Annotation because: values can be complex patterns exceeding label limits.
|
||||
AnnotationTargetNamespaces = Domain + "/target-namespaces"
|
||||
|
||||
// AnnotationExclude explicitly excludes a resource from mirroring when "true".
|
||||
// AnnotationExclude opts a resource out of mirroring when "true", overriding
|
||||
// the enabled label and sync annotation. Any mirrors it previously created are
|
||||
// removed. Use AnnotationPaused instead to freeze mirrors without deleting them.
|
||||
// Annotation because: used for configuration, not filtering.
|
||||
AnnotationExclude = Domain + "/exclude"
|
||||
|
||||
// AnnotationMaxTargets overrides the default maximum target limit per resource.
|
||||
// Annotation because: numeric configuration value.
|
||||
AnnotationMaxTargets = Domain + "/max-targets"
|
||||
|
||||
// AnnotationRecreateOnImmutableChange controls delete/recreate behavior.
|
||||
// When "true", kubemirror will delete and recreate mirrors on immutable field changes.
|
||||
// Annotation because: configuration flag, not used for filtering.
|
||||
AnnotationRecreateOnImmutableChange = Domain + "/recreate-on-immutable-change"
|
||||
|
||||
// AnnotationPaused on controller deployment pauses all reconciliation when "true".
|
||||
// AnnotationPaused freezes a source's mirrors when "true": existing mirrors
|
||||
// are left untouched (no updates, no orphan cleanup) until the annotation is
|
||||
// removed. Unlike AnnotationExclude, pausing does not delete existing mirrors.
|
||||
// Annotation because: operational control, not used for filtering.
|
||||
AnnotationPaused = Domain + "/paused"
|
||||
|
||||
// --- Source Tracking Annotations ---
|
||||
// These are set by kubemirror on source resources for change detection.
|
||||
|
||||
// AnnotationContentHash stores the SHA256 hash of the source resource content.
|
||||
// Used for efficient change detection without deep comparison.
|
||||
// Annotation because: computed value (64 chars), may exceed label limits.
|
||||
AnnotationContentHash = Domain + "/content-hash"
|
||||
|
||||
// --- Mirror Ownership Annotations ---
|
||||
// These are set by kubemirror on mirror resources to track their source.
|
||||
// All are annotations because they store tracking data, not used for filtering.
|
||||
@@ -129,19 +116,6 @@ const (
|
||||
// AnnotationSyncStatus stores human-readable sync status ("3/5 synced", etc.).
|
||||
AnnotationSyncStatus = Domain + "/sync-status"
|
||||
|
||||
// AnnotationFailedTargets stores comma-separated list of failed target namespaces.
|
||||
AnnotationFailedTargets = Domain + "/failed-targets"
|
||||
|
||||
// AnnotationWebhookError stores webhook rejection error message for debugging.
|
||||
AnnotationWebhookError = Domain + "/webhook-error"
|
||||
|
||||
// AnnotationTargetNamespaceUID tracks the UID of the target namespace.
|
||||
// Used for detecting namespace recreation.
|
||||
AnnotationTargetNamespaceUID = Domain + "/target-namespace-uid"
|
||||
|
||||
// AnnotationDeletionAttempts tracks number of failed deletion attempts.
|
||||
AnnotationDeletionAttempts = Domain + "/deletion-attempts"
|
||||
|
||||
// --- Transformation Annotations ---
|
||||
// These configure resource transformation during mirroring.
|
||||
|
||||
@@ -189,13 +163,4 @@ var (
|
||||
"bootstrap.kubernetes.io/token",
|
||||
"helm.sh/release.v1",
|
||||
}
|
||||
|
||||
// Default Denied Resource Types
|
||||
DefaultDeniedResourceTypes = []string{
|
||||
"events",
|
||||
"pods",
|
||||
"replicasets",
|
||||
"endpoints",
|
||||
"endpointslices",
|
||||
}
|
||||
)
|
||||
|
||||
+10
-23
@@ -3,6 +3,7 @@ package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -96,7 +97,7 @@ func filterKubeMirrorMetadata(metadata map[string]string) map[string]string {
|
||||
filtered := make(map[string]string)
|
||||
for k, v := range metadata {
|
||||
// Skip all kubemirror.raczylo.com keys
|
||||
if !strings.HasPrefix(k, "kubemirror.raczylo.com/") {
|
||||
if !strings.HasPrefix(k, constants.Domain+"/") {
|
||||
filtered[k] = v
|
||||
}
|
||||
}
|
||||
@@ -136,9 +137,7 @@ func createUnstructuredMirror(source runtime.Object, targetNamespace, sourceHash
|
||||
|
||||
// Add mirror-specific annotations
|
||||
annotations := buildMirrorAnnotations(source, sourceHash)
|
||||
for k, v := range annotations {
|
||||
existingAnnotations[k] = v
|
||||
}
|
||||
maps.Copy(existingAnnotations, annotations)
|
||||
mirror.SetAnnotations(existingAnnotations)
|
||||
|
||||
// Remove status (never mirror status)
|
||||
@@ -272,8 +271,8 @@ func UpdateMirror(mirror, source runtime.Object) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertToStringMap converts map[string]interface{} to map[string]string.
|
||||
func convertToStringMap(data map[string]interface{}) map[string]string {
|
||||
// convertToStringMap converts map[string]any to map[string]string.
|
||||
func convertToStringMap(data map[string]any) map[string]string {
|
||||
result := make(map[string]string)
|
||||
for k, v := range data {
|
||||
if s, ok := v.(string); ok {
|
||||
@@ -283,8 +282,8 @@ func convertToStringMap(data map[string]interface{}) map[string]string {
|
||||
return result
|
||||
}
|
||||
|
||||
// convertToByteMap converts map[string]interface{} to map[string][]byte.
|
||||
func convertToByteMap(data map[string]interface{}) map[string][]byte {
|
||||
// convertToByteMap converts map[string]any to map[string][]byte.
|
||||
func convertToByteMap(data map[string]any) map[string][]byte {
|
||||
result := make(map[string][]byte)
|
||||
for k, v := range data {
|
||||
switch val := v.(type) {
|
||||
@@ -445,13 +444,7 @@ func applyTransformations(source, mirror runtime.Object, targetNamespace string)
|
||||
|
||||
// Save original annotations to restore on failure
|
||||
originalAnnotations := mirrorObj.GetAnnotations()
|
||||
var savedAnnotations map[string]string
|
||||
if originalAnnotations != nil {
|
||||
savedAnnotations = make(map[string]string, len(originalAnnotations))
|
||||
for k, v := range originalAnnotations {
|
||||
savedAnnotations[k] = v
|
||||
}
|
||||
}
|
||||
savedAnnotations := maps.Clone(originalAnnotations)
|
||||
|
||||
mirrorAnnotations := mirrorObj.GetAnnotations()
|
||||
if mirrorAnnotations == nil {
|
||||
@@ -504,18 +497,12 @@ func buildTransformContext(source, mirror runtime.Object, targetNamespace string
|
||||
|
||||
// Copy labels (if any)
|
||||
if labels := sourceObj.GetLabels(); labels != nil {
|
||||
ctx.Labels = make(map[string]string)
|
||||
for k, v := range labels {
|
||||
ctx.Labels[k] = v
|
||||
}
|
||||
ctx.Labels = maps.Clone(labels)
|
||||
}
|
||||
|
||||
// Copy annotations (if any)
|
||||
if annotations := sourceObj.GetAnnotations(); annotations != nil {
|
||||
ctx.Annotations = make(map[string]string)
|
||||
for k, v := range annotations {
|
||||
ctx.Annotations[k] = v
|
||||
}
|
||||
ctx.Annotations = maps.Clone(annotations)
|
||||
}
|
||||
|
||||
return ctx
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// Package controller implements the kubemirror reconciliation logic.
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
// mirrorDeletionOutcome describes the result of an ownership-verified mirror
|
||||
// deletion attempt.
|
||||
type mirrorDeletionOutcome int
|
||||
|
||||
const (
|
||||
// mirrorAbsent means no resource exists at the requested namespace/name.
|
||||
mirrorAbsent mirrorDeletionOutcome = iota
|
||||
// mirrorNotOwned means a resource exists but is not a kubemirror-managed
|
||||
// mirror of the given source, so it was left untouched.
|
||||
mirrorNotOwned
|
||||
// mirrorDeleted means an owned mirror was found and deleted.
|
||||
mirrorDeleted
|
||||
)
|
||||
|
||||
// deleteOwnedMirror deletes the mirror at gvk/ns/name only if it is managed by
|
||||
// kubemirror AND its source reference matches srcNs/srcName. This ownership
|
||||
// guard is the single safety gate that prevents deleting an unrelated resource
|
||||
// that merely shares the same name. All mirror-cleanup paths route through here
|
||||
// so the guard cannot be forgotten in one place and drift in another.
|
||||
//
|
||||
// A NotFound on delete is treated as success (the mirror is already gone).
|
||||
func deleteOwnedMirror(
|
||||
ctx context.Context,
|
||||
c client.Client,
|
||||
gvk schema.GroupVersionKind,
|
||||
ns, name, srcNs, srcName string,
|
||||
) (mirrorDeletionOutcome, error) {
|
||||
mirror := &unstructured.Unstructured{}
|
||||
mirror.SetGroupVersionKind(gvk)
|
||||
|
||||
if err := c.Get(ctx, client.ObjectKey{Namespace: ns, Name: name}, mirror); err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return mirrorAbsent, nil
|
||||
}
|
||||
return mirrorAbsent, err
|
||||
}
|
||||
|
||||
// Verify this is actually our mirror (not someone else's resource with the same name).
|
||||
if !IsManagedByUs(mirror) {
|
||||
return mirrorNotOwned, nil
|
||||
}
|
||||
|
||||
// Verify this mirror points to our source.
|
||||
refNs, refName, _, found := GetSourceReference(mirror)
|
||||
if !found || refNs != srcNs || refName != srcName {
|
||||
return mirrorNotOwned, nil
|
||||
}
|
||||
|
||||
if err := c.Delete(ctx, mirror); err != nil && !errors.IsNotFound(err) {
|
||||
return mirrorNotOwned, err
|
||||
}
|
||||
return mirrorDeleted, nil
|
||||
}
|
||||
@@ -149,7 +149,7 @@ func (r *MirrorReconciler) SetupWithManager(mgr ctrl.Manager, gvk schema.GroupVe
|
||||
return false
|
||||
}
|
||||
managedBy, exists := labels[constants.LabelManagedBy]
|
||||
return exists && managedBy == "kubemirror"
|
||||
return exists && managedBy == constants.ControllerName
|
||||
})
|
||||
|
||||
// Convert GVK to resource object for watching
|
||||
@@ -157,9 +157,7 @@ func (r *MirrorReconciler) SetupWithManager(mgr ctrl.Manager, gvk schema.GroupVe
|
||||
gv := schema.GroupVersion{Group: gvk.Group, Version: gvk.Version}
|
||||
obj.SetGroupVersionKind(gv.WithKind(gvk.Kind))
|
||||
|
||||
// Set custom controller name to avoid conflicts with source reconciler and multiple API versions
|
||||
// Include group and version to make it truly unique
|
||||
controllerName := gvk.Kind + "." + gvk.Version + "." + gvk.Group + "-mirror"
|
||||
controllerName := gvkControllerName(gvk, true)
|
||||
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(obj).
|
||||
|
||||
@@ -182,7 +182,7 @@ func TestCreateMirror_Unstructured_StripsOwnerReferences(t *testing.T) {
|
||||
"externalsecrets.external-secrets.io/externalsecret-cleanup",
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"data": map[string]interface{}{ //nolint:gosec // base64 test fixture, not a real credential
|
||||
"password": "c2VjcmV0",
|
||||
},
|
||||
},
|
||||
@@ -387,7 +387,7 @@ func TestUpdateMirror_UnstructuredSecret(t *testing.T) {
|
||||
},
|
||||
},
|
||||
"type": "Opaque",
|
||||
"data": map[string]interface{}{
|
||||
"data": map[string]interface{}{ //nolint:gosec // base64 test fixture, not a real credential
|
||||
"password": "b2xkLXZhbHVl", // base64 encoded "old-value"
|
||||
},
|
||||
},
|
||||
@@ -403,7 +403,7 @@ func TestUpdateMirror_UnstructuredSecret(t *testing.T) {
|
||||
"generation": int64(10),
|
||||
},
|
||||
"type": "kubernetes.io/tls",
|
||||
"data": map[string]interface{}{
|
||||
"data": map[string]interface{}{ //nolint:gosec // base64 test fixture, not a real credential
|
||||
"password": "bmV3LXZhbHVl", // base64 encoded "new-value"
|
||||
"username": "YWRtaW4=", // base64 encoded "admin"
|
||||
},
|
||||
@@ -670,10 +670,9 @@ func TestCreateMirror_NoSyncAnnotations(t *testing.T) {
|
||||
constants.LabelEnabled: "true",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
constants.AnnotationSync: "true",
|
||||
constants.AnnotationTargetNamespaces: "app1,app2",
|
||||
constants.AnnotationExclude: "false",
|
||||
constants.AnnotationRecreateOnImmutableChange: "true",
|
||||
constants.AnnotationSync: "true",
|
||||
constants.AnnotationTargetNamespaces: "app1,app2",
|
||||
constants.AnnotationExclude: "false",
|
||||
},
|
||||
},
|
||||
Data: map[string][]byte{"key": []byte("value")},
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"slices"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
@@ -130,6 +129,13 @@ func (r *NamespaceReconciler) reconcileResourceType(ctx context.Context, rt conf
|
||||
continue
|
||||
}
|
||||
|
||||
// Skip excluded or paused sources. Excluded resources are torn down by the
|
||||
// source reconciler; paused resources must stay frozen. In both cases the
|
||||
// namespace reconciler must not create, update, or delete their mirrors.
|
||||
if annotations[constants.AnnotationExclude] == "true" || isPaused(source) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Resolve target namespaces for this source
|
||||
targetNamespaces, err := r.resolveTargetNamespaces(ctx, source)
|
||||
if err != nil {
|
||||
@@ -160,38 +166,10 @@ func (r *NamespaceReconciler) reconcileResourceType(ctx context.Context, rt conf
|
||||
"targetNamespace", namespaceName,
|
||||
"resourceType", rt.String())
|
||||
} else {
|
||||
// Namespace is no longer a target - check if mirror exists and delete it
|
||||
mirror := &unstructured.Unstructured{}
|
||||
mirror.SetGroupVersionKind(source.GroupVersionKind())
|
||||
mirror.SetNamespace(namespaceName)
|
||||
mirror.SetName(source.GetName())
|
||||
|
||||
err := r.Get(ctx, client.ObjectKey{Namespace: namespaceName, Name: source.GetName()}, mirror)
|
||||
if errors.IsNotFound(err) {
|
||||
// No mirror exists, nothing to clean up
|
||||
continue
|
||||
}
|
||||
// Namespace is no longer a target - delete the mirror if we own it.
|
||||
outcome, err := deleteOwnedMirror(ctx, r.Client, source.GroupVersionKind(),
|
||||
namespaceName, source.GetName(), source.GetNamespace(), source.GetName())
|
||||
if err != nil {
|
||||
logger.Error(err, "failed to check for mirror",
|
||||
"source", source.GetName(),
|
||||
"namespace", namespaceName)
|
||||
errorCount++
|
||||
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 != source.GetNamespace() || srcName != source.GetName() {
|
||||
continue
|
||||
}
|
||||
|
||||
// This mirror should be deleted (namespace no longer a valid target)
|
||||
if err := r.Delete(ctx, mirror); err != nil {
|
||||
logger.Error(err, "failed to delete orphaned mirror",
|
||||
"source", source.GetName(),
|
||||
"sourceNamespace", source.GetNamespace(),
|
||||
@@ -199,89 +177,27 @@ func (r *NamespaceReconciler) reconcileResourceType(ctx context.Context, rt conf
|
||||
errorCount++
|
||||
continue
|
||||
}
|
||||
|
||||
reconciledCount++
|
||||
logger.V(1).Info("deleted orphaned mirror due to namespace label change",
|
||||
"source", source.GetName(),
|
||||
"sourceNamespace", source.GetNamespace(),
|
||||
"targetNamespace", namespaceName,
|
||||
"resourceType", rt.String())
|
||||
if outcome == mirrorDeleted {
|
||||
reconciledCount++
|
||||
logger.V(1).Info("deleted orphaned mirror due to namespace label change",
|
||||
"source", source.GetName(),
|
||||
"sourceNamespace", source.GetNamespace(),
|
||||
"targetNamespace", namespaceName,
|
||||
"resourceType", rt.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return reconciledCount, errorCount, nil
|
||||
}
|
||||
|
||||
// resolveTargetNamespaces determines which namespaces should receive mirrors for a source.
|
||||
// Uses the same logic as SourceReconciler.resolveTargetNamespaces.
|
||||
// resolveTargetNamespaces determines which namespaces should receive mirrors for
|
||||
// a source. It delegates to SourceReconciler so target-resolution logic
|
||||
// (pattern parsing/validation, namespace listing, max-targets clamping) lives in
|
||||
// exactly one place, mirroring the reconcileMirror delegation below.
|
||||
func (r *NamespaceReconciler) resolveTargetNamespaces(ctx context.Context, source *unstructured.Unstructured) ([]string, error) {
|
||||
annotations := source.GetAnnotations()
|
||||
if annotations == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
targetNsAnnotation := annotations[constants.AnnotationTargetNamespaces]
|
||||
if targetNsAnnotation == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Parse patterns
|
||||
patterns := filter.ParseTargetNamespaces(targetNsAnnotation)
|
||||
if len(patterns) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Validate patterns and log warnings for invalid ones
|
||||
validationResults, allValid := filter.ValidatePatterns(patterns)
|
||||
if !allValid {
|
||||
logger := log.FromContext(ctx)
|
||||
invalidPatterns := filter.InvalidPatterns(validationResults)
|
||||
for _, invalid := range invalidPatterns {
|
||||
logger.Info("invalid glob pattern in target-namespaces annotation, pattern will be skipped",
|
||||
"pattern", invalid.Pattern,
|
||||
"error", invalid.Error.Error(),
|
||||
"source", source.GetName(),
|
||||
"namespace", source.GetNamespace(),
|
||||
)
|
||||
}
|
||||
|
||||
// Filter to only valid patterns
|
||||
var validPatterns []string
|
||||
for _, result := range validationResults {
|
||||
if result.Valid {
|
||||
validPatterns = append(validPatterns, result.Pattern)
|
||||
}
|
||||
}
|
||||
patterns = validPatterns
|
||||
|
||||
// If no valid patterns remain, return empty
|
||||
if len(patterns) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Get all namespace info in a single API call (more efficient than 3 separate calls)
|
||||
nsInfo, err := r.NamespaceLister.ListNamespacesWithLabels(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list namespaces: %w", err)
|
||||
}
|
||||
|
||||
// Resolve target namespaces using the pre-categorized namespace info
|
||||
targetNamespaces := filter.ResolveTargetNamespaces(
|
||||
patterns,
|
||||
nsInfo.All,
|
||||
nsInfo.AllowMirrors,
|
||||
nsInfo.OptOut,
|
||||
source.GetNamespace(),
|
||||
r.Filter,
|
||||
)
|
||||
|
||||
// Enforce max targets limit
|
||||
if r.Config != nil && r.Config.MaxTargetsPerResource > 0 && len(targetNamespaces) > r.Config.MaxTargetsPerResource {
|
||||
targetNamespaces = targetNamespaces[:r.Config.MaxTargetsPerResource]
|
||||
}
|
||||
|
||||
return targetNamespaces, nil
|
||||
return r.newSourceReconciler(source.GroupVersionKind()).
|
||||
resolveTargetNamespaces(ctx, source)
|
||||
}
|
||||
|
||||
// reconcileMirror creates or updates a mirror in the target namespace by
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// Package controller implements the kubemirror reconciliation logic.
|
||||
package controller
|
||||
|
||||
import "k8s.io/apimachinery/pkg/runtime/schema"
|
||||
|
||||
// gvkControllerName builds the unique controller-runtime controller name for a
|
||||
// GVK. Including version and group avoids collisions across API versions, e.g.
|
||||
// "HorizontalPodAutoscaler.v1.autoscaling" or "Secret.v1." (empty group for
|
||||
// core resources). Source and mirror reconcilers for the same GVK must differ,
|
||||
// so mirror controllers get a "-mirror" suffix. This is the single source of
|
||||
// truth for the convention shared by both reconcilers.
|
||||
func gvkControllerName(gvk schema.GroupVersionKind, mirror bool) string {
|
||||
name := gvk.Kind + "." + gvk.Version + "." + gvk.Group
|
||||
if mirror {
|
||||
name += "-mirror"
|
||||
}
|
||||
return name
|
||||
}
|
||||
@@ -214,6 +214,15 @@ func (r *SourceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Respect a per-source pause: freeze existing mirrors in place. We skip all
|
||||
// mirror create/update and orphan cleanup, leaving any prior mirrors and the
|
||||
// finalizer untouched until the annotation is removed. Deletion and disabling
|
||||
// are handled above and intentionally take precedence over pause.
|
||||
if isPaused(sourceObj) {
|
||||
logger.V(1).Info("source is paused, leaving existing mirrors unchanged")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Add finalizer if not present
|
||||
if !slices.Contains(sourceObj.GetFinalizers(), constants.FinalizerName) {
|
||||
logger.Info("adding finalizer to source resource")
|
||||
@@ -445,32 +454,16 @@ func (r *SourceReconciler) deleteAllMirrors(ctx context.Context, sourceObj metav
|
||||
continue
|
||||
}
|
||||
|
||||
existing := &unstructured.Unstructured{}
|
||||
existing.SetGroupVersionKind(sourceUnstructured.GroupVersionKind())
|
||||
getErr := r.Get(ctx, client.ObjectKey{Namespace: ns, Name: sourceObj.GetName()}, existing)
|
||||
if errors.IsNotFound(getErr) {
|
||||
continue
|
||||
}
|
||||
if getErr != nil {
|
||||
logger.Error(getErr, "failed to fetch potential mirror", "namespace", ns)
|
||||
deleteErrs = append(deleteErrs, fmt.Errorf("get mirror %s/%s: %w", ns, sourceObj.GetName(), getErr))
|
||||
continue
|
||||
}
|
||||
|
||||
if !IsManagedByUs(existing) {
|
||||
continue
|
||||
}
|
||||
srcNs, srcName, _, found := GetSourceReference(existing)
|
||||
if !found || srcNs != sourceObj.GetNamespace() || srcName != sourceObj.GetName() {
|
||||
continue
|
||||
}
|
||||
|
||||
if delErr := r.Delete(ctx, existing); delErr != nil && !errors.IsNotFound(delErr) {
|
||||
outcome, delErr := deleteOwnedMirror(ctx, r.Client, sourceUnstructured.GroupVersionKind(),
|
||||
ns, sourceObj.GetName(), sourceObj.GetNamespace(), sourceObj.GetName())
|
||||
if delErr != nil {
|
||||
logger.Error(delErr, "failed to delete mirror", "namespace", ns)
|
||||
deleteErrs = append(deleteErrs, fmt.Errorf("delete mirror %s/%s: %w", ns, sourceObj.GetName(), delErr))
|
||||
continue
|
||||
}
|
||||
deleteCount++
|
||||
if outcome == mirrorDeleted {
|
||||
deleteCount++
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("deleted mirrors", "count", deleteCount, "errors", len(deleteErrs))
|
||||
@@ -516,41 +509,16 @@ func (r *SourceReconciler) cleanupOrphanedMirrors(ctx context.Context, sourceObj
|
||||
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
|
||||
}
|
||||
outcome, err := deleteOwnedMirror(ctx, r.Client, sourceUnstructured.GroupVersionKind(),
|
||||
ns, sourceObj.GetName(), sourceObj.GetNamespace(), sourceObj.GetName())
|
||||
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)
|
||||
if outcome == mirrorDeleted {
|
||||
deletedCount++
|
||||
logger.V(1).Info("deleted orphaned mirror", "namespace", ns)
|
||||
}
|
||||
}
|
||||
|
||||
return deletedCount, nil
|
||||
@@ -662,7 +630,10 @@ func isBlacklistedSecret(obj *unstructured.Unstructured) bool {
|
||||
return slices.Contains(constants.BlacklistedSecretTypes, secretType)
|
||||
}
|
||||
|
||||
// isEnabledForMirroring checks if a resource has both the label and annotation for mirroring.
|
||||
// isEnabledForMirroring reports whether a resource should be mirrored. A resource
|
||||
// must carry both the enabled label and the sync annotation, and must not be
|
||||
// explicitly excluded. An excluded resource (exclude="true") is treated as
|
||||
// disabled, so the reconcile loop tears down any mirrors it previously created.
|
||||
func isEnabledForMirroring(obj metav1.Object) bool {
|
||||
// Check label
|
||||
labels := obj.GetLabels()
|
||||
@@ -676,9 +647,23 @@ func isEnabledForMirroring(obj metav1.Object) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Explicit opt-out always wins over the enable label/annotation.
|
||||
if annotations[constants.AnnotationExclude] == "true" {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// isPaused reports whether a source has the pause annotation set. A paused
|
||||
// source is frozen: its existing mirrors are left exactly as they are (no
|
||||
// updates, no orphan cleanup) until the annotation is removed. Unlike
|
||||
// isEnabledForMirroring returning false, pausing does not delete mirrors.
|
||||
func isPaused(obj metav1.Object) bool {
|
||||
annotations := obj.GetAnnotations()
|
||||
return annotations != nil && annotations[constants.AnnotationPaused] == "true"
|
||||
}
|
||||
|
||||
// SetupWithManagerForResourceType sets up a controller for a specific resource type.
|
||||
// This allows dynamic controller registration for any discovered resource type.
|
||||
func (r *SourceReconciler) SetupWithManagerForResourceType(
|
||||
@@ -689,10 +674,7 @@ func (r *SourceReconciler) SetupWithManagerForResourceType(
|
||||
obj := &unstructured.Unstructured{}
|
||||
obj.SetGroupVersionKind(gvk)
|
||||
|
||||
// Create unique controller name including version and group to avoid collisions
|
||||
// e.g., "HorizontalPodAutoscaler.v1.autoscaling" or "Secret.v1." (empty group for core resources)
|
||||
// This matches the naming convention used by mirror reconcilers
|
||||
controllerName := gvk.Kind + "." + gvk.Version + "." + gvk.Group
|
||||
controllerName := gvkControllerName(gvk, false)
|
||||
|
||||
// Create mirror object for watching
|
||||
mirrorObj := &unstructured.Unstructured{}
|
||||
|
||||
@@ -205,6 +205,36 @@ func TestIsEnabledForMirroring(t *testing.T) {
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "excluded overrides enabled label and sync annotation",
|
||||
obj: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
constants.LabelEnabled: "true",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
constants.AnnotationSync: "true",
|
||||
constants.AnnotationExclude: "true",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "exclude set to false does not disable mirroring",
|
||||
obj: &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{
|
||||
constants.LabelEnabled: "true",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
constants.AnnotationSync: "true",
|
||||
constants.AnnotationExclude: "false",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -215,6 +245,47 @@ func TestIsEnabledForMirroring(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPaused(t *testing.T) {
|
||||
tests := []struct {
|
||||
obj metav1.Object
|
||||
name string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "paused true",
|
||||
obj: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{constants.AnnotationPaused: "true"},
|
||||
}},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "paused false",
|
||||
obj: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{constants.AnnotationPaused: "false"},
|
||||
}},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "annotation absent",
|
||||
obj: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{
|
||||
Annotations: map[string]string{constants.AnnotationSync: "true"},
|
||||
}},
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "no annotations",
|
||||
obj: &corev1.Secret{ObjectMeta: metav1.ObjectMeta{}},
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.want, isPaused(tt.obj))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSourceReconciler_resolveTargetNamespaces(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -603,7 +674,7 @@ func TestSourceReconciler_Reconcile_AnnotationChange_AllToAllLabeled(t *testing.
|
||||
constants.FinalizerName,
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"data": map[string]interface{}{ //nolint:gosec // base64 test fixture, not a real credential
|
||||
"password": "c2VjcmV0",
|
||||
},
|
||||
},
|
||||
@@ -632,7 +703,7 @@ func TestSourceReconciler_Reconcile_AnnotationChange_AllToAllLabeled(t *testing.
|
||||
constants.AnnotationSourceUID: "source-uid-123",
|
||||
},
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"data": map[string]interface{}{ //nolint:gosec // base64 test fixture, not a real credential
|
||||
"password": "c2VjcmV0",
|
||||
},
|
||||
},
|
||||
@@ -735,6 +806,53 @@ func TestSourceReconciler_Reconcile_AnnotationChange_AllToAllLabeled(t *testing.
|
||||
mockLister.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestSourceReconciler_Reconcile_Paused_LeavesMirrorsUntouched(t *testing.T) {
|
||||
// A paused source must early-return: no List, Create, Delete, or Update.
|
||||
// The MockClient panics on any un-mocked call, so wiring only the source Get
|
||||
// proves Reconcile touches nothing else.
|
||||
source := &unstructured.Unstructured{
|
||||
Object: map[string]any{
|
||||
"apiVersion": "v1",
|
||||
"kind": "Secret",
|
||||
"metadata": map[string]any{
|
||||
"name": "test-secret",
|
||||
"namespace": "default",
|
||||
"uid": "source-uid-123",
|
||||
"labels": map[string]any{
|
||||
constants.LabelEnabled: "true",
|
||||
},
|
||||
"annotations": map[string]any{
|
||||
constants.AnnotationSync: "true",
|
||||
constants.AnnotationTargetNamespaces: "all",
|
||||
constants.AnnotationPaused: "true",
|
||||
},
|
||||
"finalizers": []any{constants.FinalizerName},
|
||||
},
|
||||
"data": map[string]any{"password": "c2VjcmV0"}, //nolint:gosec // base64 test fixture, not a real credential
|
||||
},
|
||||
}
|
||||
|
||||
mockClient := new(MockClient)
|
||||
// Only the source read is expected; nothing else.
|
||||
mockClient.On("Get", mock.Anything, types.NamespacedName{Namespace: "default", Name: "test-secret"}, mock.Anything).
|
||||
Return(nil, source).Once()
|
||||
|
||||
r := &SourceReconciler{
|
||||
Client: mockClient,
|
||||
Scheme: runtime.NewScheme(),
|
||||
Config: &config.Config{},
|
||||
Filter: filter.NewNamespaceFilter(nil, nil),
|
||||
GVK: schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Secret"},
|
||||
}
|
||||
|
||||
result, err := r.Reconcile(context.Background(),
|
||||
ctrl.Request{NamespacedName: types.NamespacedName{Namespace: "default", Name: "test-secret"}})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, ctrl.Result{}, result)
|
||||
mockClient.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestSourceReconciler_Reconcile_AnnotationChange_PatternChange(t *testing.T) {
|
||||
// Scenario: annotation changes from "app-*" → "prod-*"
|
||||
// Before: mirrors in app-1, app-2, app-3
|
||||
|
||||
Reference in New Issue
Block a user