shoot-grafter is a Kubernetes operator that automates the onboarding of Gardener Shoots (Kubernetes clusters managed by Gardener) to Greenhouse, the cloud operations platform. It bridges the gap between Gardener-managed infrastructure and Greenhouse's centralized cluster management by dynamically discovering and registering Shoots as Greenhouse Clusters.
The name "shoot-grafter" continues Gardener's and Greenhouse's botanical theme: just as a gardener grafts shoots onto rootstock to create robust plants, this operator grafts (connects) Gardener Shoots onto Greenhouse's centralized platform to create fully operational k8s clusters.
This project is part of the NeoNephos Foundation.
shoot-grafter continuously monitors Garden clusters for Shoots matching specific criteria and automatically:
- Discovers Shoots: Watches for Gardener Shoot resources in specified namespaces based on label selectors
- Extracts cluster credentials: Retrieves API server URLs and CA certificates from Shoot resources
- Creates Greenhouse Clusters: Automatically registers discovered Shoots as Greenhouse Cluster resources
- Propagates labels: Transfers specified labels from Shoots to Greenhouse Clusters for consistent organization
- Configures OIDC authentication: Optionally configures OIDC authentication on Shoot clusters to enable Greenhouse authentication
- Configures RBAC: Optionally sets up role-based access control on Shoot clusters for Greenhouse service accounts
- Maintains synchronization: Keeps Greenhouse Cluster resources in sync with their corresponding Shoots
shoot-grafter currently only creates clusters matching Shoots but does not automatically clean up clusters when Shoot labels change or Shoots are deleted. Manual cleanup of Greenhouse Cluster resources is required in these scenarios.
The operator consists of two main controllers:
The CareInstruction is the primary Custom Resource Definition (CRD) that configures how shoot-grafter operates. Each CareInstruction:
- Defines which Garden cluster to monitor (via kubeconfig secret or Greenhouse Cluster reference)
- Specifies which namespace to watch for Shoots
- Declares label selectors to filter which Shoots to onboard
- Configures label propagation and additional metadata
- Manages the lifecycle of dynamically created Shoot controllers
Key features:
- Dynamically spawns and manages Shoot controllers for each CareInstruction
- Monitors Garden cluster accessibility
- Tracks onboarding status and statistics
- Handles cleanup when CareInstructions are deleted
For each CareInstruction, a dedicated Shoot controller is dynamically created and runs in its own manager. This controller:
- Watches Shoot resources in the specified Garden cluster namespace
- Extracts cluster connection details (API server URL, CA certificate)
- Creates or updates corresponding Secret resources with OIDC configuration
- Generates Greenhouse Cluster resources with appropriate labels
- Optionally configures OIDC authentication on Shoot clusters for Greenhouse access. Also see respective Greenhouse docs and Gardener docs
- Optionally configures RBAC on the Shoot cluster for Greenhouse access
A CareInstruction defines the configuration for onboarding Shoots from a specific Garden cluster.
apiVersion: shoot-grafter.cloudoperators.dev/v1alpha1
kind: CareInstruction
metadata:
name: production-shoots
namespace: greenhouse-team
spec:
# Option 1: Reference a Greenhouse Cluster resource
gardenClusterName: garden-prod-cluster
# Option 2: Reference a kubeconfig secret directly
# gardenClusterKubeConfigSecretName:
# name: garden-kubeconfig
# key: kubeconfig
# Namespace in the Garden cluster to watch
gardenNamespace: garden-production
# Label selector for Shoots to onboard
shootSelector:
matchLabels:
environment: production
team: platform
shoot.gardener.cloud/status: "healthy"
# Labels to propagate from Shoot to Greenhouse Cluster
propagateLabels:
- region
- cost-center
- business-unit
# Additional labels to add to all created Clusters
additionalLabels:
managed-by: shoot-grafter
onboarding-source: garden-prod
# Reference to AuthenticationConfiguration ConfigMap (optional)
authenticationConfigMapName: greenhouse-oidc-config
# Enable automatic RBAC configuration (default: true)
enableRBAC: true| Field | Type | Required | Description |‚
|-------|------|----------|-------------|
| gardenClusterName | string | No*| Name of the Greenhouse Cluster resource representing the Garden cluster |
| gardenClusterKubeConfigSecretName | SecretKeyReference | No* | Reference to a secret containing the kubeconfig for the Garden cluster |
| gardenNamespace | string | Yes | Namespace in the Garden cluster where Shoots are located |
| shootSelector | LabelSelector | No | Label selector to filter which Shoots to onboard (if omitted, all Shoots in namespace are selected). It is recommended to always use shoot.gardener.cloud/status: "healthy" to only onboard healthy Shoots. |
| propagateLabels | []string | No | List of label keys to copy from Shoot to Greenhouse Cluster |
| additionalLabels | map[string]string | No | Additional labels to add to all created Greenhouse Clusters |
| authenticationConfigMapName | string | No | Name of ConfigMap in Greenhouse cluster containing AuthenticationConfiguration (config.yaml with apiserver.config.k8s.io/v1beta1 content)|
| enableRBAC | bool | No | When false, skips automatic RBAC setup on Shoot clusters (default: true‚) |
*Note: Either gardenClusterName or gardenClusterKubeConfigSecretName must be provided (priority: kubeconfig secret > cluster name)
The CareInstruction status provides real-time information about the onboarding process:
status:
statusConditions:
conditions:
- type: Ready
status: "True"
reason: Ready
message: CareInstruction is ready
- type: GardenClusterAccessReady
status: "True"
reason: GardenClusterAccessReady
- type: ShootControllerStarted
status: "True"
reason: Started
- type: ShootsReconciled
status: "True"
reason: Reconciled
message: All shoots and clusters are reconciled
clusters:
- name: shoot-cluster-1
status: Ready
- name: shoot-cluster-2
status: Ready
- name: shoot-cluster-3
status: Failed
message: Cluster managed by different CareInstruction: other-careinstruction
totalShootCount: 15
createdClusters: 15
failedClusters: 1Status Fields:
Ready: Overall readiness of the CareInstruction (derived from sub-conditions)GardenClusterAccessReady: Indicates whether the Garden cluster is accessibleShootControllerStarted: Shows if the dynamic Shoot controller has been startedShootsReconciled: Reports whether all targeted Shoots have been successfully onboardedclusters: Detailed list of all clusters managed by this CareInstruction with their individual status (Ready/Failed) and optional messagetotalShootCount: Total number of Shoots matched by the selectorcreatedClusters: Number of Greenhouse Clusters created by this CareInstructionfailedClusters: Number of Greenhouse Clusters that are not ready
apiVersion: shoot-grafter.cloudoperators/v1alpha1
kind: CareInstruction
metadata:
name: all-dev-shoots
namespace: greenhouse-dev
spec:
gardenClusterName: dev-garden
gardenNamespace: garden--dev
shootSelector:
matchLabels:
shoot.gardener.cloud/status: "healthy"apiVersion: shoot-grafter.cloudoperators/v1alpha1
kind: CareInstruction
metadata:
name: production-critical
namespace: greenhouse-prod
spec:
gardenClusterName: prod-garden
gardenNamespace: garden--production
shootSelector:
matchLabels:
environment: production
shoot.gardener.cloud/status: "healthy"
propagateLabels:
- region
- owned-by
additionalLabels:
monitoring: enabled
backup: dailyapiVersion: shoot-grafter.cloudoperators/v1alpha1
kind: CareInstruction
metadata:
name: multi-env-shoots
namespace: greenhouse
spec:
gardenClusterName: my-garden
gardenNamespace: garden--myproject
shootSelector:
matchLabels:
shoot.gardener.cloud/status: "healthy"
matchExpressions:
- key: environment
operator: In
values:
- staging
- production
- key: owned-by
operator: Exists
propagateLabels:
- environment
- region
- owned-by
additionalLabels:
onboarding-method: shoot-grafterThis example shows how to configure OIDC authentication on Shoots to enable Greenhouse authentication:
apiVersion: shoot-grafter.cloudoperators/v1alpha1
kind: CareInstruction
metadata:
name: oidc-enabled-shoots
namespace: greenhouse-prod
spec:
gardenClusterName: prod-garden
gardenNamespace: garden--production
shootSelector:
matchLabels:
oidc-enabled: "true"
authenticationConfigMapRef: greenhouse-oidc-config
propagateLabels:
- environmentThe referenced ConfigMap should contain an AuthenticationConfiguration:
apiVersion: v1
kind: ConfigMap
metadata:
name: greenhouse-oidc-config
namespace: greenhouse-prod
data:
config.yaml: |
apiVersion: apiserver.config.k8s.io/v1beta1
kind: AuthenticationConfiguration
jwt:
- issuer:
url: https://oidc.greenhouse.example.com
audiences:
- greenhouse
claimMappings:
username:
claim: sub
prefix: "greenhouse:"
groups:
claim: groups
prefix: "greenhouse:"shoot-grafter emits Kubernetes events to help you monitor and debug the Shoot onboarding process. Events are associated with the CareInstruction resource.
To see all events related to a specific CareInstruction:
# View events in the resource description
kubectl describe careinstruction <careinstruction-name> -n <namespace>
# List all events for a specific CareInstruction
kubectl get events -n <namespace> \
--field-selector involvedObject.name=<careinstruction-name>
# Filter events by reason
kubectl get events -n <namespace> \
--field-selector involvedObject.name=<careinstruction-name>,reason=ShootReconciled
# Watch events in real-time
kubectl get events -n <namespace> \
--field-selector involvedObject.name=<careinstruction-name> \
--watchshoot-grafter emits the following events during Shoot reconciliation:
| Event Reason | Description |
|---|---|
ShootReconciling |
Reconciliation has started for a Shoot |
ShootReconciled |
Successfully completed reconciliation for a Shoot |
SecretCreated |
Created Greenhouse secret with cluster credentials |
SecretUpdated |
Updated existing Greenhouse secret with new credentials |
ShootDeleted |
Shoot was deleted from the Garden cluster |
| Event Reason | Description | Resolution |
|---|---|---|
APIServerURLMissing |
Shoot doesn't have an external API server URL | Check Shoot status in Garden cluster; ensure Shoot is fully reconciled |
CAConfigMapFetchFailed |
Failed to fetch CA certificate ConfigMap | Verify ConfigMap <shoot-name>.ca-cluster exists in Garden namespace |
CADataMissing |
CA certificate data is empty in ConfigMap | Check ConfigMap data contains valid ca.crt entry |
SecretOperationFailed |
Failed to create or update Greenhouse secret | Check RBAC permissions and Greenhouse cluster connectivity |
Kubernetes automatically cleans up events after a TTL period (typically 1 hour by default). This TTL can be configured via the --event-ttl flag on the kube-apiserver.
This project is open to feature requests/suggestions, bug reports etc. via GitHub issues. Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our Contribution Guidelines.
If you find any bug that may be a security problem, please follow our instructions at in our security policy on how to report it. Please do not create GitHub issues for security-related doubts or problems.
We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its Code of Conduct at all times.
Copyright 2025 SAP SE or an SAP affiliate company and shoot-grafter contributors. Please see our LICENSE for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available via the REUSE tool.