Skip to content

Commit

Permalink
feat: allow creation of multiple project envs
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielHougaard committed Sep 30, 2024
1 parent efa54e0 commit 8c1f7fa
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 49 deletions.
7 changes: 7 additions & 0 deletions backend/src/keystore/keystore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ export const KeyStorePrefixes = {
WaitUntilReadyKmsOrgKeyCreation: "wait-until-ready-kms-org-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) =>
`sync-integration-mutex-${projectId}-${environmentSlug}-${secretPath}` as const,
SyncSecretIntegrationLastRunTimestamp: (projectId: string, environmentSlug: string, secretPath: string) =>
Expand Down
1 change: 1 addition & 0 deletions backend/src/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,7 @@ export const registerRoutes = async (
const projectEnvService = projectEnvServiceFactory({
permissionService,
projectEnvDAL,
keyStore,
licenseService,
projectDAL,
folderDAL
Expand Down
174 changes: 125 additions & 49 deletions backend/src/services/project-env/project-env-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import { ForbiddenError } from "@casl/ability";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
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 { logger } from "@app/lib/logger";

import { TProjectDALFactory } from "../project/project-dal";
import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal";
Expand All @@ -16,6 +18,7 @@ type TProjectEnvServiceFactoryDep = {
projectDAL: Pick<TProjectDALFactory, "findById">;
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "setItemWithExpiry" | "getItem" | "waitTillReady">;
};

export type TProjectEnvServiceFactory = ReturnType<typeof projectEnvServiceFactory>;
Expand All @@ -24,6 +27,7 @@ export const projectEnvServiceFactory = ({
projectEnvDAL,
permissionService,
licenseService,
keyStore,
projectDAL,
folderDAL
}: TProjectEnvServiceFactoryDep) => {
Expand All @@ -45,32 +49,56 @@ export const projectEnvServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Create, ProjectPermissionSub.Environments);

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: "Create envv"
});
const lock = await keyStore
.acquireLock([KeyStorePrefixes.ProjectEnvironmentCreation, projectId], 5000)
.catch(() => null);

try {
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 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 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;
await keyStore.setItemWithExpiry(
`${KeyStorePrefixes.WaitUntilReadyCreateProjectEnvironment}${projectId}`,
10,
"true"
);

return env;
} finally {
await lock?.release();
}
};

const updateEnvironment = async ({
Expand All @@ -93,26 +121,50 @@ export const projectEnvServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Edit, ProjectPermissionSub.Environments);

const oldEnv = await projectEnvDAL.findOne({ id, projectId });
if (!oldEnv) throw new BadRequestError({ message: "Environment not found" });

if (slug) {
const existingEnv = await projectEnvDAL.findOne({ slug, projectId });
if (existingEnv && existingEnv.id !== id) {
throw new BadRequestError({
message: "Environment with slug already exist",
name: "Create envv"
const lock = await keyStore
.acquireLock([KeyStorePrefixes.ProjectEnvironmentUpdate, projectId], 5000)
.catch(() => null);

try {
if (!lock) {
await keyStore.waitTillReady({
key: `${KeyStorePrefixes.WaitUntilReadyUpdateProjectEnvironment}${projectId}`,
keyCheckCb: (val) => val === "true",
waitingCb: () => logger.debug("Update project environment. Waiting for project environment update"),
delay: 500
});
}
}

const env = await projectEnvDAL.transaction(async (tx) => {
if (position) {
await projectEnvDAL.updateAllPosition(projectId, oldEnv.position, position, tx);
const oldEnv = await projectEnvDAL.findOne({ id, projectId });
if (!oldEnv) throw new NotFoundError({ message: "Environment not found", name: "UpdateEnvironment" });

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);
});
return { environment: env, old: oldEnv };

const env = await projectEnvDAL.transaction(async (tx) => {
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) => {
Expand All @@ -125,18 +177,42 @@ export const projectEnvServiceFactory = ({
);
ForbiddenError.from(permission).throwUnlessCan(ProjectPermissionActions.Delete, ProjectPermissionSub.Environments);

const env = await projectEnvDAL.transaction(async (tx) => {
const [doc] = await projectEnvDAL.delete({ id, projectId }, tx);
if (!doc)
throw new BadRequestError({
message: "Env doesn't exist",
name: "Re-order env"
const lock = await keyStore
.acquireLock([KeyStorePrefixes.ProjectEnvironmentDelete, projectId], 5000)
.catch(() => null);

try {
if (!lock) {
await keyStore.waitTillReady({
key: `${KeyStorePrefixes.WaitUntilReadyDeleteProjectEnvironment}${projectId}`,
keyCheckCb: (val) => val === "true",
waitingCb: () => logger.debug("Delete project environment. Waiting for "),
delay: 500
});
}

await projectEnvDAL.updateAllPosition(projectId, doc.position, -1, tx);
return doc;
});
return env;
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) => {
Expand Down

0 comments on commit 8c1f7fa

Please sign in to comment.