mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 18:29:07 +00:00
Merge pull request #4711 from Infisical/feat/add-dynamic-secret-kube-auth-vault-migration
feat: add dynamic secret kube role vault migration
This commit is contained in:
@@ -5,7 +5,8 @@ import { apiRequest } from "@app/config/request";
|
||||
import {
|
||||
ExternalMigrationProviders,
|
||||
TVaultExternalMigrationConfig,
|
||||
VaultKubernetesAuthRole
|
||||
VaultKubernetesAuthRole,
|
||||
VaultKubernetesRole
|
||||
} from "./types";
|
||||
|
||||
export const externalMigrationQueryKeys = {
|
||||
@@ -31,6 +32,11 @@ export const externalMigrationQueryKeys = {
|
||||
"vault-kubernetes-auth-roles",
|
||||
namespace,
|
||||
mountPath
|
||||
],
|
||||
vaultKubernetesRoles: (namespace?: string, mountPath?: string) => [
|
||||
"vault-kubernetes-roles",
|
||||
namespace,
|
||||
mountPath
|
||||
]
|
||||
};
|
||||
|
||||
@@ -172,3 +178,30 @@ export const useGetVaultKubernetesAuthRoles = (
|
||||
enabled: enabled && !!namespace && !!mountPath
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetVaultKubernetesRoles = (
|
||||
enabled = true,
|
||||
namespace?: string,
|
||||
mountPath?: string
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: externalMigrationQueryKeys.vaultKubernetesRoles(namespace, mountPath),
|
||||
queryFn: async () => {
|
||||
if (!namespace || !mountPath) {
|
||||
throw new Error("Both namespace and mountPath are required");
|
||||
}
|
||||
|
||||
const { data } = await apiRequest.get<{
|
||||
roles: VaultKubernetesRole[];
|
||||
}>("/api/v3/external-migration/vault/kubernetes-roles", {
|
||||
params: {
|
||||
namespace,
|
||||
mountPath
|
||||
}
|
||||
});
|
||||
|
||||
return data.roles;
|
||||
},
|
||||
enabled: enabled && !!namespace && !!mountPath
|
||||
});
|
||||
};
|
||||
|
||||
@@ -49,3 +49,24 @@ export type VaultKubernetesAuthRole = {
|
||||
disable_local_ca_jwt?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type VaultKubernetesRole = {
|
||||
name: string;
|
||||
mountPath: string;
|
||||
allowed_kubernetes_namespaces?: string[];
|
||||
allowed_kubernetes_namespace_selector?: string;
|
||||
token_max_ttl?: number;
|
||||
token_default_ttl?: number;
|
||||
token_default_audiences?: string[];
|
||||
service_account_name?: string;
|
||||
kubernetes_role_name?: string;
|
||||
kubernetes_role_type?: string;
|
||||
generated_role_rules?: string;
|
||||
name_template?: string;
|
||||
extra_annotations?: Record<string, string>;
|
||||
extra_labels?: Record<string, string>;
|
||||
config: {
|
||||
kubernetes_host: string;
|
||||
kubernetes_ca_cert?: string;
|
||||
};
|
||||
};
|
||||
|
||||
+25
-13
@@ -22,11 +22,12 @@ import {
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useSubscription } from "@app/context";
|
||||
import { useOrganization, useOrgPermission, useSubscription } from "@app/context";
|
||||
import {
|
||||
OrgGatewayPermissionActions,
|
||||
OrgPermissionSubjects
|
||||
} from "@app/context/OrgPermissionContext/types";
|
||||
import { OrgMembershipRole } from "@app/helpers/roles";
|
||||
import {
|
||||
gatewaysQueryKeys,
|
||||
useAddIdentityKubernetesAuth,
|
||||
@@ -129,6 +130,8 @@ export const IdentityKubernetesAuthForm = ({
|
||||
] as const);
|
||||
const { data: vaultConfigs = [] } = useGetVaultExternalMigrationConfigs();
|
||||
const hasVaultConnection = vaultConfigs.some((config) => config.connectionId);
|
||||
const { hasOrgRole } = useOrgPermission();
|
||||
const isOrgAdmin = hasOrgRole(OrgMembershipRole.Admin);
|
||||
|
||||
const {
|
||||
control,
|
||||
@@ -409,20 +412,29 @@ export const IdentityKubernetesAuthForm = ({
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="mt-0.5 text-primary" />
|
||||
<span className="text-mineshaft-200">Load values from HashiCorp Vault</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
leftIcon={
|
||||
<img
|
||||
src="/images/integrations/Vault.png"
|
||||
alt="HashiCorp Vault"
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<Tooltip
|
||||
content={
|
||||
!isOrgAdmin
|
||||
? "Only organization admins can import configurations from HashiCorp Vault"
|
||||
: undefined
|
||||
}
|
||||
onClick={() => handleImportPopUpToggle("importFromVault", true)}
|
||||
>
|
||||
Load from Vault
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
leftIcon={
|
||||
<img
|
||||
src="/images/integrations/Vault.png"
|
||||
alt="HashiCorp Vault"
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
}
|
||||
onClick={() => handleImportPopUpToggle("importFromVault", true)}
|
||||
isDisabled={!isOrgAdmin}
|
||||
>
|
||||
Load from Vault
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex w-full items-center gap-2">
|
||||
|
||||
+29
-17
@@ -55,6 +55,7 @@ import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionDynamicSecretActions,
|
||||
ProjectPermissionSub,
|
||||
useOrgPermission,
|
||||
useProject,
|
||||
useProjectPermission,
|
||||
useSubscription
|
||||
@@ -64,6 +65,7 @@ import {
|
||||
ProjectPermissionSecretActions,
|
||||
ProjectPermissionSecretRotationActions
|
||||
} from "@app/context/ProjectPermissionContext/types";
|
||||
import { OrgMembershipRole } from "@app/helpers/roles";
|
||||
import { usePopUp } from "@app/hooks";
|
||||
import {
|
||||
useCreateFolder,
|
||||
@@ -203,6 +205,8 @@ export const ActionBar = ({
|
||||
const { permission } = useProjectPermission();
|
||||
const { data: vaultConfigs = [] } = useGetVaultExternalMigrationConfigs();
|
||||
const hasVaultConnection = vaultConfigs.some((config) => config.connectionId);
|
||||
const { hasOrgRole } = useOrgPermission();
|
||||
const isOrgAdmin = hasOrgRole(OrgMembershipRole.Admin);
|
||||
|
||||
const handleFolderCreate = async (folderName: string, description: string | null) => {
|
||||
try {
|
||||
@@ -1121,25 +1125,33 @@ export const ActionBar = ({
|
||||
})}
|
||||
>
|
||||
{(isAllowed) => (
|
||||
<Button
|
||||
leftIcon={
|
||||
<img
|
||||
src="/images/integrations/Vault.png"
|
||||
alt="HashiCorp Vault"
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
<Tooltip
|
||||
content={
|
||||
!isOrgAdmin
|
||||
? "Only organization admins can import secrets from HashiCorp Vault"
|
||||
: undefined
|
||||
}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("importFromVault");
|
||||
handlePopUpClose("misc");
|
||||
}}
|
||||
isDisabled={!isAllowed}
|
||||
variant="outline_bg"
|
||||
className="h-10 text-left"
|
||||
isFullWidth
|
||||
>
|
||||
Add from HashiCorp Vault
|
||||
</Button>
|
||||
<Button
|
||||
leftIcon={
|
||||
<img
|
||||
src="/images/integrations/Vault.png"
|
||||
alt="HashiCorp Vault"
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
handlePopUpOpen("importFromVault");
|
||||
handlePopUpClose("misc");
|
||||
}}
|
||||
isDisabled={!isAllowed || !isOrgAdmin}
|
||||
variant="outline_bg"
|
||||
className="h-10 text-left"
|
||||
isFullWidth
|
||||
>
|
||||
Add from HashiCorp Vault
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
</ProjectPermissionCan>
|
||||
)}
|
||||
|
||||
+123
-5
@@ -1,5 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { Controller, FieldValues, useFieldArray, useForm } from "react-hook-form";
|
||||
import { faQuestionCircle, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faInfoCircle, faQuestionCircle, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
@@ -21,16 +22,22 @@ import {
|
||||
TextArea,
|
||||
Tooltip
|
||||
} from "@app/components/v2";
|
||||
import { useOrgPermission } from "@app/context";
|
||||
import { OrgPermissionSubjects } from "@app/context/OrgPermissionContext";
|
||||
import { OrgGatewayPermissionActions } from "@app/context/OrgPermissionContext/types";
|
||||
import { OrgMembershipRole } from "@app/helpers/roles";
|
||||
import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import {
|
||||
DynamicSecretProviders,
|
||||
KubernetesDynamicSecretCredentialType
|
||||
} from "@app/hooks/api/dynamicSecret/types";
|
||||
import { useGetVaultExternalMigrationConfigs } from "@app/hooks/api/migration/queries";
|
||||
import { VaultKubernetesRole } from "@app/hooks/api/migration/types";
|
||||
import { ProjectEnv } from "@app/hooks/api/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
import { VaultKubernetesImportModal } from "./VaultKubernetesImportModal";
|
||||
|
||||
enum RoleType {
|
||||
ClusterRole = "cluster-role",
|
||||
Role = "role"
|
||||
@@ -162,11 +169,14 @@ export const KubernetesInputForm = ({
|
||||
environments,
|
||||
isSingleEnvironmentMode
|
||||
}: Props) => {
|
||||
const [isVaultImportModalOpen, setIsVaultImportModalOpen] = useState(false);
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
handleSubmit,
|
||||
watch
|
||||
watch,
|
||||
setValue
|
||||
} = useForm<TForm>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
@@ -186,18 +196,88 @@ export const KubernetesInputForm = ({
|
||||
}
|
||||
});
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
const { fields, append, remove, replace } = useFieldArray({
|
||||
control,
|
||||
name: "provider.audiences"
|
||||
});
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
||||
const { data: vaultConfigs = [] } = useGetVaultExternalMigrationConfigs();
|
||||
const hasVaultConnection = vaultConfigs.some((config) => config.connectionId);
|
||||
const { hasOrgRole } = useOrgPermission();
|
||||
const isOrgAdmin = hasOrgRole(OrgMembershipRole.Admin);
|
||||
|
||||
const sslEnabled = watch("provider.sslEnabled");
|
||||
const credentialType = watch("provider.credentialType");
|
||||
const authMethod = watch("provider.authMethod");
|
||||
|
||||
const handleVaultImport = (role: VaultKubernetesRole) => {
|
||||
try {
|
||||
setValue("name", role.name);
|
||||
|
||||
setValue("provider.url", role.config.kubernetes_host);
|
||||
|
||||
// Set CA certificate if available
|
||||
if (role.config.kubernetes_ca_cert) {
|
||||
setValue("provider.ca", role.config.kubernetes_ca_cert);
|
||||
setValue("provider.sslEnabled", true);
|
||||
}
|
||||
|
||||
// Determine credential type based on role configuration
|
||||
if (role.service_account_name) {
|
||||
// Static credential type
|
||||
setValue("provider.credentialType", KubernetesDynamicSecretCredentialType.Static);
|
||||
setValue("provider.serviceAccountName", role.service_account_name);
|
||||
|
||||
// Set namespace (single namespace for static)
|
||||
if (role.allowed_kubernetes_namespaces && role.allowed_kubernetes_namespaces.length > 0) {
|
||||
setValue("provider.namespace", role.allowed_kubernetes_namespaces[0]);
|
||||
}
|
||||
} else if (role.kubernetes_role_name) {
|
||||
// Dynamic credential type
|
||||
setValue("provider.credentialType", KubernetesDynamicSecretCredentialType.Dynamic);
|
||||
setValue("provider.role", role.kubernetes_role_name);
|
||||
|
||||
// Set role type
|
||||
const roleType =
|
||||
role.kubernetes_role_type === "ClusterRole" ? RoleType.ClusterRole : RoleType.Role;
|
||||
setValue("provider.roleType", roleType);
|
||||
|
||||
// Set allowed namespaces (comma-separated for dynamic)
|
||||
if (role.allowed_kubernetes_namespaces && role.allowed_kubernetes_namespaces.length > 0) {
|
||||
setValue("provider.namespace", role.allowed_kubernetes_namespaces.join(", "));
|
||||
}
|
||||
}
|
||||
|
||||
// Set TTLs if available
|
||||
if (role.token_default_ttl) {
|
||||
const defaultTTL = `${role.token_default_ttl}s`;
|
||||
setValue("defaultTTL", defaultTTL);
|
||||
}
|
||||
|
||||
if (role.token_max_ttl) {
|
||||
const maxTTL = `${role.token_max_ttl}s`;
|
||||
setValue("maxTTL", maxTTL);
|
||||
}
|
||||
|
||||
// Set audiences if available
|
||||
if (role.token_default_audiences && role.token_default_audiences.length > 0) {
|
||||
replace(role.token_default_audiences);
|
||||
}
|
||||
|
||||
createNotification({
|
||||
type: "info",
|
||||
text: "Configuration loaded successfully from HashiCorp Vault"
|
||||
});
|
||||
} catch {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to load configuration from HashiCorp Vault"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateDynamicSecret = async (formData: TForm) => {
|
||||
const { provider, usernameTemplate, ...rest } = formData;
|
||||
// wait till previous request is finished
|
||||
@@ -235,6 +315,39 @@ export const KubernetesInputForm = ({
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
|
||||
<div>
|
||||
{hasVaultConnection && (
|
||||
<div className="mb-4 flex items-center justify-between rounded-md border border-primary-400/30 bg-primary/10 px-3 py-2.5">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="text-primary" />
|
||||
<span className="text-mineshaft-200">Load values from HashiCorp Vault</span>
|
||||
</div>
|
||||
<Tooltip
|
||||
content={
|
||||
!isOrgAdmin
|
||||
? "Only organization admins can import configurations from HashiCorp Vault"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="outline_bg"
|
||||
size="xs"
|
||||
type="button"
|
||||
onClick={() => setIsVaultImportModalOpen(true)}
|
||||
isDisabled={!isOrgAdmin}
|
||||
leftIcon={
|
||||
<img
|
||||
src="/images/integrations/Vault.png"
|
||||
alt="HashiCorp Vault"
|
||||
className="h-4 w-4"
|
||||
/>
|
||||
}
|
||||
>
|
||||
Load from Vault
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="grow">
|
||||
<Controller
|
||||
@@ -286,8 +399,8 @@ export const KubernetesInputForm = ({
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="mt-4 mb-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
||||
Configuration
|
||||
<div className="mt-4 mb-4 border-b border-mineshaft-500 pb-2 pl-1">
|
||||
<h3 className="font-medium text-mineshaft-200">Configuration</h3>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -654,6 +767,11 @@ export const KubernetesInputForm = ({
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
<VaultKubernetesImportModal
|
||||
isOpen={isVaultImportModalOpen}
|
||||
onOpenChange={setIsVaultImportModalOpen}
|
||||
onImport={handleVaultImport}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
+223
@@ -0,0 +1,223 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
useGetVaultKubernetesRoles,
|
||||
useGetVaultMounts,
|
||||
useGetVaultNamespaces
|
||||
} from "@app/hooks/api/migration/queries";
|
||||
import { VaultKubernetesRole } from "@app/hooks/api/migration/types";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
onImport: (role: VaultKubernetesRole) => void;
|
||||
};
|
||||
|
||||
type ContentProps = {
|
||||
onClose: () => void;
|
||||
onImport: (role: VaultKubernetesRole) => void;
|
||||
};
|
||||
|
||||
const Content = ({ onClose, onImport }: ContentProps) => {
|
||||
const [selectedNamespace, setSelectedNamespace] = useState<string | null>(null);
|
||||
const [selectedMountPath, setSelectedMountPath] = useState<string | null>(null);
|
||||
const [selectedRole, setSelectedRole] = useState<VaultKubernetesRole | null>(null);
|
||||
const [shouldFetchRoles, setShouldFetchRoles] = useState(false);
|
||||
const [shouldFetchMounts, setShouldFetchMounts] = useState(false);
|
||||
|
||||
const { data: namespaces, isLoading: isLoadingNamespaces } = useGetVaultNamespaces();
|
||||
const { data: roles, isLoading: isLoadingRoles } = useGetVaultKubernetesRoles(
|
||||
shouldFetchRoles,
|
||||
selectedNamespace ?? undefined,
|
||||
selectedMountPath ?? undefined
|
||||
);
|
||||
const { data: mounts, isLoading: isLoadingMounts } = useGetVaultMounts(
|
||||
shouldFetchMounts,
|
||||
selectedNamespace ?? undefined
|
||||
);
|
||||
|
||||
// Filter to only show Kubernetes mounts
|
||||
const kubernetesMounts = mounts?.filter((mount) => mount.type === "kubernetes");
|
||||
|
||||
// Enable fetching mounts when namespace is selected
|
||||
useEffect(() => {
|
||||
if (selectedNamespace) {
|
||||
setShouldFetchMounts(true);
|
||||
}
|
||||
}, [selectedNamespace]);
|
||||
|
||||
// Enable fetching roles when both namespace and mount path are selected
|
||||
useEffect(() => {
|
||||
if (selectedNamespace && selectedMountPath) {
|
||||
setShouldFetchRoles(true);
|
||||
} else {
|
||||
setShouldFetchRoles(false);
|
||||
}
|
||||
}, [selectedNamespace, selectedMountPath]);
|
||||
|
||||
const handleImport = () => {
|
||||
if (!selectedRole) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Please select a Vault Kubernetes role to load"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedNamespace) {
|
||||
createNotification({ type: "error", text: "Please select a namespace" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mounts || mounts.length === 0) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "No Vault mounts found. Please ensure you have Kubernetes secrets engine configured."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
onImport(selectedRole);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 rounded-md bg-primary/10 p-3 text-sm text-mineshaft-200">
|
||||
<div className="flex items-start gap-2">
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="mt-0.5 text-primary" />
|
||||
<div className="space-y-1.5 text-xs leading-relaxed">
|
||||
<p>
|
||||
Select a Kubernetes secrets engine role from Vault to pre-fill the form with its
|
||||
configuration including cluster URL, CA certificate, TTL settings, etc.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormControl
|
||||
label="Namespace"
|
||||
className="mb-4"
|
||||
tooltipText="Select the Vault namespace containing the Kubernetes secrets engine."
|
||||
>
|
||||
<>
|
||||
<FilterableSelect
|
||||
value={namespaces?.find((ns) => ns.name === selectedNamespace)}
|
||||
onChange={(value) => {
|
||||
if (value && !Array.isArray(value)) {
|
||||
const namespace = value as { id: string; name: string };
|
||||
setSelectedNamespace(namespace.name);
|
||||
setSelectedMountPath(null);
|
||||
setSelectedRole(null);
|
||||
}
|
||||
}}
|
||||
options={namespaces || []}
|
||||
getOptionValue={(option) => option.name}
|
||||
getOptionLabel={(option) => (option.name === "/" ? "root" : option.name)}
|
||||
isDisabled={isLoadingNamespaces}
|
||||
placeholder="Select namespace..."
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-mineshaft-400">
|
||||
Select the Vault namespace to fetch available Kubernetes secrets engines
|
||||
</p>
|
||||
</>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
label="Kubernetes Secrets Engine"
|
||||
className="mb-4"
|
||||
tooltipText="Select the Kubernetes secrets engine mount to fetch available roles."
|
||||
>
|
||||
<>
|
||||
<FilterableSelect
|
||||
value={kubernetesMounts?.find((mount) => mount.path === selectedMountPath)}
|
||||
onChange={(value) => {
|
||||
if (value && !Array.isArray(value)) {
|
||||
const mount = value as { path: string; type: string; version: string | null };
|
||||
setSelectedMountPath(mount.path.replace(/\/$/, "")); // Remove trailing slash
|
||||
setSelectedRole(null);
|
||||
}
|
||||
}}
|
||||
options={kubernetesMounts || []}
|
||||
getOptionValue={(option) => option.path}
|
||||
getOptionLabel={(option) => option.path.replace(/\/$/, "")}
|
||||
isDisabled={isLoadingMounts || !kubernetesMounts?.length}
|
||||
placeholder="Select Kubernetes secrets engine..."
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-mineshaft-400">
|
||||
Choose a Kubernetes secrets engine mount to list available roles
|
||||
</p>
|
||||
</>
|
||||
</FormControl>
|
||||
|
||||
<FormControl label="Kubernetes Role" className="mb-6">
|
||||
<>
|
||||
<FilterableSelect
|
||||
value={selectedRole}
|
||||
onChange={(value) => {
|
||||
if (value && !Array.isArray(value)) {
|
||||
setSelectedRole(value as VaultKubernetesRole);
|
||||
} else {
|
||||
setSelectedRole(null);
|
||||
}
|
||||
}}
|
||||
options={roles || []}
|
||||
getOptionValue={(option) => option.name}
|
||||
getOptionLabel={(option) => option.name}
|
||||
isDisabled={isLoadingRoles || !roles?.length || !selectedMountPath}
|
||||
placeholder={
|
||||
!selectedMountPath ? "Select a mount path first..." : "Select a role to load..."
|
||||
}
|
||||
isClearable
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-mineshaft-400">
|
||||
Choose a Kubernetes role from the selected mount to load its configuration
|
||||
</p>
|
||||
</>
|
||||
</FormControl>
|
||||
|
||||
<div className="mt-8 flex space-x-4">
|
||||
<Button
|
||||
onClick={handleImport}
|
||||
isDisabled={!selectedRole || isLoadingMounts || isLoadingRoles}
|
||||
>
|
||||
Load Configuration
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const VaultKubernetesImportModal = ({ isOpen, onOpenChange, onImport }: Props) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
bodyClassName="overflow-visible"
|
||||
title="Load from HashiCorp Vault"
|
||||
subTitle="Select a Kubernetes secrets engine role to load its configuration."
|
||||
className="max-w-2xl"
|
||||
>
|
||||
<Content onClose={() => onOpenChange(false)} onImport={onImport} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user