Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions deploy/k8s-dev/operator-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ spec:
value: "true"
- name: OPERATOR_TENANT_MONITOR_INTERVAL_SECONDS
value: "300"
- name: OPERATOR_CLUSTER_DOMAIN
value: "cluster.local"
- name: POD_NAME
valueFrom:
fieldRef:
Expand Down
3 changes: 3 additions & 0 deletions deploy/rustfs-operator/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,14 @@ The following table lists the configurable parameters of the RustFS Operator cha
| `operator.prometheusRule.enabled` | Create Prometheus alert rules for operator and tenant storage health | `false` |
| `operator.tenantMonitor.enabled` | Poll RustFS tenant storage health and capacity metrics | `true` |
| `operator.tenantMonitor.intervalSeconds` | Tenant storage monitor interval | `300` |
| `clusterDomain` | Kubernetes cluster DNS domain used for Tenant peer URLs, generated TLS SANs, and operator STS auto TLS | `cluster.local` |
| `operator.env` | Environment variables | `[{name: RUST_LOG, value: info}]` |
| `operator.nodeSelector` | Node selector for pod placement | `{}` |
| `operator.tolerations` | Tolerations for pod scheduling | `[]` |
| `operator.affinity` | Affinity rules for pod scheduling | `{}` |

Use `clusterDomain` for custom Kubernetes DNS domains; `operator.env` must not set `OPERATOR_CLUSTER_DOMAIN`.

### Operator STS Configuration

| Parameter | Description | Default |
Expand Down
7 changes: 7 additions & 0 deletions deploy/rustfs-operator/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
{{- if and (not .Values.operator.metrics.enabled) (or (hasKey $livenessProbe "httpGet") (hasKey $readinessProbe "httpGet")) -}}
{{- fail "operator.metrics.enabled=false requires overriding operator.livenessProbe and operator.readinessProbe because the chart defaults use the metrics port" -}}
{{- end -}}
{{- range $env := .Values.operator.env }}
{{- if eq $env.name "OPERATOR_CLUSTER_DOMAIN" -}}
{{- fail "operator.env must not set OPERATOR_CLUSTER_DOMAIN; use clusterDomain instead" -}}
{{- end -}}
{{- end -}}
apiVersion: apps/v1
kind: Deployment
metadata:
Expand Down Expand Up @@ -81,6 +86,8 @@ spec:
value: {{ .Values.operator.tenantMonitor.enabled | quote }}
- name: OPERATOR_TENANT_MONITOR_INTERVAL_SECONDS
value: {{ .Values.operator.tenantMonitor.intervalSeconds | quote }}
- name: OPERATOR_CLUSTER_DOMAIN
value: {{ .Values.clusterDomain | default "cluster.local" | quote }}
- name: OPERATOR_STS_ENABLED
value: {{ .Values.sts.enabled | quote }}
- name: OPERATOR_STS_AUDIENCE
Expand Down
21 changes: 21 additions & 0 deletions deploy/rustfs-operator/values.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"$schema": "https://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"clusterDomain": {
"type": "string",
"minLength": 1,
"oneOf": [
{
"maxLength": 253,
"pattern": "^([A-Za-z0-9]([-A-Za-z0-9]{0,61}[A-Za-z0-9])?)(\\.([A-Za-z0-9]([-A-Za-z0-9]{0,61}[A-Za-z0-9])?))*$"
},
{
"maxLength": 254,
"pattern": "^([A-Za-z0-9]([-A-Za-z0-9]{0,61}[A-Za-z0-9])?)(\\.([A-Za-z0-9]([-A-Za-z0-9]{0,61}[A-Za-z0-9])?))*\\.$"
}
],
"description": "Kubernetes cluster DNS domain used for Tenant peer URLs, generated TLS SANs, and operator STS auto TLS."
}
}
}
3 changes: 3 additions & 0 deletions deploy/rustfs-operator/values.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Default values for rustfs-operator.

# Kubernetes cluster DNS domain used for Tenant peer URLs and generated TLS SANs.
clusterDomain: cluster.local

