feat: integrate vault provider management and environment variable resolution

- Added support for managing various vault providers (HashiCorp, AWS, Azure, Doppler, Infisical) in the dashboard.
- Implemented environment variable resolution using vault references, allowing seamless integration of secrets into application environments.
- Enhanced UI components to display and manage vault providers effectively.
- Introduced tests for vault reference resolution and environment variable preparation.
- Updated related components to utilize new vault management features.
This commit is contained in:
Mauricio Siu
2026-08-10 11:14:55 -06:00
parent 771d76a57c
commit b8822a1fde
44 changed files with 3577 additions and 20 deletions
+9 -6
View File
@@ -17,6 +17,7 @@ import {
} from "../utils/docker/utils";
import { execAsync, execAsyncRemote } from "../utils/process/execAsync";
import { getRemoteDocker } from "../utils/servers/remote-docker";
import { withResolvedVaultRefs } from "../utils/vault";
import { type Application, findApplicationById } from "./application";
import { findDeploymentById } from "./deployment";
import type { Environment } from "./environment";
@@ -215,7 +216,9 @@ const rollbackApplication = async (
throw new Error("Full context is required for rollback");
}
const rollbackRegistry = fullContext.rollbackRegistry ?? undefined;
const resolvedContext = await withResolvedVaultRefs(fullContext);
const rollbackRegistry = resolvedContext.rollbackRegistry ?? undefined;
// Ensure Docker daemon is authenticated with the rollback registry
// before updating the swarm service. The authconfig in CreateServiceOptions
@@ -236,7 +239,7 @@ const rollbackApplication = async (
cpuReservation,
command,
ports,
} = fullContext;
} = resolvedContext;
const resources = calculateResources({
memoryLimit,
@@ -248,7 +251,7 @@ const rollbackApplication = async (
const volumesMount = generateVolumeMounts(mounts);
const resolvedNetworks = await resolveServiceNetworks(
fullContext as Parameters<typeof resolveServiceNetworks>[0],
resolvedContext as Parameters<typeof resolveServiceNetworks>[0],
);
const {
@@ -261,14 +264,14 @@ const rollbackApplication = async (
UpdateConfig,
Ulimits,
} = generateConfigContainer(
fullContext as Parameters<typeof generateConfigContainer>[0],
resolvedContext as Parameters<typeof generateConfigContainer>[0],
);
const bindsMount = generateBindMounts(mounts);
const envVariables = prepareEnvironmentVariables(
env,
fullContext.environment.project.env,
fullContext.environment.env,
resolvedContext.environment.project.env,
resolvedContext.environment.env,
);
let rollbackImage = image;
@@ -0,0 +1,200 @@
import { db } from "@dokploy/server/db";
import {
type apiCreateVaultProvider,
type VaultProviderConfig,
vaultProvider,
} from "@dokploy/server/db/schema";
import { getVaultClient } from "@dokploy/server/utils/vault";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
import type { z } from "zod";
export type VaultProvider = typeof vaultProvider.$inferSelect;
export const VAULT_SECRET_MASK = "********";
const SENSITIVE_FIELDS: Record<VaultProviderConfig["providerType"], string[]> =
{
hashicorp: ["token"],
infisical: ["clientSecret"],
aws: ["secretAccessKey"],
doppler: ["serviceToken"],
azure: ["clientSecret"],
};
export const maskVaultProviderConfig = (
config: VaultProviderConfig,
): VaultProviderConfig => {
const masked: Record<string, unknown> = { ...config };
for (const field of SENSITIVE_FIELDS[config.providerType]) {
if (masked[field]) {
masked[field] = VAULT_SECRET_MASK;
}
}
return masked as VaultProviderConfig;
};
export const mergeVaultProviderConfig = (
incoming: VaultProviderConfig,
existing: VaultProviderConfig,
): VaultProviderConfig => {
const merged: Record<string, unknown> = { ...incoming };
for (const field of SENSITIVE_FIELDS[incoming.providerType]) {
if (merged[field] === VAULT_SECRET_MASK) {
if (incoming.providerType !== existing.providerType) {
throw new TRPCError({
code: "BAD_REQUEST",
message:
"Credentials must be re-entered when changing the provider type",
});
}
merged[field] = (existing as Record<string, unknown>)[field];
}
}
return merged as VaultProviderConfig;
};
const isUniqueNameViolation = (error: unknown) =>
error instanceof Error &&
error.message.includes("vault_provider_org_name_idx");
export const createVaultProvider = async (
input: z.infer<typeof apiCreateVaultProvider>,
organizationId: string,
) => {
try {
const newProvider = await db
.insert(vaultProvider)
.values({
name: input.name,
providerType: input.config.providerType,
config: input.config,
organizationId,
})
.returning()
.then((value) => value[0]);
if (!newProvider) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the vault provider",
});
}
return newProvider;
} catch (error) {
if (isUniqueNameViolation(error)) {
throw new TRPCError({
code: "CONFLICT",
message: `A vault provider named "${input.name}" already exists in this organization`,
});
}
throw error;
}
};
export const findVaultProviderById = async (vaultProviderId: string) => {
const provider = await db.query.vaultProvider.findFirst({
where: eq(vaultProvider.vaultProviderId, vaultProviderId),
});
if (!provider) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Vault provider not found",
});
}
return provider;
};
export const findVaultProvidersByOrganizationId = async (
organizationId: string,
) => {
return await db.query.vaultProvider.findMany({
where: eq(vaultProvider.organizationId, organizationId),
orderBy: (providers, { asc }) => [asc(providers.name)],
});
};
export const updateVaultProvider = async (
vaultProviderId: string,
name: string,
config: VaultProviderConfig,
) => {
const existing = await findVaultProviderById(vaultProviderId);
const mergedConfig = mergeVaultProviderConfig(config, existing.config);
try {
const updated = await db
.update(vaultProvider)
.set({
name,
providerType: mergedConfig.providerType,
config: mergedConfig,
})
.where(eq(vaultProvider.vaultProviderId, vaultProviderId))
.returning()
.then((res) => res[0]);
if (!updated) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error updating the vault provider",
});
}
return updated;
} catch (error) {
if (isUniqueNameViolation(error)) {
throw new TRPCError({
code: "CONFLICT",
message: `A vault provider named "${name}" already exists in this organization`,
});
}
throw error;
}
};
export const removeVaultProvider = async (vaultProviderId: string) => {
const removed = await db
.delete(vaultProvider)
.where(eq(vaultProvider.vaultProviderId, vaultProviderId))
.returning()
.then((res) => res[0]);
if (!removed) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Vault provider not found",
});
}
return removed;
};
export const testVaultProviderConnection = async (
config: VaultProviderConfig,
) => {
const client = getVaultClient(config.providerType);
try {
await client.testConnection(config);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message:
error instanceof Error
? error.message
: "Error connecting to the vault provider",
});
}
};
export const listVaultProviderSecretNames = async (
config: VaultProviderConfig,
) => {
const client = getVaultClient(config.providerType);
if (!client.listSecretNames) {
return [];
}
try {
return await client.listSecretNames(config);
} catch {
return [];
}
};