Merge pull request #17949 from rook/mergify/bp/release-1.20/pr-17917

helm: add multisite CR support to rook-ceph-cluster chart (backport #17917)
This commit is contained in:
Joshua Hoblitt authored and GitHub committed 2026-07-13 16:40:28 -07:00
commit 8f43115c69
9 files changed
+301 -14

No files matched your search

@@ -126,6 +126,22 @@ The `cephObjectStores` array in the values file will define a list of CephObject
| `ingress.tls` | Ingress tls | `/` |
| `ingress.ingressClassName` | Ingress tls | `""` |
### **Ceph Object Multisite**
The `cephObjectRealms`, `cephObjectZoneGroups`, and `cephObjectZones` arrays in the values file define
[RGW multisite](../Storage-Configuration/Object-Storage-RGW/ceph-object-multisite.md) resources. They are
commented out by default; uncomment and adjust them to deploy a multisite configuration, then point a
`cephObjectStores` entry at the zone via its `spec.zone.name`.
| Parameter | Description | Default |
| --------- | ----------- | ------- |
| `cephObjectRealms[].name` | The name of the CephObjectRealm | `realm-a` |
| `cephObjectRealms[].spec` | The CephObjectRealm spec, see the [CephObjectRealm CRD](../CRDs/Object-Storage/ceph-object-realm-crd.md) documentation. Omit to create a new realm; set `pull.endpoint` to sync a realm from another cluster. | not set |
| `cephObjectZoneGroups[].name` | The name of the CephObjectZoneGroup | `zonegroup-a` |
| `cephObjectZoneGroups[].spec` | The CephObjectZoneGroup spec, see the [CephObjectZoneGroup CRD](../CRDs/Object-Storage/ceph-object-zonegroup-crd.md) documentation. | see values.yaml |
| `cephObjectZones[].name` | The name of the CephObjectZone | `zone-a` |
| `cephObjectZones[].spec` | The CephObjectZone spec, see the [CephObjectZone CRD](../CRDs/Object-Storage/ceph-object-zone-crd.md) documentation. | see values.yaml |
### **Existing Clusters**
If you have an existing CephCluster CR that was created without the helm chart and you want the helm
@@ -163,6 +163,22 @@ The `cephObjectStores` array in the values file will define a list of CephObject
| `ingress.tls` | Ingress tls | `/` |
| `ingress.ingressClassName` | Ingress tls | `""` |
### **Ceph Object Multisite**
The `cephObjectRealms`, `cephObjectZoneGroups`, and `cephObjectZones` arrays in the values file define
[RGW multisite](../Storage-Configuration/Object-Storage-RGW/ceph-object-multisite.md) resources. They are
commented out by default; uncomment and adjust them to deploy a multisite configuration, then point a
`cephObjectStores` entry at the zone via its `spec.zone.name`.
| Parameter | Description | Default |
| --------- | ----------- | ------- |
| `cephObjectRealms[].name` | The name of the CephObjectRealm | `realm-a` |
| `cephObjectRealms[].spec` | The CephObjectRealm spec, see the [CephObjectRealm CRD](../CRDs/Object-Storage/ceph-object-realm-crd.md) documentation. Omit to create a new realm; set `pull.endpoint` to sync a realm from another cluster. | not set |
| `cephObjectZoneGroups[].name` | The name of the CephObjectZoneGroup | `zonegroup-a` |
| `cephObjectZoneGroups[].spec` | The CephObjectZoneGroup spec, see the [CephObjectZoneGroup CRD](../CRDs/Object-Storage/ceph-object-zonegroup-crd.md) documentation. | see values.yaml |
| `cephObjectZones[].name` | The name of the CephObjectZone | `zone-a` |
| `cephObjectZones[].spec` | The CephObjectZone spec, see the [CephObjectZone CRD](../CRDs/Object-Storage/ceph-object-zone-crd.md) documentation. | see values.yaml |
### **Existing Clusters**
If you have an existing CephCluster CR that was created without the helm chart and you want the helm
@@ -0,0 +1,13 @@
{{- $root := . -}}
{{- range $realm := .Values.cephObjectRealms }}
---
kind: CephObjectRealm
apiVersion: ceph.rook.io/v1
metadata:
name: {{ $realm.name }}
namespace: {{ $root.Release.Namespace }} # namespace:cluster
{{- with $realm.spec }}
spec:
{{- . | toYaml | nindent 2 }}
{{- end }}
{{- end }}
@@ -0,0 +1,11 @@
{{- $root := . -}}
{{- range $zone := .Values.cephObjectZones }}
---
kind: CephObjectZone
apiVersion: ceph.rook.io/v1
metadata:
name: {{ $zone.name }}
namespace: {{ $root.Release.Namespace }} # namespace:cluster
spec:
{{- $zone.spec | toYaml | nindent 2 }}
{{- end }}
@@ -0,0 +1,11 @@
{{- $root := . -}}
{{- range $zonegroup := .Values.cephObjectZoneGroups }}
---
kind: CephObjectZoneGroup
apiVersion: ceph.rook.io/v1
metadata:
name: {{ $zonegroup.name }}
namespace: {{ $root.Release.Namespace }} # namespace:cluster
spec:
{{- $zonegroup.spec | toYaml | nindent 2 }}
{{- end }}
@@ -698,6 +698,43 @@ cephObjectStores:
# - name: internal
# namespace: kube-system
# sectionName: https
## CephObjectRealm, CephObjectZoneGroup, and CephObjectZone configure RGW multisite. They are
## disabled by default; remove the comments and set desired values to enable them. See
## https://rook.io/docs/rook/latest/Storage-Configuration/Object-Storage-RGW/ceph-object-multisite/
## The example below creates a new realm on this (primary) cluster. On a secondary cluster that
## syncs from another realm, add a `spec.pull.endpoint` to the realm instead (see the realm CRD docs).
#cephObjectRealms:
# - name: realm-a
# # spec is only required to pull a realm from another cluster; omit it to create a new realm
# # spec:
# # pull:
# # endpoint: http://<master-zone-endpoint>
#cephObjectZoneGroups:
# - name: zonegroup-a
# spec:
# realm: realm-a
#cephObjectZones:
# - name: zone-a
# spec:
# zoneGroup: zonegroup-a
# metadataPool:
# failureDomain: host
# replicated:
# size: 3
# requireSafeReplicaSize: true
# dataPool:
# failureDomain: host
# replicated:
# size: 3
# requireSafeReplicaSize: true
# parameters:
# compression_mode: none
# bulk: "true"
# # recommended to set this value if ingress used for exposing rgw endpoints
# # customEndpoints:
# # - "http://rgw-a.fqdn"
# # if the pools need to be removed from backend enable following setting
# # preservePoolsOnDelete: false
## cephECBlockPools are disabled by default, please remove the comments and set desired values to enable it
## For erasure coded a replicated metadata pool is required.
## https://rook.io/docs/rook/latest/CRDs/Shared-Filesystem/ceph-filesystem-crd/#erasure-coded
@@ -285,6 +285,49 @@ func (h *CephInstaller) removeCephClusterHelmResources() {
if !h.k8shelper.WaitUntilPodWithLabelDeleted(fmt.Sprintf("rook_object_store=%s", ObjectStoreName), h.settings.Namespace) {
assert.Fail(h.T(), "rgw did not stop via helm uninstall")
}
if h.settings.UseMultisiteObjectStore {
h.removeMultisiteHelmResources(ObjectStoreName)
}
}
// removeMultisiteHelmResources tears down the CephObjectRealm, CephObjectZoneGroup,
// CephObjectZone, and shared rgw pools that CreateObjectStoreConfiguration added
// to the chart. They are not part of the chart's default storage CRs, so they
// must be removed explicitly or they orphan and trip the teardown pool check.
// Order matters: the zone's deletion finalizer runs radosgw-admin against realm
// metadata stored in the .rgw.root pool, so the store (already deleted above),
// zone, zone group, and realm must all be gone before their pools are removed.
func (h *CephInstaller) removeMultisiteHelmResources(name string) {
ctx := context.TODO()
ceph := h.k8shelper.RookClientset.CephV1()
ns := h.settings.Namespace
deleteAndWait := func(kind, crName string, del func() error, get func() error) {
if err := del(); err != nil {
assert.True(h.T(), kerrors.IsNotFound(err), "unexpected error deleting %s %q", kind, crName)
return
}
if err := h.k8shelper.WaitForCustomResourceDeletion(ns, crName, get); err != nil {
assert.NoError(h.T(), err, "failed waiting for %s %q deletion", kind, crName)
}
}
deleteAndWait("CephObjectZone", name,
func() error { return ceph.CephObjectZones(ns).Delete(ctx, name, v1.DeleteOptions{}) },
func() error { _, err := ceph.CephObjectZones(ns).Get(ctx, name, v1.GetOptions{}); return err })
deleteAndWait("CephObjectZoneGroup", name,
func() error { return ceph.CephObjectZoneGroups(ns).Delete(ctx, name, v1.DeleteOptions{}) },
func() error { _, err := ceph.CephObjectZoneGroups(ns).Get(ctx, name, v1.GetOptions{}); return err })
deleteAndWait("CephObjectRealm", name,
func() error { return ceph.CephObjectRealms(ns).Delete(ctx, name, v1.DeleteOptions{}) },
func() error { _, err := ceph.CephObjectRealms(ns).Get(ctx, name, v1.GetOptions{}); return err })
for poolName := range rgwSharedPools(name) {
deleteAndWait("CephBlockPool", poolName,
func() error { return ceph.CephBlockPools(ns).Delete(ctx, poolName, v1.DeleteOptions{}) },
func() error { _, err := ceph.CephBlockPools(ns).Get(ctx, poolName, v1.GetOptions{}); return err })
}
}
// ConfirmHelmClusterInstalledCorrectly runs some validation to check whether the helm chart installed correctly.
@@ -381,8 +424,32 @@ func (h *CephInstaller) CreateFileSystemConfiguration(values map[string]interfac
return nil
}
// CreateObjectStoreConfiguration creates an object store configuration
// rgwSharedPools maps the CephBlockPool CR name to its rados pool name (empty
// means the operator derives the pool name from the CR name) for the multisite
// object store configured by CreateObjectStoreConfiguration. .rgw.root is the
// realm-global metadata pool; the operator sets pg_num_min=8 on it, so it must
// be created with pg_num>=8 or the zone reconcile fails.
func rgwSharedPools(name string) map[string]string {
return map[string]string{
"rgw.root": ".rgw.root",
name + ".rgw.control": "",
name + ".rgw.meta": "",
name + ".rgw.log": "",
name + ".rgw.otp": "",
name + ".rgw.buckets.index": "",
name + ".rgw.buckets.data": "",
name + ".rgw.buckets.data.foo": "",
}
}
// CreateObjectStoreConfiguration adds the chart's object store and its bucket
// storage class to the Helm values. When UseMultisiteObjectStore is set it
// configures an RGW multisite zone; otherwise it configures a plain store.
func (h *CephInstaller) CreateObjectStoreConfiguration(values map[string]interface{}, name, scName string) error {
if h.settings.UseMultisiteObjectStore {
return h.createMultisiteObjectStoreConfiguration(values, name, scName)
}
testObjectStoreBytes := []byte(h.Manifests.GetObjectStore(name, 2, 8080, false, false))
var testObjectStoreCRD map[string]interface{}
if err := yaml.Unmarshal(testObjectStoreBytes, &testObjectStoreCRD); err != nil {
@@ -409,3 +476,112 @@ func (h *CephInstaller) CreateObjectStoreConfiguration(values map[string]interfa
}
return nil
}
// createMultisiteObjectStoreConfiguration configures the object store as an RGW
// multisite zone, exercising the chart's CephObjectRealm, CephObjectZoneGroup,
// and CephObjectZone templates on install. The realm/zone group/zone/shared-pools
// layout mirrors the fixture in tests/integration/object/util/sharedstore.
func (h *CephInstaller) createMultisiteObjectStoreConfiguration(values map[string]interface{}, name, scName string) error {
poolNames := rgwSharedPools(name)
rgwPools := make([]map[string]interface{}, 0, len(poolNames))
for poolName, radosName := range poolNames {
pgNum := "1"
if radosName == ".rgw.root" {
pgNum = "8"
}
spec := map[string]interface{}{
"replicated": map[string]interface{}{
"size": 1,
"requireSafeReplicaSize": false,
},
"parameters": map[string]interface{}{
"pg_autoscale_mode": "off",
"pg_num": pgNum,
},
}
// spec.name is enum-validated (.rgw.root/.nfs/.mgr) and must be omitted
// rather than set to "" when the pool name matches the CR name.
if radosName != "" {
spec["name"] = radosName
}
rgwPools = append(rgwPools, map[string]interface{}{
"name": poolName,
"spec": spec,
"storageClass": map[string]interface{}{
"enabled": false,
},
})
}
// Append the rgw pools to the block pools already configured for CSI so both
// sets land in the chart's cephBlockPools list.
existingPools, _ := values["cephBlockPools"].([]map[string]interface{})
values["cephBlockPools"] = append(existingPools, rgwPools...)
values["cephObjectRealms"] = []map[string]interface{}{
{
"name": name,
},
}
values["cephObjectZoneGroups"] = []map[string]interface{}{
{
"name": name,
"spec": map[string]interface{}{
"realm": name,
},
},
}
values["cephObjectZones"] = []map[string]interface{}{
{
"name": name,
"spec": map[string]interface{}{
"zoneGroup": name,
"sharedPools": map[string]interface{}{
"poolPlacements": []map[string]interface{}{
{
"name": "default",
"default": true,
"metadataPoolName": name + ".rgw.buckets.index",
"dataPoolName": name + ".rgw.buckets.data",
"storageClasses": []map[string]interface{}{
{
"name": "FOO",
"dataPoolName": name + ".rgw.buckets.data.foo",
},
},
},
},
},
},
},
}
storageClassBytes := []byte(h.Manifests.GetBucketStorageClass(name, scName, "Delete"))
var testObjectStoreSC map[string]interface{}
if err := yaml.Unmarshal(storageClassBytes, &testObjectStoreSC); err != nil {
return err
}
values["cephObjectStores"] = []map[string]interface{}{
{
"name": name,
"spec": map[string]interface{}{
"zone": map[string]interface{}{
"name": name,
},
"gateway": map[string]interface{}{
"port": 8080,
"instances": 2,
"resources": nil,
},
},
"storageClass": map[string]interface{}{
"enabled": true,
"name": scName,
"parameters": testObjectStoreSC["parameters"],
"reclaimPolicy": "Delete",
},
},
}
return nil
}
@@ -54,6 +54,12 @@ type TestCephSettings struct {
KubernetesVersion string
EnableCsiOperator bool
ClusterConcurrency int
// UseMultisiteObjectStore configures the Helm chart's object store as an RGW
// multisite zone (realm/zone group/zone/shared pools) instead of a plain
// store. Only valid for local-chart installs, which carry the multisite
// templates, so it must stay off for the upgrade suite's older base chart.
UseMultisiteObjectStore bool
}
func (s *TestCephSettings) ApplyEnvVars() {
+14 -13
View File
@@ -59,19 +59,20 @@ type HelmSuite struct {
func (h *HelmSuite) SetupSuite() {
namespace := "helm-ns"
h.settings = &installer.TestCephSettings{
Namespace: namespace,
OperatorNamespace: namespace,
StorageClassName: "",
UseHelm: true,
UsePVC: false,
Mons: 1,
SkipOSDCreation: false,
EnableDiscovery: true,
ChangeHostName: true,
ConnectionsEncrypted: true,
RookVersion: installer.LocalBuildTag,
CephVersion: installer.SquidVersion,
EnableCsiOperator: true,
Namespace: namespace,
OperatorNamespace: namespace,
StorageClassName: "",
UseHelm: true,
UseMultisiteObjectStore: true,
UsePVC: false,
Mons: 1,
SkipOSDCreation: false,
EnableDiscovery: true,
ChangeHostName: true,
ConnectionsEncrypted: true,
RookVersion: installer.LocalBuildTag,
CephVersion: installer.SquidVersion,
EnableCsiOperator: true,
}
h.settings.ApplyEnvVars()
h.installer, h.k8shelper = StartTestCluster(h.T, h.settings)