# Operator deployment configuration
operator:
# Number of operator replicas
Expand Down
7 changes: 5 additions & 2 deletions docs/operator-user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,12 +119,15 @@ Common chart sections:
| `sts` | Operator STS endpoint, service port, TokenReview audience, and TLS handling. |
| `serviceAccount` / `rbac` | Operator ServiceAccount and RBAC creation. |
| `console` | Operator Console backend/UI Deployment, service, session cookie secret, ingress, resources, and optional split frontend. |
| `clusterDomain` | Kubernetes cluster DNS domain used for Tenant peer URLs, generated TLS SANs, and operator STS auto TLS. Defaults to `cluster.local`. |
| `namespace` | Namespace override for chart resources; defaults to the Helm release namespace. |
| `commonLabels` / `commonAnnotations` | Labels and annotations added to chart-managed resources. |

Example production-oriented values:

```yaml
clusterDomain: cluster.local

operator:
replicas: 2
image:
Expand Down Expand Up @@ -348,7 +351,7 @@ The operator reserves these environment variables and manages them automatically
- `RUSTFS_KMS_*` variables; use `spec.encryption` instead.
- TLS-related RustFS variables when Tenant TLS is enabled.

For a single-pool single-node single-disk Tenant, `RUSTFS_VOLUMES` is rendered as the local data path, for example `/data/rustfs0`. Multi-pool tenants and other layouts render peer DNS URLs through the Tenant headless Service and are validated by RustFS at runtime.
For a single-pool single-node single-disk Tenant, `RUSTFS_VOLUMES` is rendered as the local data path, for example `/data/rustfs0`. Multi-pool tenants and other layouts render peer DNS URLs through the Tenant headless Service and are validated by RustFS at runtime. Set the Helm chart `clusterDomain` value when the Kubernetes cluster DNS domain is not `cluster.local`; the same domain is used for generated TLS SANs.

`podDeletionPolicyWhenNodeIsDown` accepts:

Expand Down Expand Up @@ -437,7 +440,7 @@ spec:

The default certificate is projected to `rustfs_cert.pem` and `rustfs_key.pem` at `mountPath`, so RustFS can use it as the fallback certificate and for internode HTTPS. Each `hosts` value is projected as a RustFS SNI directory, for example `s3.example.com/rustfs_cert.pem` and `s3.example.com/rustfs_key.pem`.
When `certificates` is set, omitted `includeGeneratedDnsNames` is treated as `true` only on the `default: true` certificate. Non-default entries include only `hosts` and `certManager.dnsNames` unless they explicitly set `includeGeneratedDnsNames: true`.
When `enableInternodeHttps: true`, the default managed certificate must cover the generated RustFS peer DNS names. Keep `includeGeneratedDnsNames` enabled, or list the generated peer names explicitly in `hosts` or `certManager.dnsNames`.
When `enableInternodeHttps: true`, the default managed certificate must cover the RustFS peer DNS names used by the operator and `RUSTFS_VOLUMES`. Keep `includeGeneratedDnsNames` enabled, or list the required names explicitly in `hosts` or `certManager.dnsNames`. Clusters with a custom Kubernetes DNS domain should set the Helm chart `clusterDomain`; required names then use `.svc.<clusterDomain>`. External certificates must cover the headless Service FQDN (`<tenant>-hl.<namespace>.svc.<clusterDomain>`) and pod FQDNs; a wildcard such as `*.<tenant>-hl.<namespace>.svc.<clusterDomain>` covers the generated pod FQDNs.
When `certificates` is set, configure process-wide trust with top-level `caTrust` or the `caTrust` on the `default: true` certificate. The legacy `certManager.caTrust` field is only used by the single-certificate form, and `certManager.caTrust` on non-default entries is rejected.

### 7.6 Logging
Expand Down
7 changes: 5 additions & 2 deletions docs/operator-user-guide.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,15 @@ helm upgrade --install rustfs-operator deploy/rustfs-operator/ \
| `sts` | Operator STS 端点、Service 端口、TokenReview audience 和 TLS。 |
| `serviceAccount` / `rbac` | Operator ServiceAccount 和 RBAC 创建策略。 |
| `console` | Operator Console 后端/UI Deployment、Service、session cookie 密钥、Ingress、资源和可选独立前端。 |
| `clusterDomain` | Kubernetes 集群 DNS 域,用于 Tenant peer URL、自动生成的 TLS SAN 和 operator STS 自动 TLS。默认 `cluster.local`。 |
| `namespace` | Chart 资源命名空间覆盖;默认使用 Helm release namespace。 |
| `commonLabels` / `commonAnnotations` | 添加到 Chart 管理资源上的统一 label 和 annotation。 |

生产风格 values 示例:

```yaml
clusterDomain: cluster.local

