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
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ export function TenantDetailClient({ namespace, name, initialTab, initialYamlEdi
const [secCtxLoaded, setSecCtxLoaded] = useState(false)
const [secCtxLoading, setSecCtxLoading] = useState(false)
const [secCtxSaving, setSecCtxSaving] = useState(false)
const [secCtxDelegated, setSecCtxDelegated] = useState(false)
const [secCtx, setSecCtx] = useState<SecurityContextFormState>({
runAsUser: "",
runAsGroup: "",
Expand Down Expand Up @@ -337,6 +338,7 @@ export function TenantDetailClient({ namespace, name, initialTab, initialYamlEdi

useEffect(() => {
setSecCtxLoaded(false)
setSecCtxDelegated(false)
setSecCtxDirty(cleanSecurityContextDirtyFields())
}, [namespace, name])

Expand All @@ -350,6 +352,7 @@ export function TenantDetailClient({ namespace, name, initialTab, initialYamlEdi
fsGroup: data.fsGroup?.toString() ?? "",
runAsNonRoot: data.runAsNonRoot === null ? "default" : data.runAsNonRoot ? "true" : "false",
})
setSecCtxDelegated(data.operatorDefaultsDelegated)
setSecCtxDirty(cleanSecurityContextDirtyFields())
} catch (e) {
const err = e as ApiError
Expand All @@ -362,6 +365,10 @@ export function TenantDetailClient({ namespace, name, initialTab, initialYamlEdi

const handleSaveSecurityContext = async (e: React.FormEvent) => {
e.preventDefault()
if (secCtxDelegated) {
toast.error(t("Platform-delegated security contexts can only be changed in Raw YAML."))
return
}
setSecCtxSaving(true)
try {
await api.updateSecurityContext(namespace, name, buildSecurityContextUpdate(secCtx, secCtxDirty))
Expand Down Expand Up @@ -1300,13 +1307,26 @@ export function TenantDetailClient({ namespace, name, initialTab, initialYamlEdi
</div>
) : (
<form onSubmit={handleSaveSecurityContext} className="space-y-6">
{secCtxDelegated && (
<div className="rounded-md border border-border bg-muted/50 px-4 py-3 text-sm">
<p>
{t(
"Security defaults are delegated to platform admission. Effective UID/GID and runAsNonRoot values are platform-owned.",
)}
</p>
<Button type="button" variant="outline" size="sm" className="mt-3" onClick={() => setTab("edit")}>
{t("Open Raw YAML")}
</Button>
</div>
)}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<div className="space-y-2">
<Label>{t("Run As User")}</Label>
<Input
type="number"
placeholder="10001"
value={secCtx.runAsUser}
disabled={secCtxDelegated}
onChange={(e) => {
setSecCtx((s) => ({ ...s, runAsUser: e.target.value }))
setSecCtxDirty((dirty) => ({ ...dirty, runAsUser: true }))
Expand All @@ -1319,6 +1339,7 @@ export function TenantDetailClient({ namespace, name, initialTab, initialYamlEdi
type="number"
placeholder="10001"
value={secCtx.runAsGroup}
disabled={secCtxDelegated}
onChange={(e) => {
setSecCtx((s) => ({ ...s, runAsGroup: e.target.value }))
setSecCtxDirty((dirty) => ({ ...dirty, runAsGroup: true }))
Expand All @@ -1331,6 +1352,7 @@ export function TenantDetailClient({ namespace, name, initialTab, initialYamlEdi
type="number"
placeholder="10001"
value={secCtx.fsGroup}
disabled={secCtxDelegated}
onChange={(e) => {
setSecCtx((s) => ({ ...s, fsGroup: e.target.value }))
setSecCtxDirty((dirty) => ({ ...dirty, fsGroup: true }))
Expand All @@ -1344,6 +1366,7 @@ export function TenantDetailClient({ namespace, name, initialTab, initialYamlEdi
<select
id="sec-nonroot"
value={secCtx.runAsNonRoot}
disabled={secCtxDelegated}
onChange={(e) => {
setSecCtx((s) => ({ ...s, runAsNonRoot: e.target.value as RunAsNonRootMode }))
setSecCtxDirty((dirty) => ({ ...dirty, runAsNonRoot: true }))
Expand All @@ -1357,7 +1380,7 @@ export function TenantDetailClient({ namespace, name, initialTab, initialYamlEdi
</div>
</div>
<div className="flex gap-2">
<Button type="submit" disabled={secCtxSaving}>
<Button type="submit" disabled={secCtxSaving || secCtxDelegated}>
{secCtxSaving && <Spinner className="mr-2 size-4" />}
{secCtxSaving ? t("Saving...") : t("Save")}
</Button>
Expand Down
3 changes: 3 additions & 0 deletions console-web/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,9 @@
"Override Pod SecurityContext UID/GID fields. Use Raw YAML for seccomp, container, and Pool-level settings. Changes apply after Pods are recreated.": "Override Pod SecurityContext UID/GID fields. Use Raw YAML for seccomp, container, and Pool-level settings. Changes apply after Pods are recreated.",
"SecurityContext updated": "SecurityContext updated",
"Failed to load security context": "Failed to load security context",
"Security defaults are delegated to platform admission. Effective UID/GID and runAsNonRoot values are platform-owned.": "Security defaults are delegated to platform admission. Effective UID/GID and runAsNonRoot values are platform-owned.",
"Platform-delegated security contexts can only be changed in Raw YAML.": "Platform-delegated security contexts can only be changed in Raw YAML.",
"Open Raw YAML": "Open Raw YAML",
"Do not run as Root": "Do not run as Root",
"Default": "Default",
"Loading...": "Loading..."
Expand Down
3 changes: 3 additions & 0 deletions console-web/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,9 @@
"Override Pod SecurityContext UID/GID fields. Use Raw YAML for seccomp, container, and Pool-level settings. Changes apply after Pods are recreated.": "覆盖 Pod SecurityContext 的 UID/GID 字段。如需配置 seccomp、容器级和 Pool 级设置,请使用原始 YAML。修改将在 Pod 重建后生效。",
"SecurityContext updated": "SecurityContext 已更新",
"Failed to load security context": "加载 SecurityContext 失败",
"Security defaults are delegated to platform admission. Effective UID/GID and runAsNonRoot values are platform-owned.": "安全默认值已委托给平台准入机制,实际的 UID/GID 和 runAsNonRoot 值由平台决定。",
"Platform-delegated security contexts can only be changed in Raw YAML.": "平台委托的 SecurityContext 只能通过原始 YAML 修改。",
"Open Raw YAML": "打开原始 YAML",
"Do not run as Root": "禁止以 Root 运行",
"Default": "默认值",
"Custom Certificates": "自定义证书",
Expand Down
3 changes: 2 additions & 1 deletion console-web/types/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,8 @@ export interface SecurityContextInfo {
runAsGroup: number | null
fsGroup: number | null
runAsNonRoot: boolean | null
effectiveRunAsNonRoot: boolean
effectiveRunAsNonRoot: boolean | null
operatorDefaultsDelegated: boolean
}

export interface EncryptionInfoResponse {
Expand Down
20 changes: 14 additions & 6 deletions deploy/rustfs-operator/crds/tenant-crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,10 @@ spec:
x-kubernetes-list-type: map
containerSecurityContext:
description: |-
Override the default RustFS container SecurityContext for every Pool in this Tenant.
Pool-level values take precedence.
RustFS container SecurityContext overrides for every Pool in this Tenant.
Non-empty values merge by field over operator defaults, with Pool-level values taking
precedence. Setting this and `securityContext` to explicit empty objects resets the
defaults and delegates unspecified values to platform admission.
nullable: true
properties:
allowPrivilegeEscalation:
Expand Down Expand Up @@ -1204,7 +1206,9 @@ spec:
containerSecurityContext:
description: |-
RustFS container SecurityContext overrides for this Pool.
Values are merged over Tenant-level container settings and operator defaults.
Non-empty values merge by field over Tenant-level settings and operator defaults. An
explicit empty object paired with an empty `securityContext` resets inherited values and
delegates unspecified values to platform admission.
nullable: true
properties:
allowPrivilegeEscalation:
Expand Down Expand Up @@ -1500,7 +1504,9 @@ spec:
securityContext:
description: |-
Pod SecurityContext overrides for this Pool.
Values override Tenant-level Pod SecurityContext settings.
Non-empty values merge by field over Tenant-level settings and operator defaults. An
explicit empty object paired with an empty `containerSecurityContext` resets inherited
values and delegates unspecified values to platform admission.
nullable: true
properties:
fsGroup:
Expand Down Expand Up @@ -1700,8 +1706,10 @@ spec:
type: string
securityContext:
description: |-
Override the default Pod SecurityContext for every Pool in this Tenant.
Pool-level values take precedence.
Pod SecurityContext overrides for every Pool in this Tenant.
Non-empty values merge by field over operator defaults, with Pool-level values taking
precedence. Setting this and `containerSecurityContext` to explicit empty objects resets
the defaults and delegates unspecified values to platform admission.
nullable: true
properties:
fsGroup:
Expand Down
94 changes: 38 additions & 56 deletions src/console/handlers/encryption.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ use crate::console::{
use crate::types::v1alpha1::encryption::{
EncryptionConfig, KmsBackendType, LocalKmsConfig, LocalKmsMasterKeySecretRef, VaultKmsConfig,
};
use crate::types::v1alpha1::security_context::effective_run_as_non_root;
use crate::types::v1alpha1::tenant::Tenant;
use axum::{Extension, Json, extract::Path};
use k8s_openapi::api::core::v1 as corev1;
Expand Down Expand Up @@ -136,63 +135,46 @@ pub async fn get_encryption(
.get(&name)
.await
.map_err(|e| error::map_kube_error(e, format!("Tenant '{}'", name)))?;

let enc_resp =
match tenant.spec.encryption {
Some(ref enc) => EncryptionInfoResponse {
enabled: enc.enabled,
backend: enc.backend.to_string(),
vault: enc.vault.as_ref().map(|v| VaultInfo {
endpoint: v.endpoint.clone(),
}),
local: enc.local.as_ref().map(|l| LocalInfo {
key_directory: l.key_directory.clone(),
master_key_secret_ref: l.master_key_secret_ref.as_ref().map(|s| {
LocalMasterKeySecretRefInfo {
name: s.name.clone(),
key: s.key.clone(),
}
}),
allow_insecure_dev_defaults: l.allow_insecure_dev_defaults,
}),
kms_secret_name: (enc.backend == KmsBackendType::Vault)
.then(|| enc.kms_secret.as_ref().map(|s| s.name.clone()))
.flatten(),
default_key_id: enc.default_key_id.clone(),
security_context: tenant.spec.security_context.as_ref().map(|sc| {
SecurityContextInfo {
run_as_user: sc.run_as_user,
run_as_group: sc.run_as_group,
fs_group: sc.fs_group,
run_as_non_root: sc.run_as_non_root,
effective_run_as_non_root: effective_run_as_non_root(
sc.run_as_user,
sc.run_as_non_root,
),
}
}),
},
None => EncryptionInfoResponse {
enabled: false,
backend: "local".to_string(),
vault: None,
local: None,
kms_secret_name: None,
default_key_id: None,
security_context: tenant.spec.security_context.as_ref().map(|sc| {
SecurityContextInfo {
run_as_user: sc.run_as_user,
run_as_group: sc.run_as_group,
fs_group: sc.fs_group,
run_as_non_root: sc.run_as_non_root,
effective_run_as_non_root: effective_run_as_non_root(
sc.run_as_user,
sc.run_as_non_root,
),
let security_context = tenant.spec.security_context.as_ref().map(|_| {
SecurityContextInfo::from_contexts(
tenant.spec.security_context.as_ref(),
tenant.spec.container_security_context.as_ref(),
)
});

let enc_resp = match tenant.spec.encryption {
Some(ref enc) => EncryptionInfoResponse {
enabled: enc.enabled,
backend: enc.backend.to_string(),
vault: enc.vault.as_ref().map(|v| VaultInfo {
endpoint: v.endpoint.clone(),
}),
local: enc.local.as_ref().map(|l| LocalInfo {
key_directory: l.key_directory.clone(),
master_key_secret_ref: l.master_key_secret_ref.as_ref().map(|s| {
LocalMasterKeySecretRefInfo {
name: s.name.clone(),
key: s.key.clone(),
}
}),
},
};
allow_insecure_dev_defaults: l.allow_insecure_dev_defaults,
}),
kms_secret_name: (enc.backend == KmsBackendType::Vault)
.then(|| enc.kms_secret.as_ref().map(|s| s.name.clone()))
.flatten(),
default_key_id: enc.default_key_id.clone(),
security_context,
},
None => EncryptionInfoResponse {
enabled: false,
backend: "local".to_string(),
vault: None,
local: None,
kms_secret_name: None,
default_key_id: None,
security_context,
},
};

Ok(Json(enc_resp))
}
Expand Down
Loading
Loading