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 @@ -33,6 +33,7 @@ import type {
AddPoolRequest,
EncryptionInfoResponse,
UpdateEncryptionRequest,
ProvisioningItemStatus,
} from "@/types/api"
import { ApiError } from "@/lib/api-client"

Expand Down Expand Up @@ -65,7 +66,7 @@ function normalizeTab(value?: string | null): Tab {
}
}

function formatTimestamp(value: string | null): string {
function formatTimestamp(value: string | null | undefined): string {
if (!value) return "-"
const date = new Date(value)
if (Number.isNaN(date.getTime())) return value
Expand Down Expand Up @@ -95,6 +96,25 @@ function podLastExitSummary(pod: PodListItem, exitedLabel: string): string {
return message ? `${summary}: ${message}` : summary
}

function provisioningGroups(tenant: TenantDetailsResponse) {
const provisioning = tenant.provisioning
if (!provisioning) return []
return [
{ type: "Policy", items: provisioning.policies ?? [] },
{ type: "User", items: provisioning.users ?? [] },
{ type: "Bucket", items: provisioning.buckets ?? [] },
].filter((group) => group.items.length > 0)
}

function provisioningItemDetails(item: ProvisioningItemStatus): string {
const details: string[] = []
if (item.policies && item.policies.length > 0) details.push(`policies=${item.policies.join(",")}`)
if (item.region) details.push(`region=${item.region}`)
if (item.objectLock != null) details.push(`objectLock=${item.objectLock ? "true" : "false"}`)
if (item.lastAppliedGeneration != null) details.push(`generation=${item.lastAppliedGeneration}`)
return details.length > 0 ? details.join(" ") : "-"
}

export function TenantDetailClient({ namespace, name, initialTab, initialYamlEditable }: TenantDetailClientProps) {
const router = useRouter()
const { t } = useTranslation()
Expand Down Expand Up @@ -521,6 +541,8 @@ export function TenantDetailClient({ namespace, name, initialTab, initialYamlEdi
const statusReason = statusSummary.primary_reason || tenant.state
const statusMessage = statusSummary.primary_message || "-"
const statusNextActions = statusSummary.next_actions.length > 0 ? statusSummary.next_actions : tenant.next_actions
const provisioning = tenant.provisioning
const provisioningStatusGroups = provisioningGroups(tenant)

return (
<Page>
Expand Down Expand Up @@ -606,6 +628,53 @@ export function TenantDetailClient({ namespace, name, initialTab, initialYamlEdi
</p>
</CardContent>
</Card>
{provisioningStatusGroups.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="text-base">{t("Provisioning")}</CardTitle>
<CardDescription>
{t("Phase")}: {provisioning?.phase ?? "-"}
{provisioning?.observedGeneration != null
? ` · ${t("Observed generation")}: ${provisioning.observedGeneration}`
: ""}
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>{t("Type")}</TableHead>
<TableHead>{t("Name")}</TableHead>
<TableHead>{t("Status")}</TableHead>
<TableHead>{t("Reason")}</TableHead>
<TableHead>{t("Details")}</TableHead>
<TableHead>{t("Message")}</TableHead>
<TableHead>{t("Last transition")}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{provisioningStatusGroups.flatMap((group) =>
group.items.map((item) => (
<TableRow key={`${group.type}-${item.name}`}>
<TableCell>{t(group.type)}</TableCell>
<TableCell className="font-medium">{item.name}</TableCell>
<TableCell>{item.state}</TableCell>
<TableCell>{item.reason || "-"}</TableCell>
<TableCell className="max-w-[320px] whitespace-normal break-words">
{provisioningItemDetails(item)}
</TableCell>
<TableCell className="max-w-[420px] whitespace-normal break-words">
{item.message || "-"}
</TableCell>
<TableCell>{formatTimestamp(item.lastTransitionTime)}</TableCell>
</TableRow>
)),
)}
</TableBody>
</Table>
</CardContent>
</Card>
)}
{tenant.services.length > 0 && (
<Card>
<CardHeader>
Expand Down
123 changes: 114 additions & 9 deletions console-web/app/(dashboard)/tenants/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ import { Label } from "@/components/ui/label"
import { Spinner } from "@/components/ui/spinner"
import { routes } from "@/lib/routes"
import * as api from "@/lib/api"
import type { CreatePoolRequest, CreateTenantRequest } from "@/types/api"
import type {
CreatePoolRequest,
CreateTenantRequest,
ProvisioningBucket,
ProvisioningPolicy,
ProvisioningUser,
} from "@/types/api"
import { ApiError } from "@/lib/api-client"

type CreateMode = "form" | "yaml"
Expand All @@ -29,19 +35,26 @@ const defaultPool: CreatePoolRequest = {
storage_class: "",
}

const defaultTenantYaml = `apiVersion: rustfs.io/v1alpha1
const defaultTenantYaml = `apiVersion: rustfs.com/v1alpha1
kind: Tenant
metadata:
name: my-tenant
namespace: default
spec:
image: rustfs/rustfs:latest
credsSecret: rustfs-creds
credsSecret:
name: rustfs-creds
pools:
- name: pool-0
servers: 2
volumesPerServer: 2
storageSize: 10Gi
persistence:
volumesPerServer: 2
volumeClaimTemplate:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 10Gi
`

function asRecord(value: unknown): Record<string, unknown> | null {
Expand All @@ -64,6 +77,17 @@ function asPositiveInt(value: unknown): number | undefined {
return undefined
}

function asBoolean(value: unknown): boolean | undefined {
if (typeof value === "boolean") return value
return undefined
}

function asStringArray(value: unknown): string[] | undefined {
if (!Array.isArray(value)) return undefined
const values = value.map(asString)
return values.every((item): item is string => !!item) ? values : undefined
}

export default function TenantCreatePage() {
const { t } = useTranslation()
const router = useRouter()
Expand Down Expand Up @@ -119,9 +143,15 @@ export default function TenantCreatePage() {

const metadata = asRecord(root.metadata)
const spec = asRecord(root.spec)
const apiVersion = asString(root.apiVersion)
const kind = asString(root.kind)
const parsedName = asString(metadata?.name)
const parsedNamespace = asString(metadata?.namespace)

if (apiVersion !== "rustfs.com/v1alpha1" || kind !== "Tenant") {
throw new Error(t("YAML must be a rustfs.com/v1alpha1 Tenant"))
}

if (!parsedName || !parsedNamespace) {
throw new Error(t("YAML must include metadata.name and metadata.namespace"))
}
Expand All @@ -133,9 +163,18 @@ export default function TenantCreatePage() {

const parsedPools: CreatePoolRequest[] = poolsRaw.map((poolItem, index) => {
const pool = asRecord(poolItem)
const persistence = asRecord(pool?.persistence)
const volumeClaimTemplate = asRecord(persistence?.volumeClaimTemplate ?? persistence?.volume_claim_template)
const resources = asRecord(volumeClaimTemplate?.resources)
const requests = asRecord(resources?.requests)
const servers = asPositiveInt(pool?.servers)
const volumesPerServer = asPositiveInt(pool?.volumesPerServer ?? pool?.volumes_per_server)
const storageSize = asString(pool?.storageSize ?? pool?.storage_size ?? pool?.size)
const volumesPerServer = asPositiveInt(
pool?.volumesPerServer ??
pool?.volumes_per_server ??
persistence?.volumesPerServer ??
persistence?.volumes_per_server,
)
const storageSize = asString(pool?.storageSize ?? pool?.storage_size ?? pool?.size ?? requests?.storage)

if (!pool || !servers || !volumesPerServer || !storageSize) {
throw new Error(t("YAML pool fields are invalid"))
Expand All @@ -146,11 +185,18 @@ export default function TenantCreatePage() {
servers,
volumes_per_server: volumesPerServer,
storage_size: storageSize,
storage_class: asString(pool.storageClass ?? pool.storage_class) || undefined,
storage_class:
asString(
pool.storageClass ??
pool.storage_class ??
volumeClaimTemplate?.storageClassName ??
volumeClaimTemplate?.storage_class_name,
) || undefined,
}
})

const specSc = asRecord(spec?.securityContext ?? spec?.security_context)
const credsSecretRef = asRecord(spec?.credsSecret ?? spec?.creds_secret)
const security_context = specSc
? {
runAsUser: asPositiveInt(specSc.runAsUser ?? specSc.run_as_user),
Expand All @@ -165,13 +211,72 @@ export default function TenantCreatePage() {
}
: undefined

const policiesRaw = spec?.policies
const policies = Array.isArray(policiesRaw)
? policiesRaw.map((item): ProvisioningPolicy => {
const policy = asRecord(item)
const document = asRecord(policy?.document)
const configMapKeyRef = asRecord(document?.configMapKeyRef ?? document?.config_map_key_ref)
const policyName = asString(policy?.name)
const configMapName = asString(configMapKeyRef?.name)
const key = asString(configMapKeyRef?.key)
if (!policy || !policyName || !configMapName || !key) {
throw new Error(t("YAML policy provisioning fields are invalid"))
}
return {
name: policyName,
document: {
configMapKeyRef: {
name: configMapName,
key,
},
},
}
})
: undefined

const usersRaw = spec?.users
const users = Array.isArray(usersRaw)
? usersRaw.map((item): ProvisioningUser => {
const user = asRecord(item)
const userName = asString(user?.name)
const userPolicies = asStringArray(user?.policies)
if (!user || !userName || !userPolicies || userPolicies.length === 0) {
throw new Error(t("YAML user provisioning fields are invalid"))
}
return {
name: userName,
policies: userPolicies,
}
})
: undefined

const bucketsRaw = spec?.buckets
const buckets = Array.isArray(bucketsRaw)
? bucketsRaw.map((item): ProvisioningBucket => {
const bucket = asRecord(item)
const bucketName = asString(bucket?.name)
if (!bucket || !bucketName) {
throw new Error(t("YAML bucket provisioning fields are invalid"))
}
return {
name: bucketName,
region: asString(bucket.region),
objectLock: asBoolean(bucket.objectLock ?? bucket.object_lock),
}
})
: undefined

return {
name: parsedName,
namespace: parsedNamespace,
pools: parsedPools,
image: asString(spec?.image),
mount_path: asString(spec?.mountPath ?? spec?.mount_path),
creds_secret: asString(spec?.credsSecret ?? spec?.creds_secret),
creds_secret: asString(spec?.credsSecret ?? spec?.creds_secret) ?? asString(credsSecretRef?.name),
policies,
users,
buckets,
security_context,
}
}
Expand Down
4 changes: 4 additions & 0 deletions console-web/i18n/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,13 @@
"Paste tenant YAML and create directly.": "Paste tenant YAML and create directly.",
"YAML Content": "YAML Content",
"YAML format is invalid": "YAML format is invalid",
"YAML must be a rustfs.com/v1alpha1 Tenant": "YAML must be a rustfs.com/v1alpha1 Tenant",
"YAML must include metadata.name and metadata.namespace": "YAML must include metadata.name and metadata.namespace",
"YAML must include spec.pools with at least one item": "YAML must include spec.pools with at least one item",
"YAML pool fields are invalid": "YAML pool fields are invalid",
"YAML policy provisioning fields are invalid": "YAML policy provisioning fields are invalid",
"YAML user provisioning fields are invalid": "YAML user provisioning fields are invalid",
"YAML bucket provisioning fields are invalid": "YAML bucket provisioning fields are invalid",
"Tenant name is required": "Tenant name is required",
"Namespace is required": "Namespace is required",
"Delete tenant \"{{name}}\"? This cannot be undone.": "Delete tenant \"{{name}}\"? This cannot be undone.",
Expand Down
4 changes: 4 additions & 0 deletions console-web/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,9 +137,13 @@
"Paste tenant YAML and create directly.": "粘贴租户 YAML 后可直接创建。",
"YAML Content": "YAML 内容",
"YAML format is invalid": "YAML 格式无效",
"YAML must be a rustfs.com/v1alpha1 Tenant": "YAML 必须是 rustfs.com/v1alpha1 Tenant",
"YAML must include metadata.name and metadata.namespace": "YAML 必须包含 metadata.name 和 metadata.namespace",
"YAML must include spec.pools with at least one item": "YAML 必须包含至少一个 spec.pools 项",
"YAML pool fields are invalid": "YAML 中 pool 字段不合法",
"YAML policy provisioning fields are invalid": "YAML 中 policy provisioning 字段不合法",
"YAML user provisioning fields are invalid": "YAML 中 user provisioning 字段不合法",
"YAML bucket provisioning fields are invalid": "YAML 中 bucket provisioning 字段不合法",
"Tenant name is required": "请输入租户名称",
"Namespace is required": "请输入命名空间",
"Delete tenant \"{{name}}\"? This cannot be undone.": "确定删除租户「{{name}}」?此操作不可恢复。",
Expand Down
3 changes: 3 additions & 0 deletions console-web/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ export async function updateTenant(
if (body.env !== undefined) payload.env = body.env
if (body.pod_management_policy !== undefined) payload.podManagementPolicy = body.pod_management_policy
if (body.image_pull_policy !== undefined) payload.imagePullPolicy = body.image_pull_policy
if (body.policies !== undefined) payload.policies = body.policies
if (body.users !== undefined) payload.users = body.users
if (body.buckets !== undefined) payload.buckets = body.buckets
if (body.logging !== undefined) payload.logging = body.logging
return apiClient.put(`${tenant(namespace, name)}`, Object.keys(payload).length ? payload : undefined)
}
Expand Down
Loading
Loading