mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 16:26:37 +00:00
feat: add dynamic secret kube auth vault migration
This commit is contained in:
@@ -393,6 +393,56 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/vault/kubernetes-roles",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
namespace: z.string(),
|
||||||
|
mountPath: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
roles: z.array(
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
mountPath: z.string(),
|
||||||
|
allowed_kubernetes_namespaces: z.array(z.string()).nullish(),
|
||||||
|
allowed_kubernetes_namespace_selector: z.string().nullish(),
|
||||||
|
token_max_ttl: z.number().nullish(),
|
||||||
|
token_default_ttl: z.number().nullish(),
|
||||||
|
token_default_audiences: z.array(z.string()).nullish(),
|
||||||
|
service_account_name: z.string().nullish(),
|
||||||
|
kubernetes_role_name: z.string().nullish(),
|
||||||
|
kubernetes_role_type: z.string().nullish(),
|
||||||
|
generated_role_rules: z.string().nullish(),
|
||||||
|
name_template: z.string().nullish(),
|
||||||
|
extra_annotations: z.record(z.string()).nullish(),
|
||||||
|
extra_labels: z.record(z.string()).nullish(),
|
||||||
|
config: z.object({
|
||||||
|
kubernetes_host: z.string(),
|
||||||
|
kubernetes_ca_cert: z.string().nullish()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const roles = await server.services.migration.getVaultKubernetesRoles({
|
||||||
|
actor: req.permission,
|
||||||
|
namespace: req.query.namespace,
|
||||||
|
mountPath: req.query.mountPath
|
||||||
|
});
|
||||||
|
|
||||||
|
return { roles };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
server.route({
|
server.route({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/vault/secret-paths",
|
url: "/vault/secret-paths",
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import {
|
|||||||
THCVaultKubernetesAuthConfig,
|
THCVaultKubernetesAuthConfig,
|
||||||
THCVaultKubernetesAuthRole,
|
THCVaultKubernetesAuthRole,
|
||||||
THCVaultKubernetesAuthRoleWithConfig,
|
THCVaultKubernetesAuthRoleWithConfig,
|
||||||
|
THCVaultKubernetesRole,
|
||||||
|
THCVaultKubernetesSecretsConfig,
|
||||||
THCVaultMount,
|
THCVaultMount,
|
||||||
THCVaultMountResponse
|
THCVaultMountResponse
|
||||||
} from "./hc-vault-connection-types";
|
} from "./hc-vault-connection-types";
|
||||||
@@ -799,3 +801,122 @@ export const getHCVaultKubernetesAuthRoles = async (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getHCVaultKubernetesRoles = async (
|
||||||
|
namespace: string,
|
||||||
|
mountPath: string,
|
||||||
|
connection: THCVaultConnection,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||||
|
): Promise<THCVaultKubernetesRole[]> => {
|
||||||
|
// Remove trailing slash from mount path
|
||||||
|
const cleanMountPath = mountPath.endsWith("/") ? mountPath.slice(0, -1) : mountPath;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const instanceUrl = await getHCVaultInstanceUrl(connection);
|
||||||
|
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
|
||||||
|
// 1. Get the Kubernetes secrets engine configuration for this mount
|
||||||
|
const { data: configResponse } = await requestWithHCVaultGateway<{ data: THCVaultKubernetesSecretsConfig }>(
|
||||||
|
connection,
|
||||||
|
gatewayService,
|
||||||
|
{
|
||||||
|
url: `${instanceUrl}/v1/${cleanMountPath}/config`,
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"X-Vault-Token": accessToken,
|
||||||
|
"X-Vault-Namespace": namespace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const kubernetesConfig = configResponse.data;
|
||||||
|
|
||||||
|
// 2. List all roles in this mount
|
||||||
|
let roleNames: string[] = [];
|
||||||
|
try {
|
||||||
|
const { data: roleListResponse } = await requestWithHCVaultGateway<{ data: { keys: string[] } }>(
|
||||||
|
connection,
|
||||||
|
gatewayService,
|
||||||
|
{
|
||||||
|
url: `${instanceUrl}/v1/${cleanMountPath}/roles?list=true`,
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"X-Vault-Token": accessToken,
|
||||||
|
"X-Vault-Namespace": namespace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
roleNames = roleListResponse.data.keys || [];
|
||||||
|
} catch (error) {
|
||||||
|
// Vault returns 404 when no roles are configured yet
|
||||||
|
if (error && typeof error === "object" && "response" in error) {
|
||||||
|
const axiosError = error as { response?: { status?: number } };
|
||||||
|
if (axiosError.response?.status === 404) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!roleNames || roleNames.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fetch details for each role with concurrency control
|
||||||
|
const limiter = createConcurrencyLimiter(HC_VAULT_CONCURRENCY_LIMIT);
|
||||||
|
|
||||||
|
const roleDetailsPromises = roleNames.map((roleName) =>
|
||||||
|
limiter(async () => {
|
||||||
|
const { data: roleResponse } = await requestWithHCVaultGateway<{
|
||||||
|
data: {
|
||||||
|
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>;
|
||||||
|
};
|
||||||
|
}>(connection, gatewayService, {
|
||||||
|
url: `${instanceUrl}/v1/${cleanMountPath}/roles/${roleName}`,
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"X-Vault-Token": accessToken,
|
||||||
|
"X-Vault-Namespace": namespace
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. Merge the role with the config
|
||||||
|
return {
|
||||||
|
...roleResponse.data,
|
||||||
|
name: roleName,
|
||||||
|
config: kubernetesConfig,
|
||||||
|
mountPath: cleanMountPath
|
||||||
|
} as THCVaultKubernetesRole;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const roles = await Promise.all(roleDetailsPromises);
|
||||||
|
|
||||||
|
return roles;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
logger.error(error, "Unable to list HC Vault Kubernetes secrets engine roles");
|
||||||
|
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
const errorMessage =
|
||||||
|
(error.response?.data as { errors?: string[] })?.errors?.[0] || error.message || "Unknown error";
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to list Kubernetes secrets engine roles: ${errorMessage}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to list Kubernetes secrets engine roles from HashiCorp Vault"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -95,3 +95,26 @@ export type THCVaultKubernetesAuthRoleWithConfig = THCVaultKubernetesAuthRole &
|
|||||||
config: THCVaultKubernetesAuthConfig;
|
config: THCVaultKubernetesAuthConfig;
|
||||||
mountPath: string;
|
mountPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type THCVaultKubernetesSecretsConfig = {
|
||||||
|
kubernetes_host: string;
|
||||||
|
kubernetes_ca_cert?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type THCVaultKubernetesRole = {
|
||||||
|
name: 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: THCVaultKubernetesSecretsConfig;
|
||||||
|
mountPath: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { TAppConnectionServiceFactory } from "../app-connection/app-connection-s
|
|||||||
import {
|
import {
|
||||||
getHCVaultAuthMounts,
|
getHCVaultAuthMounts,
|
||||||
getHCVaultKubernetesAuthRoles,
|
getHCVaultKubernetesAuthRoles,
|
||||||
|
getHCVaultKubernetesRoles,
|
||||||
getHCVaultSecretsForPath,
|
getHCVaultSecretsForPath,
|
||||||
HCVaultAuthType,
|
HCVaultAuthType,
|
||||||
listHCVaultMounts,
|
listHCVaultMounts,
|
||||||
@@ -747,6 +748,55 @@ export const externalMigrationServiceFactory = ({
|
|||||||
return roles;
|
return roles;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getVaultKubernetesRoles = async ({
|
||||||
|
actor,
|
||||||
|
namespace,
|
||||||
|
mountPath
|
||||||
|
}: {
|
||||||
|
actor: OrgServiceActor;
|
||||||
|
namespace: string;
|
||||||
|
mountPath: string;
|
||||||
|
}) => {
|
||||||
|
const { hasRole } = await permissionService.getOrgPermission(
|
||||||
|
actor.type,
|
||||||
|
actor.id,
|
||||||
|
actor.orgId,
|
||||||
|
actor.authMethod,
|
||||||
|
actor.orgId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!hasRole(OrgMembershipRole.Admin)) {
|
||||||
|
throw new ForbiddenRequestError({ message: "Only admins can get Kubernetes roles" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({
|
||||||
|
orgId: actor.orgId,
|
||||||
|
namespace
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!vaultConfig) {
|
||||||
|
throw new NotFoundError({ message: "Vault migration config not found for this namespace" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!vaultConfig.connection) {
|
||||||
|
throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentials = await decryptAppConnectionCredentials({
|
||||||
|
orgId: vaultConfig.orgId,
|
||||||
|
encryptedCredentials: vaultConfig.connection.encryptedCredentials,
|
||||||
|
kmsService,
|
||||||
|
projectId: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const connection = {
|
||||||
|
...vaultConfig.connection,
|
||||||
|
credentials
|
||||||
|
} as THCVaultConnection;
|
||||||
|
|
||||||
|
return getHCVaultKubernetesRoles(namespace, mountPath, connection, gatewayService);
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
importEnvKeyData,
|
importEnvKeyData,
|
||||||
importVaultData,
|
importVaultData,
|
||||||
@@ -761,6 +811,7 @@ export const externalMigrationServiceFactory = ({
|
|||||||
getVaultAuthMounts,
|
getVaultAuthMounts,
|
||||||
getVaultSecretPaths,
|
getVaultSecretPaths,
|
||||||
importVaultSecrets,
|
importVaultSecrets,
|
||||||
getVaultKubernetesAuthRoles
|
getVaultKubernetesAuthRoles,
|
||||||
|
getVaultKubernetesRoles
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,6 +26,12 @@ Infisical provides two approaches for migrating from HashiCorp Vault.
|
|||||||
|
|
||||||
This migration approach lets you set up a connection to your Vault instance once, then import specific resources as needed throughout Infisical.
|
This migration approach lets you set up a connection to your Vault instance once, then import specific resources as needed throughout Infisical.
|
||||||
|
|
||||||
|
<Warning>
|
||||||
|
**Organization Admin Access Required:** All in-platform migration features
|
||||||
|
(importing secrets, Kubernetes configurations, and policies from Vault) are
|
||||||
|
only accessible to organization admins.
|
||||||
|
</Warning>
|
||||||
|
|
||||||
### Step 1: Set Up Your Vault Connection
|
### Step 1: Set Up Your Vault Connection
|
||||||
|
|
||||||
<Steps>
|
<Steps>
|
||||||
@@ -84,6 +90,19 @@ This migration approach lets you set up a connection to your Vault instance once
|
|||||||
path "auth/+/role/*" {
|
path "auth/+/role/*" {
|
||||||
capabilities = ["read"]
|
capabilities = ["read"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Kubernetes secrets engine - for reading secrets engine configuration and roles
|
||||||
|
path "+/config" {
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "+/roles" {
|
||||||
|
capabilities = ["list"]
|
||||||
|
}
|
||||||
|
|
||||||
|
path "+/roles/*" {
|
||||||
|
capabilities = ["read"]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
</Accordion>
|
</Accordion>
|
||||||
|
|
||||||
@@ -160,6 +179,34 @@ The authentication settings (service accounts, TTL, policies, etc.) will be auto
|
|||||||
must be manually provided in the form after importing the configuration.
|
must be manually provided in the form after importing the configuration.
|
||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
|
#### Import Kubernetes Dynamic Secret Configurations
|
||||||
|
|
||||||
|
When creating a Kubernetes dynamic secret, you can import the configuration from a Vault Kubernetes secrets engine role:
|
||||||
|
|
||||||
|
1. Navigate to your project and select an environment
|
||||||
|
2. Click **"+ Add Secret"** dropdown and choose **"Dynamic Secret"**
|
||||||
|
3. Select **Kubernetes** as the provider
|
||||||
|
4. Click **"Load from Vault"** at the top of the form
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
5. Select your Vault namespace, Kubernetes secrets engine mount, and role
|
||||||
|
6. Click **"Load Configuration"**
|
||||||
|
|
||||||
|
The form will be automatically populated with the role's configuration including:
|
||||||
|
|
||||||
|
- Cluster URL and CA certificate
|
||||||
|
- Credential type (Static or Dynamic)
|
||||||
|
- Service account name or Kubernetes role settings
|
||||||
|
- Allowed namespaces
|
||||||
|
- Token TTL values
|
||||||
|
- Token audiences
|
||||||
|
|
||||||
|
<Note>
|
||||||
|
Sensitive values like cluster tokens cannot be retrieved from Vault and must
|
||||||
|
be manually provided in the form after loading the configuration.
|
||||||
|
</Note>
|
||||||
|
|
||||||
#### Import and Translate Access Control Policies
|
#### Import and Translate Access Control Policies
|
||||||
|
|
||||||
When configuring project role-based access control, you can import Vault HCL policies and automatically translate them to Infisical permissions.
|
When configuring project role-based access control, you can import Vault HCL policies and automatically translate them to Infisical permissions.
|
||||||
|
|||||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 408 KiB |
@@ -5,7 +5,8 @@ import { apiRequest } from "@app/config/request";
|
|||||||
import {
|
import {
|
||||||
ExternalMigrationProviders,
|
ExternalMigrationProviders,
|
||||||
TVaultExternalMigrationConfig,
|
TVaultExternalMigrationConfig,
|
||||||
VaultKubernetesAuthRole
|
VaultKubernetesAuthRole,
|
||||||
|
VaultKubernetesRole
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export const externalMigrationQueryKeys = {
|
export const externalMigrationQueryKeys = {
|
||||||
@@ -31,6 +32,11 @@ export const externalMigrationQueryKeys = {
|
|||||||
"vault-kubernetes-auth-roles",
|
"vault-kubernetes-auth-roles",
|
||||||
namespace,
|
namespace,
|
||||||
mountPath
|
mountPath
|
||||||
|
],
|
||||||
|
vaultKubernetesRoles: (namespace?: string, mountPath?: string) => [
|
||||||
|
"vault-kubernetes-roles",
|
||||||
|
namespace,
|
||||||
|
mountPath
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -172,3 +178,30 @@ export const useGetVaultKubernetesAuthRoles = (
|
|||||||
enabled: enabled && !!namespace && !!mountPath
|
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;
|
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,
|
TextArea,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
import { useOrganization, useSubscription } from "@app/context";
|
import { useOrganization, useOrgPermission, useSubscription } from "@app/context";
|
||||||
import {
|
import {
|
||||||
OrgGatewayPermissionActions,
|
OrgGatewayPermissionActions,
|
||||||
OrgPermissionSubjects
|
OrgPermissionSubjects
|
||||||
} from "@app/context/OrgPermissionContext/types";
|
} from "@app/context/OrgPermissionContext/types";
|
||||||
|
import { OrgMembershipRole } from "@app/helpers/roles";
|
||||||
import {
|
import {
|
||||||
gatewaysQueryKeys,
|
gatewaysQueryKeys,
|
||||||
useAddIdentityKubernetesAuth,
|
useAddIdentityKubernetesAuth,
|
||||||
@@ -129,6 +130,8 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
] as const);
|
] as const);
|
||||||
const { data: vaultConfigs = [] } = useGetVaultExternalMigrationConfigs();
|
const { data: vaultConfigs = [] } = useGetVaultExternalMigrationConfigs();
|
||||||
const hasVaultConnection = vaultConfigs.some((config) => config.connectionId);
|
const hasVaultConnection = vaultConfigs.some((config) => config.connectionId);
|
||||||
|
const { hasOrgRole } = useOrgPermission();
|
||||||
|
const isOrgAdmin = hasOrgRole(OrgMembershipRole.Admin);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
@@ -409,20 +412,29 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
<FontAwesomeIcon icon={faInfoCircle} className="mt-0.5 text-primary" />
|
<FontAwesomeIcon icon={faInfoCircle} className="mt-0.5 text-primary" />
|
||||||
<span className="text-mineshaft-200">Load values from HashiCorp Vault</span>
|
<span className="text-mineshaft-200">Load values from HashiCorp Vault</span>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Tooltip
|
||||||
variant="outline_bg"
|
content={
|
||||||
size="xs"
|
!isOrgAdmin
|
||||||
leftIcon={
|
? "Only organization admins can import configurations from HashiCorp Vault"
|
||||||
<img
|
: undefined
|
||||||
src="/images/integrations/Vault.png"
|
|
||||||
alt="HashiCorp Vault"
|
|
||||||
className="h-4 w-4"
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
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>
|
||||||
)}
|
)}
|
||||||
<div className="flex w-full items-center gap-2">
|
<div className="flex w-full items-center gap-2">
|
||||||
|
|||||||
+29
-17
@@ -55,6 +55,7 @@ import {
|
|||||||
ProjectPermissionActions,
|
ProjectPermissionActions,
|
||||||
ProjectPermissionDynamicSecretActions,
|
ProjectPermissionDynamicSecretActions,
|
||||||
ProjectPermissionSub,
|
ProjectPermissionSub,
|
||||||
|
useOrgPermission,
|
||||||
useProject,
|
useProject,
|
||||||
useProjectPermission,
|
useProjectPermission,
|
||||||
useSubscription
|
useSubscription
|
||||||
@@ -64,6 +65,7 @@ import {
|
|||||||
ProjectPermissionSecretActions,
|
ProjectPermissionSecretActions,
|
||||||
ProjectPermissionSecretRotationActions
|
ProjectPermissionSecretRotationActions
|
||||||
} from "@app/context/ProjectPermissionContext/types";
|
} from "@app/context/ProjectPermissionContext/types";
|
||||||
|
import { OrgMembershipRole } from "@app/helpers/roles";
|
||||||
import { usePopUp } from "@app/hooks";
|
import { usePopUp } from "@app/hooks";
|
||||||
import {
|
import {
|
||||||
useCreateFolder,
|
useCreateFolder,
|
||||||
@@ -203,6 +205,8 @@ export const ActionBar = ({
|
|||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
const { data: vaultConfigs = [] } = useGetVaultExternalMigrationConfigs();
|
const { data: vaultConfigs = [] } = useGetVaultExternalMigrationConfigs();
|
||||||
const hasVaultConnection = vaultConfigs.some((config) => config.connectionId);
|
const hasVaultConnection = vaultConfigs.some((config) => config.connectionId);
|
||||||
|
const { hasOrgRole } = useOrgPermission();
|
||||||
|
const isOrgAdmin = hasOrgRole(OrgMembershipRole.Admin);
|
||||||
|
|
||||||
const handleFolderCreate = async (folderName: string, description: string | null) => {
|
const handleFolderCreate = async (folderName: string, description: string | null) => {
|
||||||
try {
|
try {
|
||||||
@@ -1114,25 +1118,33 @@ export const ActionBar = ({
|
|||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{(isAllowed) => (
|
{(isAllowed) => (
|
||||||
<Button
|
<Tooltip
|
||||||
leftIcon={
|
content={
|
||||||
<img
|
!isOrgAdmin
|
||||||
src="/images/integrations/Vault.png"
|
? "Only organization admins can import secrets from HashiCorp Vault"
|
||||||
alt="HashiCorp Vault"
|
: undefined
|
||||||
className="h-4 w-4"
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
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>
|
</ProjectPermissionCan>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+122
-4
@@ -1,5 +1,6 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { Controller, FieldValues, useFieldArray, useForm } from "react-hook-form";
|
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 { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
@@ -21,16 +22,22 @@ import {
|
|||||||
TextArea,
|
TextArea,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from "@app/components/v2";
|
} from "@app/components/v2";
|
||||||
|
import { useOrgPermission } from "@app/context";
|
||||||
import { OrgPermissionSubjects } from "@app/context/OrgPermissionContext";
|
import { OrgPermissionSubjects } from "@app/context/OrgPermissionContext";
|
||||||
import { OrgGatewayPermissionActions } from "@app/context/OrgPermissionContext/types";
|
import { OrgGatewayPermissionActions } from "@app/context/OrgPermissionContext/types";
|
||||||
|
import { OrgMembershipRole } from "@app/helpers/roles";
|
||||||
import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
|
import { gatewaysQueryKeys, useCreateDynamicSecret } from "@app/hooks/api";
|
||||||
import {
|
import {
|
||||||
DynamicSecretProviders,
|
DynamicSecretProviders,
|
||||||
KubernetesDynamicSecretCredentialType
|
KubernetesDynamicSecretCredentialType
|
||||||
} from "@app/hooks/api/dynamicSecret/types";
|
} 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 { ProjectEnv } from "@app/hooks/api/types";
|
||||||
import { slugSchema } from "@app/lib/schemas";
|
import { slugSchema } from "@app/lib/schemas";
|
||||||
|
|
||||||
|
import { VaultKubernetesImportModal } from "./VaultKubernetesImportModal";
|
||||||
|
|
||||||
enum RoleType {
|
enum RoleType {
|
||||||
ClusterRole = "cluster-role",
|
ClusterRole = "cluster-role",
|
||||||
Role = "role"
|
Role = "role"
|
||||||
@@ -162,11 +169,14 @@ export const KubernetesInputForm = ({
|
|||||||
environments,
|
environments,
|
||||||
isSingleEnvironmentMode
|
isSingleEnvironmentMode
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
|
const [isVaultImportModalOpen, setIsVaultImportModalOpen] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
formState: { isSubmitting },
|
formState: { isSubmitting },
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
watch
|
watch,
|
||||||
|
setValue
|
||||||
} = useForm<TForm>({
|
} = useForm<TForm>({
|
||||||
resolver: zodResolver(formSchema),
|
resolver: zodResolver(formSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -193,11 +203,81 @@ export const KubernetesInputForm = ({
|
|||||||
|
|
||||||
const createDynamicSecret = useCreateDynamicSecret();
|
const createDynamicSecret = useCreateDynamicSecret();
|
||||||
const { data: gateways, isPending: isGatewaysLoading } = useQuery(gatewaysQueryKeys.list());
|
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 sslEnabled = watch("provider.sslEnabled");
|
||||||
const credentialType = watch("provider.credentialType");
|
const credentialType = watch("provider.credentialType");
|
||||||
const authMethod = watch("provider.authMethod");
|
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) {
|
||||||
|
setValue("provider.audiences", 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 handleCreateDynamicSecret = async (formData: TForm) => {
|
||||||
const { provider, usernameTemplate, ...rest } = formData;
|
const { provider, usernameTemplate, ...rest } = formData;
|
||||||
// wait till previous request is finished
|
// wait till previous request is finished
|
||||||
@@ -235,6 +315,39 @@ export const KubernetesInputForm = ({
|
|||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
|
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
|
||||||
<div>
|
<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="flex items-center space-x-2">
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<Controller
|
<Controller
|
||||||
@@ -286,8 +399,8 @@ export const KubernetesInputForm = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="mt-4 mb-4 border-b border-mineshaft-500 pb-2 pl-1 font-medium text-mineshaft-200">
|
<div className="mt-4 mb-4 border-b border-mineshaft-500 pb-2 pl-1">
|
||||||
Configuration
|
<h3 className="font-medium text-mineshaft-200">Configuration</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
@@ -654,6 +767,11 @@ export const KubernetesInputForm = ({
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<VaultKubernetesImportModal
|
||||||
|
isOpen={isVaultImportModalOpen}
|
||||||
|
onOpenChange={setIsVaultImportModalOpen}
|
||||||
|
onImport={handleVaultImport}
|
||||||
|
/>
|
||||||
</form>
|
</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