operator:
replicas: 2
image:
Expand Down Expand Up @@ -350,7 +353,7 @@ Operator 会自动管理以下环境变量:
- `RUSTFS_KMS_*` 变量;请改用 `spec.encryption` 配置
- 启用 TLS 时的 RustFS TLS 相关变量

对于单 pool 的单节点单盘 Tenant,`RUSTFS_VOLUMES` 会渲染为本地数据路径,例如 `/data/rustfs0`。多 pool Tenant 和其他布局仍会通过 Tenant headless Service 渲染 peer DNS URL,并由 RustFS 在运行时校验。
对于单 pool 的单节点单盘 Tenant,`RUSTFS_VOLUMES` 会渲染为本地数据路径,例如 `/data/rustfs0`。多 pool Tenant 和其他布局仍会通过 Tenant headless Service 渲染 peer DNS URL,并由 RustFS 在运行时校验。当 Kubernetes 集群 DNS 域不是 `cluster.local` 时,请设置 Helm chart 的 `clusterDomain`;自动生成的 TLS SAN 也会使用同一个域。

`podDeletionPolicyWhenNodeIsDown` 支持以下值:

Expand Down Expand Up @@ -439,7 +442,7 @@ spec:

默认条目会被投影到 `mountPath` 根目录下的 `rustfs_cert.pem` 和 `rustfs_key.pem`,供 RustFS 作为 fallback 证书和节点间 HTTPS 证书使用。每个 `hosts` 值会被投影为 RustFS SNI 子目录,例如 `s3.example.com/rustfs_cert.pem` 和 `s3.example.com/rustfs_key.pem`。
配置了 `certificates` 时,省略 `includeGeneratedDnsNames` 只有在 `default: true` 证书条目中才按 `true` 处理。非默认条目只包含 `hosts` 和 `certManager.dnsNames`,除非显式设置 `includeGeneratedDnsNames: true`。
当 `enableInternodeHttps: true` 时,默认的托管证书必须覆盖 RustFS 自动生成的节点间 DNS 名称。应保持 `includeGeneratedDnsNames` 启用,或在 `hosts` / `certManager.dnsNames` 中显式列出这些节点间名称
当 `enableInternodeHttps: true` 时,默认托管证书必须覆盖 operator 和 `RUSTFS_VOLUMES` 实际使用的 RustFS peer DNS 名称。应保持 `includeGeneratedDnsNames` 启用,或在 `hosts` / `certManager.dnsNames` 中显式列出必需名称。使用自定义 Kubernetes DNS 域的集群应设置 Helm chart 的 `clusterDomain`;必需名称会使用 `.svc.<clusterDomain>`。外部证书必须覆盖 headless Service FQDN(`<tenant>-hl.<namespace>.svc.<clusterDomain>`)和 pod FQDN;类似 `*.<tenant>-hl.<namespace>.svc.<clusterDomain>` 的 wildcard 可以覆盖生成的 pod FQDN
配置了 `certificates` 时,进程级 trust 应放在顶层 `caTrust` 或 `default: true` 证书条目的 `caTrust` 中。旧的 `certManager.caTrust` 只对单证书写法生效,非默认条目上的 `certManager.caTrust` 会被拒绝。

### 7.6 日志配置
Expand Down
2 changes: 1 addition & 1 deletion e2e/src/bin/rustfs-e2e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ fn create_kind_cluster(config: &E2eConfig) -> Result<()> {
live::require_live_enabled(config)?;
let kind = KindCluster::new(config.clone());
kind.reset_host_storage_dirs()?;
kind.create_command().run_checked()?;
kind.create_command()?.run_checked()?;
Ok(())
}

Expand Down
23 changes: 18 additions & 5 deletions e2e/src/framework/cert_manager_tls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ use std::path::Path;
use std::time::Duration;
use tempfile::TempDir;

