diff --git a/README.md b/README.md index b65fee8..0cfe343 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/charts/kubemirror/README.md b/charts/kubemirror/README.md new file mode 100644 index 0000000..83445d2 --- /dev/null +++ b/charts/kubemirror/README.md @@ -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. diff --git a/examples/README.md b/examples/README.md index 75ba45a..a9a6699 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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.