mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: allow creation of multiple project envs
This commit is contained in:
@@ -16,6 +16,13 @@ export const KeyStorePrefixes = {
|
|||||||
WaitUntilReadyKmsOrgKeyCreation: "wait-until-ready-kms-org-key-creation-",
|
WaitUntilReadyKmsOrgKeyCreation: "wait-until-ready-kms-org-key-creation-",
|
||||||
WaitUntilReadyKmsOrgDataKeyCreation: "wait-until-ready-kms-org-data-key-creation-",
|
WaitUntilReadyKmsOrgDataKeyCreation: "wait-until-ready-kms-org-data-key-creation-",
|
||||||
|
|
||||||
|
ProjectEnvironmentCreation: "project-environment-creation-lock",
|
||||||
|
ProjectEnvironmentUpdate: "project-environment-update-lock",
|
||||||
|
ProjectEnvironmentDelete: "project-environment-delete-lock",
|
||||||
|
WaitUntilReadyCreateProjectEnvironment: "wait-until-ready-create-project-environments-",
|
||||||
|
WaitUntilReadyUpdateProjectEnvironment: "wait-until-ready-update-project-environments-",
|
||||||
|
WaitUntilReadyDeleteProjectEnvironment: "wait-until-ready-delete-project-environments-",
|
||||||
|
|
||||||
SyncSecretIntegrationLock: (projectId: string, environmentSlug: string, secretPath: string) =>
|
SyncSecretIntegrationLock: (projectId: string, environmentSlug: string, secretPath: string) =>
|
||||||
`sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const,
|
`sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const,
|
||||||
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
|
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
|
||||||
|
|||||||
@@ -748,6 +748,7 @@ export const registerRoutes = async (
|
|||||||
const projectEnvService = projectEnvServiceFactory({
|
const projectEnvService = projectEnvServiceFactory({
|
||||||
permissionService,
|
permissionService,
|
||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
|
keyStore,
|
||||||
licenseService,
|
licenseService,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
folderDAL
|
folderDAL
|
||||||
|
|||||||
@@ -3,7 +3,9 @@ import { ForbiddenError } from "@casl/ability";
|
|||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||||
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
||||||
|
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||||
|
import { logger } from "@app/lib/logger";
|
||||||
|
|
||||||
import { TProjectDALFactory } from "../project/project-dal";
|
import { TProjectDALFactory } from "../project/project-dal";
|
||||||
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
|
||||||
@@ -16,6 +18,7 @@ type TProjectEnvServiceFactoryDep = {
|
|||||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
|
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem" | "waitTillReady">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TProjectEnvServiceFactory = ReturnType<typeof projectEnvServiceFactory>;
|
export type TProjectEnvServiceFactory = ReturnType<typeof projectEnvServiceFactory>;
|
||||||
@@ -24,6 +27,7 @@ export const projectEnvServiceFactory = ({
|
|||||||
projectEnvDAL,
|
projectEnvDAL,
|
||||||
permissionService,
|
permissionService,
|
||||||
licenseService,
|
licenseService,
|
||||||
|
keyStore,
|
||||||
projectDAL,
|
projectDAL,
|
||||||
folderDAL
|
folderDAL
|
||||||
}: TProjectEnvServiceFactoryDep) => {
|
}: TProjectEnvServiceFactoryDep) => {
|
||||||
@@ -45,32 +49,56 @@ export const projectEnvServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);
|
||||||
|
|
||||||
const envs = await projectEnvDAL.find({ projectId });
|
const lock = await keyStore
|
||||||
const existingEnv = envs.find(({ slug: envSlug }) => envSlug === slug);
|
.acquireLock([KeyStorePrefixes.ProjectEnvironmentCreation, projectId], 5000)
|
||||||
if (existingEnv)
|
.catch(() => null);
|
||||||
throw new BadRequestError({
|
|
||||||
message: "Environment with slug already exist",
|
try {
|
||||||
name: "CreateEnvironment"
|
if (!lock) {
|
||||||
|
await keyStore.waitTillReady({
|
||||||
|
key: `${KeyStorePrefixes.WaitUntilReadyCreateProjectEnvironment}${projectId}`,
|
||||||
|
keyCheckCb: (val) => val === "true",
|
||||||
|
waitingCb: () => logger.debug("Create project environment. Waiting for "),
|
||||||
|
delay: 500
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const envs = await projectEnvDAL.find({ projectId });
|
||||||
|
const existingEnv = envs.find(({ slug: envSlug }) => envSlug === slug);
|
||||||
|
if (existingEnv)
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Environment with slug already exist",
|
||||||
|
name: "CreateEnvironment"
|
||||||
|
});
|
||||||
|
|
||||||
|
const project = await projectDAL.findById(projectId);
|
||||||
|
const plan = await licenseService.getPlan(project.orgId);
|
||||||
|
if (plan.environmentLimit !== null && envs.length >= plan.environmentLimit) {
|
||||||
|
// case: limit imposed on number of environments allowed
|
||||||
|
// case: number of environments used exceeds the number of environments allowed
|
||||||
|
throw new BadRequestError({
|
||||||
|
message:
|
||||||
|
"Failed to create environment due to environment limit reached. Upgrade plan to create more environments."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const env = await projectEnvDAL.transaction(async (tx) => {
|
||||||
|
const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx);
|
||||||
|
const doc = await projectEnvDAL.create({ slug, name, projectId, position: lastPos + 1 }, tx);
|
||||||
|
await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx);
|
||||||
|
return doc;
|
||||||
});
|
});
|
||||||
|
|
||||||
const project = await projectDAL.findById(projectId);
|
await keyStore.setItemWithExpiry(
|
||||||
const plan = await licenseService.getPlan(project.orgId);
|
`${KeyStorePrefixes.WaitUntilReadyCreateProjectEnvironment}${projectId}`,
|
||||||
if (plan.environmentLimit !== null && envs.length >= plan.environmentLimit) {
|
10,
|
||||||
// case: limit imposed on number of environments allowed
|
"true"
|
||||||
// case: number of environments used exceeds the number of environments allowed
|
);
|
||||||
throw new BadRequestError({
|
|
||||||
message:
|
return env;
|
||||||
"Failed to create environment due to environment limit reached. Upgrade plan to create more environments."
|
} finally {
|
||||||
});
|
await lock?.release();
|
||||||
}
|
}
|
||||||
|
|
||||||
const env = await projectEnvDAL.transaction(async (tx) => {
|
|
||||||
const lastPos = await projectEnvDAL.findLastEnvPosition(projectId, tx);
|
|
||||||
const doc = await projectEnvDAL.create({ slug, name, projectId, position: lastPos + 1 }, tx);
|
|
||||||
await folderDAL.create({ name: "root", parentId: null, envId: doc.id, version: 1 }, tx);
|
|
||||||
return doc;
|
|
||||||
});
|
|
||||||
return env;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateEnvironment = async ({
|
const updateEnvironment = async ({
|
||||||
@@ -93,26 +121,50 @@ export const projectEnvServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);
|
||||||
|
|
||||||
const oldEnv = await projectEnvDAL.findOne({ id, projectId });
|
const lock = await keyStore
|
||||||
if (!oldEnv) throw new NotFoundError({ message: "Environment not found" });
|
.acquireLock([KeyStorePrefixes.ProjectEnvironmentUpdate, projectId], 5000)
|
||||||
|
.catch(() => null);
|
||||||
|
|
||||||
if (slug) {
|
try {
|
||||||
const existingEnv = await projectEnvDAL.findOne({ slug, projectId });
|
if (!lock) {
|
||||||
if (existingEnv && existingEnv.id !== id) {
|
await keyStore.waitTillReady({
|
||||||
throw new BadRequestError({
|
key: `${KeyStorePrefixes.WaitUntilReadyUpdateProjectEnvironment}${projectId}`,
|
||||||
message: "Environment with slug already exist",
|
keyCheckCb: (val) => val === "true",
|
||||||
name: "UpdateEnvironment"
|
waitingCb: () => logger.debug("Update project environment. Waiting for project environment update"),
|
||||||
|
delay: 500
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const env = await projectEnvDAL.transaction(async (tx) => {
|
const oldEnv = await projectEnvDAL.findOne({ id, projectId });
|
||||||
if (position) {
|
if (!oldEnv) throw new NotFoundError({ message: "Environment not found", name: "UpdateEnvironment" });
|
||||||
await projectEnvDAL.updateAllPosition(projectId, oldEnv.position, position, tx);
|
|
||||||
|
if (slug) {
|
||||||
|
const existingEnv = await projectEnvDAL.findOne({ slug, projectId });
|
||||||
|
if (existingEnv && existingEnv.id !== id) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Environment with slug already exist",
|
||||||
|
name: "UpdateEnvironment"
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return projectEnvDAL.updateById(oldEnv.id, { name, slug, position }, tx);
|
|
||||||
});
|
const env = await projectEnvDAL.transaction(async (tx) => {
|
||||||
return { environment: env, old: oldEnv };
|
if (position) {
|
||||||
|
await projectEnvDAL.updateAllPosition(projectId, oldEnv.position, position, tx);
|
||||||
|
}
|
||||||
|
return projectEnvDAL.updateById(oldEnv.id, { name, slug, position }, tx);
|
||||||
|
});
|
||||||
|
|
||||||
|
await keyStore.setItemWithExpiry(
|
||||||
|
`${KeyStorePrefixes.WaitUntilReadyUpdateProjectEnvironment}${projectId}`,
|
||||||
|
10,
|
||||||
|
"true"
|
||||||
|
);
|
||||||
|
|
||||||
|
return { environment: env, old: oldEnv };
|
||||||
|
} finally {
|
||||||
|
await lock?.release();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteEnvironment = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteEnvDTO) => {
|
const deleteEnvironment = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteEnvDTO) => {
|
||||||
@@ -125,18 +177,42 @@ export const projectEnvServiceFactory = ({
|
|||||||
);
|
);
|
||||||
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
|
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);
|
||||||
|
|
||||||
const env = await projectEnvDAL.transaction(async (tx) => {
|
const lock = await keyStore
|
||||||
const [doc] = await projectEnvDAL.delete({ id, projectId }, tx);
|
.acquireLock([KeyStorePrefixes.ProjectEnvironmentDelete, projectId], 5000)
|
||||||
if (!doc)
|
.catch(() => null);
|
||||||
throw new NotFoundError({
|
|
||||||
message: "Env doesn't exist",
|
|
||||||
name: "DeleteEnvironment"
|
|
||||||
});
|
|
||||||
|
|
||||||
await projectEnvDAL.updateAllPosition(projectId, doc.position, -1, tx);
|
try {
|
||||||
return doc;
|
if (!lock) {
|
||||||
});
|
await keyStore.waitTillReady({
|
||||||
return env;
|
key: `${KeyStorePrefixes.WaitUntilReadyDeleteProjectEnvironment}${projectId}`,
|
||||||
|
keyCheckCb: (val) => val === "true",
|
||||||
|
waitingCb: () => logger.debug("Delete project environment. Waiting for "),
|
||||||
|
delay: 500
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const env = await projectEnvDAL.transaction(async (tx) => {
|
||||||
|
const [doc] = await projectEnvDAL.delete({ id, projectId }, tx);
|
||||||
|
if (!doc)
|
||||||
|
throw new NotFoundError({
|
||||||
|
message: "Environment doesn't exist",
|
||||||
|
name: "DeleteEnvironment"
|
||||||
|
});
|
||||||
|
|
||||||
|
await projectEnvDAL.updateAllPosition(projectId, doc.position, -1, tx);
|
||||||
|
return doc;
|
||||||
|
});
|
||||||
|
|
||||||
|
await keyStore.setItemWithExpiry(
|
||||||
|
`${KeyStorePrefixes.WaitUntilReadyDeleteProjectEnvironment}${projectId}`,
|
||||||
|
10,
|
||||||
|
"true"
|
||||||
|
);
|
||||||
|
|
||||||
|
return env;
|
||||||
|
} finally {
|
||||||
|
await lock?.release();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEnvironmentById = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TGetEnvDTO) => {
|
const getEnvironmentById = async ({ projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TGetEnvDTO) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user