An Argo CD type Cluster Generator (ApplicationSets) for Flux ResourceSets
The flux-cluster-generator is a lightweight Kubernetes controller that watches for Flux KubeConfig reference Secrets. For every Flux KubeConfig reference Secret that meet specific, configurable criteria, it creates and manages Flux ResourceSetInputProviders derived from those Secrets.
The controller watches for Kubernetes Secrets that:
- Contain a valid KubeConfig (for use as a Flux KubeConfig reference for HelmRelease and/or Kustomization resources).
- Match a configurable label selector (default:
fluxcd.io/secret-type=cluster). - Optionally watch namespaces that match a configurable label selector (e.g.
flux-cluster-generator-enabled=true)
For each matching Secret, the controller creates a corresponding ResourceSetInputProvider (RSIP).
- The generated RSIP includes the Secret’s name, namespace, and KubeConfig key as defaultValues and as
.metadata.labels. - Optional extra defaultValues can also be derived from the Secret’s labels, based on configuration (see below).
Flux supports ResourceSets for multi-cluster GitOps workflows. However, the official examples are static—you must manually add or remove cluster inputs in each ResourceSet definition whenever clusters are added or removed.
The flux-cluster-generator brings the same dynamic behavior that Argo CD’s Cluster Generator for ApplicationSets provides:
- When a new Kubernetes cluster is represented as a Flux KubeConfig reference
Secret, the controller automatically generates an RSIP. ResourceSetscan then be reused across all clusters, without requiring per-cluster manual edits.- When a cluster is removed (and its Secret deleted), the corresponding RSIP is also cleaned up.
This enables dynamic, GitOps-driven multi-cluster resource management.
While originally designed for dynamic vCluster environments (and paired with the vcluster-platform-flux-secret-controller), it can be used with any Kubernetes Flux KubeConfig reference Secret.
-
Dynamic vCluster environments
Originally built for vCluster Platform
VirtualClusterInstanceswhere it is common to have ephemeral or short-lived vCluster instances appear and disappear frequently. And when paired with vcluster-platform-flux-secret-controller, the Flux KubeConfig referenceSecretwill be created when aVirtualClusterInstanceis created with a label selector. -
Any Flux-managed clusters
Works with any Kubernetes Secret that Flux recognizes as a KubeConfig reference, making it useful outside of vCluster as well.
- Watches
Secretobjects that match:- A configurable label selector (e.g.
fluxcd.io/secret-type=cluster) - An optional namespace label selector
- Or explicit namespaces in
--watch-namespaces
- A configurable label selector (e.g.
- Creates/updates a
ResourceSetInputProvider(RSIP) per matchingSecretin a target namespace - Ensures RSIPs are deleted when their source
Secretis removed or no longer matches - Copies selected labels and prefixes from the source
Secretinto the RSIP as labels anddefaultValues- The labels enable the triggering of one or more
ResourceSetsbased on the `inputsFrom label selector. - The
SecretbaseddefaultValuesprovidesinputsto be used in theResourceSettemplated resources.
- The labels enable the triggering of one or more
flux-cluster-generator is packaged as a Helm chart and published as an OCI artifact.
Add the OCI Helm repository (Flux can read OCI directly):
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: flux-cluster-generator
namespace: flux-cluster-generator
spec:
interval: 12h
type: oci
url: oci://ghcr.io/loft-demos/chartsDeploy with a HelmRelease:
apiVersion: helm.toolkit.fluxcd.io/v2beta2
kind: HelmRelease
metadata:
name: flux-cluster-generator
namespace: flux-cluster-generator
spec:
releaseName: flux-cluster-generator
targetNamespace: flux-cluster-generator
chart:
spec:
chart: flux-cluster-generator
version: "0.1.1" # change to the desired version
sourceRef:
kind: HelmRepository
name: flux-cluster-generator
interval: 30m
install:
createNamespace: true
values:
args:
copyLabelKeys: "env,team,,app-subdomain"
namespaceLabelSelector: "flux-cluster-generator-enabled=true"The controller supports flags for tuning and customization (all flags are exposed as Helm values):
--rsip-namespace: Target namespace for RSIPs--label-selector: Label selector for source secrets--secret-key: Key in secret data containing kubeconfig (defaultvalue)--rsip-name-prefix: Prefix for generated RSIP names (default input--copy-label-keys: Comma-separated label keys to copy into RSIP--copy-label-prefixes: Comma-separated label KEY PREFIXES to copy into RSIP (e.g. flux-app/)--rsip-name-template: Optional template for RSIP names (default falls back to prefix + project + cluster)--namespace-label-selector: Label selector for Namespaces to include (e.g. flux-cluster-generator-enabled=true)--watch-namespaces: Comma-separated namespaces to watch (empty = all)
Config:
containers:
- name: flux-cluster-generator
image: ghcr.io/loft-demos/flux-cluster-generator:0.1.1
args:
- --rsip-namespace=flux-apps
- --label-selector=fluxcd.io/secret-type=cluster
- --secret-key=value
- --rsip-name-prefix=inputs-
- --cluster-name-label-key=vci.flux.loft.sh/name
- --project-label-key=vci.flux.loft.sh/project
- --copy-label-keys=env,team,,app-subdomain
- --copy-label-prefixes=flux-app/
- --namespace-label-selector=flux-cluster-generator-enabled=true
- --max-concurrent=2
- --cache-sync-seconds=120
- --zap-log-level=infoSecret (and Namespace) that matches the above config:
apiVersion: v1
kind: Namespace
metadata:
name: flux-apps
labels:
flux-cluster-generator-enabled: 'true'
---
apiVersion: v1
kind: Secret
metadata:
name: vci-vcluster-flux-demo-flux-cluster-generator-demo-kubeconfig
namespace: flux-apps
labels:
app-subdomain: beta.us.demo.dev
app.kubernetes.io/managed-by: vcluster-platform-flux-secret-controller
env: dev
flux-app/hello-world: 'false'
flux-app/podinfo: 'true'
fluxcd.io/kubeconfig: 'true'
fluxcd.io/secret-type: cluster
vci.flux.loft.sh/name: flux-cluster-generator-demo
vci.flux.loft.sh/namespace: p-vcluster-flux-demo
vci.flux.loft.sh/project: vcluster-flux-demo
vcluster.com/import-fluxcd: 'true'
annotations:
vci.flux.loft.sh/kcfg-sha256: 60ca29a7e7ce91d0e6d899b296760c2e170d01589697f0cfcef49b8b9bcee48c
data:
value: eyJ...xXf==
type: OpaqueGenerated ResourceSetInputProvider:
apiVersion: fluxcd.controlplane.io/v1
kind: ResourceSetInputProvider
metadata:
labels:
app-subdomain: beta.acme.com
env: dev
flux-app/hello-world: 'false'
flux-app/podinfo: 'true'
mirror.fluxcd.io/clusterName: flux-cluster-generator-demo
mirror.fluxcd.io/managed: 'true'
mirror.fluxcd.io/project: vcluster-flux-demo
mirror.fluxcd.io/secretKey: value
mirror.fluxcd.io/secretNS: flux-apps
mirror.fluxcd.io/secretName: vci-vcluster-flux-demo-flux-cluster-generator-demo-kubeconfig
name: inputs-vcluster-flux-demo-flux-cluster-generator-demo
namespace: flux-apps
spec:
defaultValues:
appSubdomain: beta.acme.com
env: dev
fluxAppHelloWorld: 'false'
fluxAppPodinfo: 'true'
kubeSecretKey: value
kubeSecretNS: flux-apps
kubeSecretName: vci-vcluster-flux-demo-flux-cluster-generator-demo-kubeconfig
name: flux-cluster-generator-demo
project: vcluster-flux-demo
type: StaticExample ResourceSet trigged by the above RSIP:
apiVersion: fluxcd.controlplane.io/v1
kind: ResourceSet
metadata:
labels:
env: dev
flux-app/podinfo: 'true'
name: podinfo
namespace: flux-apps
spec:
inputsFrom:
- apiVersion: fluxcd.controlplane.io/v1
kind: ResourceSetInputProvider
selector:
matchLabels:
flux-app/podinfo: 'true'
resources:
- apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: HelmRepository
metadata:
name: podinfo
namespace: flux-apps
spec:
interval: 12h
type: oci
url: oci://ghcr.io/stefanprodan/charts
- apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
labels:
env: dev
flux-app/podinfo: 'true'
mirror.fluxcd.io/clusterName: << inputs.name >>
name: podinfo-<< inputs.project >>-<< inputs.name >>
namespace: flux-apps
spec:
chart:
spec:
chart: podinfo
sourceRef:
kind: HelmRepository
name: podinfo
namespace: flux-apps
install:
createNamespace: true
remediation:
retries: 4
interval: 1m
kubeConfig:
secretRef:
key: << inputs.kubeSecretKey >>
name: << inputs.kubeSecretName >>
releaseName: podinfo
storageNamespace: podinfo
targetNamespace: podinfo
values:
ingress:
enabled: true
hosts:
- host: podinfo-<< inputs.name >>-<< inputs.project >>-<< inputs.appSubdomain >>
paths:
- path: /
pathType: ImplementationSpecific
replicaCount: 2
ui:
color: '#3455ff'