mirror of
https://github.com/mulgadc/spinifex.git
synced 2026-10-15 17:11:30 +00:00
feat(ui): inline EKS cluster IAM role creation in wizard (#379)
* feat(ui): inline EKS cluster IAM role creation in wizard * feat(ui): show trust policy + attached managed policy in EKS role dialog Render the trust policy with PolicyDocumentViewer (pretty-printed, safe fallback) instead of a raw read-only textarea, matching the IAM detail pages. Surface the attached AmazonEKSClusterPolicy ARN so users can see exactly which permissions the cluster role grants.
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+49
-22
@@ -34,6 +34,7 @@ import {
|
||||
EKS_SUPPORTED_VERSIONS,
|
||||
} from "@/types/eks"
|
||||
|
||||
import { CreateClusterRoleDialog } from "./create-cluster-role-dialog"
|
||||
import { EksSystemImageRequired } from "./eks-system-image-required"
|
||||
|
||||
function vpcLabel(vpc: Vpc): string {
|
||||
@@ -51,6 +52,7 @@ export function CreateClusterPage() {
|
||||
const navigate = useNavigate()
|
||||
const queryClient = useQueryClient()
|
||||
const [isRechecking, setIsRechecking] = useState(false)
|
||||
const [isRoleDialogOpen, setIsRoleDialogOpen] = useState(false)
|
||||
const { data: vpcsData } = useSuspenseQuery(ec2VpcsQueryOptions)
|
||||
const { data: subnetsData } = useSuspenseQuery(ec2SubnetsQueryOptions)
|
||||
const { data: rolesData } = useSuspenseQuery(iamRolesQueryOptions)
|
||||
@@ -133,6 +135,15 @@ export function CreateClusterPage() {
|
||||
{ shouldValidate: true },
|
||||
)
|
||||
|
||||
const clusterName = watch("name")
|
||||
|
||||
const handleRoleCreated = async (roleArn: string) => {
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: iamRolesQueryOptions.queryKey,
|
||||
})
|
||||
setValue("roleArn", roleArn, { shouldValidate: true })
|
||||
}
|
||||
|
||||
const onSubmit = async (data: CreateClusterFormData) => {
|
||||
await createCluster.mutateAsync(data)
|
||||
await navigate({
|
||||
@@ -209,31 +220,47 @@ export function CreateClusterPage() {
|
||||
<FieldTitle>
|
||||
<label htmlFor="cluster-role">Cluster IAM role</label>
|
||||
</FieldTitle>
|
||||
<Controller
|
||||
control={control}
|
||||
name="roleArn"
|
||||
render={({ field }) => (
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger
|
||||
aria-invalid={!!errors.roleArn}
|
||||
className="w-full"
|
||||
id="cluster-role"
|
||||
>
|
||||
<SelectValue placeholder="Select role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{roles.map((role) => (
|
||||
<SelectItem key={role.Arn} value={role.Arn ?? ""}>
|
||||
{role.RoleName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<Controller
|
||||
control={control}
|
||||
name="roleArn"
|
||||
render={({ field }) => (
|
||||
<Select onValueChange={field.onChange} value={field.value}>
|
||||
<SelectTrigger
|
||||
aria-invalid={!!errors.roleArn}
|
||||
className="w-full"
|
||||
id="cluster-role"
|
||||
>
|
||||
<SelectValue placeholder="Select role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{roles.map((role) => (
|
||||
<SelectItem key={role.Arn} value={role.Arn ?? ""}>
|
||||
{role.RoleName}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => setIsRoleDialogOpen(true)}
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
Create role
|
||||
</Button>
|
||||
</div>
|
||||
<FieldError errors={[errors.roleArn]} />
|
||||
</Field>
|
||||
|
||||
<CreateClusterRoleDialog
|
||||
clusterName={clusterName}
|
||||
onCreated={handleRoleCreated}
|
||||
onOpenChange={setIsRoleDialogOpen}
|
||||
open={isRoleDialogOpen}
|
||||
/>
|
||||
|
||||
<Field>
|
||||
<FieldTitle>
|
||||
<label htmlFor="cluster-vpc">VPC</label>
|
||||
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog"
|
||||
import { Field, FieldTitle } from "@/components/ui/field"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { useAttachRolePolicy, useCreateRole } from "@/mutations/iam"
|
||||
import { PolicyDocumentViewer } from "@/routes/_auth/iam/-components/policy-document-viewer"
|
||||
import {
|
||||
AMAZON_EKS_CLUSTER_POLICY_ARN,
|
||||
EKS_CLUSTER_ASSUME_ROLE_POLICY_DOCUMENT,
|
||||
} from "@/types/eks"
|
||||
|
||||
interface CreateClusterRoleDialogProps {
|
||||
clusterName: string
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onCreated: (roleArn: string) => void
|
||||
}
|
||||
|
||||
function defaultRoleName(clusterName: string): string {
|
||||
const base = clusterName.trim() || "cluster"
|
||||
return `${base}-eks-cluster-role`
|
||||
}
|
||||
|
||||
export function CreateClusterRoleDialog({
|
||||
clusterName,
|
||||
open,
|
||||
onOpenChange,
|
||||
onCreated,
|
||||
}: CreateClusterRoleDialogProps) {
|
||||
const createRole = useCreateRole()
|
||||
const attachPolicy = useAttachRolePolicy()
|
||||
const [roleName, setRoleName] = useState(defaultRoleName(clusterName))
|
||||
const [formError, setFormError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setRoleName(defaultRoleName(clusterName))
|
||||
setFormError(null)
|
||||
}
|
||||
}, [open, clusterName])
|
||||
|
||||
const isPending = createRole.isPending || attachPolicy.isPending
|
||||
|
||||
function handleClose(nextOpen: boolean) {
|
||||
if (!nextOpen) {
|
||||
setFormError(null)
|
||||
}
|
||||
onOpenChange(nextOpen)
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
setFormError(null)
|
||||
const name = roleName.trim()
|
||||
try {
|
||||
const result = await createRole.mutateAsync({
|
||||
roleName: name,
|
||||
assumeRolePolicyDocument: EKS_CLUSTER_ASSUME_ROLE_POLICY_DOCUMENT,
|
||||
})
|
||||
await attachPolicy.mutateAsync({
|
||||
roleName: name,
|
||||
policyArn: AMAZON_EKS_CLUSTER_POLICY_ARN,
|
||||
})
|
||||
const arn = result.Role?.Arn
|
||||
if (!arn) {
|
||||
setFormError("Role was created but no ARN was returned.")
|
||||
return
|
||||
}
|
||||
onCreated(arn)
|
||||
onOpenChange(false)
|
||||
} catch (error) {
|
||||
setFormError(
|
||||
error instanceof Error ? error.message : "Failed to create role.",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AlertDialog onOpenChange={handleClose} open={open}>
|
||||
<AlertDialogContent className="max-w-lg sm:max-w-lg">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Create cluster IAM role</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Creates an IAM role trusted by the EKS control plane and attaches
|
||||
the AmazonEKSClusterPolicy managed policy.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<Field>
|
||||
<FieldTitle>
|
||||
<label htmlFor="eks-role-name">Role name</label>
|
||||
</FieldTitle>
|
||||
<Input
|
||||
id="eks-role-name"
|
||||
onChange={(e) => setRoleName(e.target.value)}
|
||||
placeholder="my-cluster-eks-cluster-role"
|
||||
value={roleName}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldTitle>Trust policy (JSON)</FieldTitle>
|
||||
<PolicyDocumentViewer
|
||||
document={EKS_CLUSTER_ASSUME_ROLE_POLICY_DOCUMENT}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldTitle>Attached managed policy</FieldTitle>
|
||||
<code className="block rounded-md border bg-muted p-2 font-mono text-sm break-all">
|
||||
{AMAZON_EKS_CLUSTER_POLICY_ARN}
|
||||
</code>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Grants the EKS control plane the permissions it needs to manage the
|
||||
cluster.
|
||||
</p>
|
||||
</Field>
|
||||
|
||||
{formError && <p className="text-sm text-destructive">{formError}</p>}
|
||||
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
disabled={isPending || !roleName.trim()}
|
||||
onClick={handleCreate}
|
||||
>
|
||||
{isPending ? "Creating…" : "Create role"}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
@@ -31,6 +31,26 @@ export const EKS_ACCESS_POLICIES = [
|
||||
export const EKS_AMI_TYPES = ["AL2_x86_64", "AL2_ARM_64"] as const
|
||||
export const EKS_CAPACITY_TYPES = ["ON_DEMAND", "SPOT"] as const
|
||||
|
||||
// Trust policy for an EKS cluster IAM role: the EKS control plane assumes it.
|
||||
export const EKS_CLUSTER_ASSUME_ROLE_POLICY_DOCUMENT = JSON.stringify(
|
||||
{
|
||||
Version: "2012-10-17",
|
||||
Statement: [
|
||||
{
|
||||
Effect: "Allow",
|
||||
Principal: { Service: "eks.amazonaws.com" },
|
||||
Action: "sts:AssumeRole",
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
)
|
||||
|
||||
// Managed policy granting the EKS control plane the permissions it needs.
|
||||
export const AMAZON_EKS_CLUSTER_POLICY_ARN =
|
||||
"arn:aws:iam::aws:policy/AmazonEKSClusterPolicy"
|
||||
|
||||
const eksNameField = z
|
||||
.string()
|
||||
.min(1, "Name is required")
|
||||
|
||||
Reference in New Issue
Block a user