use crate::framework::cluster_dns;
use crate::framework::{
assertions, command::CommandSpec, config::E2eConfig, kubectl::Kubectl, resources, storage,
tenant_factory::TenantTemplate, wait,
Expand Down Expand Up @@ -171,17 +172,29 @@ fn tls_certificate_dns_names(config: &E2eConfig, tenant: &Tenant) -> Vec<String>
if cert_manager.include_generated_dns_names.unwrap_or(true) {
let tenant_name = &config.tenant_name;
let namespace = &config.test_namespace;
let cluster_domain = &config.cluster_domain;
let io_service = format!("{tenant_name}-io");
let headless_service = format!("{tenant_name}-hl");
names.insert(format!("{io_service}.{namespace}.svc"));
names.insert(format!("{io_service}.{namespace}.svc.cluster.local"));
names.insert(cluster_dns::service_fqdn(
&io_service,
namespace,
cluster_domain,
));
names.insert(format!("{headless_service}.{namespace}.svc"));
names.insert(format!("{headless_service}.{namespace}.svc.cluster.local"));
names.insert(cluster_dns::service_fqdn(
&headless_service,
namespace,
cluster_domain,
));
for pool in &tenant.spec.pools {
for ordinal in 0..pool.servers.max(0) {
names.insert(format!(
"{tenant_name}-{}-{ordinal}.{headless_service}.{namespace}.svc.cluster.local",
pool.name
let pod_name = format!("{tenant_name}-{}-{ordinal}", pool.name);
names.insert(cluster_dns::pod_fqdn(
&pod_name,
&headless_service,
namespace,
cluster_domain,
));
}
}
Expand Down
84 changes: 84 additions & 0 deletions e2e/src/framework/cluster_dns.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright 2025 RustFS Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

pub const DEFAULT_CLUSTER_DOMAIN: &str = "cluster.local";
const E2E_CLUSTER_DOMAIN_ENV: &str = "RUSTFS_E2E_CLUSTER_DOMAIN";
const MAX_DNS_NAME_LENGTH: usize = 253;

pub fn configured_cluster_domain(value: &str) -> String {
normalize_cluster_domain(value).unwrap_or_else(|| {
panic!(
"{E2E_CLUSTER_DOMAIN_ENV} must be a valid DNS domain, for example 'cluster.local' or 'k8s.mse.cloud'"
)
})
}

pub fn service_fqdn(service_name: &str, namespace: &str, cluster_domain: &str) -> String {
format!("{service_name}.{namespace}.svc.{cluster_domain}")
}

pub fn pod_fqdn(
pod_name: &str,
headless_service: &str,
namespace: &str,
cluster_domain: &str,
) -> String {
format!("{pod_name}.{headless_service}.{namespace}.svc.{cluster_domain}")
}

fn normalize_cluster_domain(value: &str) -> Option<String> {
let domain = value
.strip_suffix('.')
.unwrap_or(value)
.to_ascii_lowercase();
if domain.is_empty()
|| domain.len() > MAX_DNS_NAME_LENGTH
|| !domain.split('.').all(valid_dns_label)
{
return None;
}
Some(domain)
}

fn valid_dns_label(label: &str) -> bool {
!label.is_empty()
&& label.len() <= 63
&& label
.bytes()
.all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
&& label
.as_bytes()
.first()
.is_some_and(u8::is_ascii_alphanumeric)
&& label
.as_bytes()
.last()
.is_some_and(u8::is_ascii_alphanumeric)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn configured_cluster_domain_normalizes_supported_values() {
assert_eq!(configured_cluster_domain("K8S.MSE.Cloud."), "k8s.mse.cloud");
}

#[test]
#[should_panic(expected = "RUSTFS_E2E_CLUSTER_DOMAIN must be a valid DNS domain")]
fn configured_cluster_domain_rejects_invalid_values() {
configured_cluster_domain(&format!("{}.local", "a".repeat(64)));
}
}
10 changes: 10 additions & 0 deletions e2e/src/framework/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::framework::cluster_dns;
use operator::types::v1alpha1::k8s::PodManagementPolicy;
use std::ops::{Deref, DerefMut};
use std::path::PathBuf;
Expand Down Expand Up @@ -45,6 +46,7 @@ pub struct E2eConfig {
pub operator_image: String,
pub console_web_image: String,
pub cert_manager_version: String,
pub cluster_domain: String,
pub kind_config: PathBuf,
pub live_enabled: bool,
}
Expand Down Expand Up @@ -112,6 +114,11 @@ impl E2eConfig {
"RUSTFS_E2E_CERT_MANAGER_VERSION",
DEFAULT_CERT_MANAGER_VERSION,
),
cluster_domain: cluster_dns::configured_cluster_domain(&env_or(
&get_env,
"RUSTFS_E2E_CLUSTER_DOMAIN",
cluster_dns::DEFAULT_CLUSTER_DOMAIN,
)),
kind_config: PathBuf::from(env_or(
&get_env,
"RUSTFS_E2E_KIND_CONFIG",
Expand Down Expand Up @@ -190,6 +197,7 @@ mod tests {
assert_eq!(config.pv_count, 12);
assert_eq!(config.rustfs_image, DEFAULT_RUSTFS_IMAGE);
assert_eq!(config.cert_manager_version, "v1.16.2");
assert_eq!(config.cluster_domain, "cluster.local");
assert_eq!(
config.kind_config,
std::path::PathBuf::from("e2e/manifests/kind-rustfs-e2e.yaml")
Expand All @@ -207,6 +215,7 @@ mod tests {
"RUSTFS_E2E_CONSOLE_WEB_IMAGE" => Some("rustfs/console-web:other".to_string()),
"RUSTFS_E2E_SERVER_IMAGE" => Some("rustfs/rustfs:dev".to_string()),
"RUSTFS_E2E_CERT_MANAGER_VERSION" => Some("v9.9.9".to_string()),
"RUSTFS_E2E_CLUSTER_DOMAIN" => Some("K8S.MSE.Cloud.".to_string()),
"RUSTFS_E2E_LIVE" => Some("true".to_string()),
_ => None,
});
Expand All @@ -217,6 +226,7 @@ mod tests {
assert_eq!(config.console_web_image, "rustfs/console-web:e2e");
assert_eq!(config.rustfs_image, "rustfs/rustfs:dev");
assert_eq!(config.cert_manager_version, "v9.9.9");
assert_eq!(config.cluster_domain, "k8s.mse.cloud");
assert!(config.live_enabled);
}
}
24 changes: 22 additions & 2 deletions e2e/src/framework/deploy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ pub fn deploy_dev(config: &E2eConfig) -> Result<()> {

kubectl
.apply_yaml_command(patch_images_and_tags(
OPERATOR_DEPLOYMENT,
&patch_operator_cluster_domain(OPERATOR_DEPLOYMENT, &config.cluster_domain),
&config.operator_image,
E2E_OPERATOR_IMAGE_TAG_DEFAULT,
))
Expand Down Expand Up @@ -140,6 +140,15 @@ stringData:
)
}

fn patch_operator_cluster_domain(yaml: &str, cluster_domain: &str) -> String {
yaml.replace(
" - name: OPERATOR_CLUSTER_DOMAIN\n value: \"cluster.local\"",
&format!(
" - name: OPERATOR_CLUSTER_DOMAIN\n value: \"{cluster_domain}\""
),
)
}

fn patch_images_and_tags(manifest: &str, image: &str, fallback: &str) -> String {
if image == fallback {
manifest.to_string()
Expand All @@ -152,7 +161,7 @@ fn patch_images_and_tags(manifest: &str, image: &str, fallback: &str) -> String
mod tests {
use super::{
E2E_CONSOLE_WEB_IMAGE_TAG_DEFAULT, E2E_CONTROL_PLANE_DEPLOYMENTS,
E2E_OPERATOR_IMAGE_TAG_DEFAULT, patch_images_and_tags,
E2E_OPERATOR_IMAGE_TAG_DEFAULT, patch_images_and_tags, patch_operator_cluster_domain,
};

#[test]
Expand All @@ -174,6 +183,17 @@ mod tests {
assert!(!web.contains(E2E_CONSOLE_WEB_IMAGE_TAG_DEFAULT));
}

#[test]
fn patch_operator_cluster_domain_updates_managed_env() {
let patched = patch_operator_cluster_domain(
"env:\n - name: OPERATOR_CLUSTER_DOMAIN\n value: \"cluster.local\"\n",
"k8s.mse.cloud",
);

assert!(patched.contains("value: \"k8s.mse.cloud\""));
assert!(!patched.contains("value: \"cluster.local\""));
}

#[test]
fn rollout_deployments_are_explicit_and_stable() {
assert_eq!(
Expand Down
Loading
Loading