Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
Warnings:

- A unique constraint covering the columns `[orgId,name]` on the table `AppGroup` will be added. If there are existing duplicate values, this will fail.

*/
-- CreateIndex
CREATE UNIQUE INDEX "AppGroup_orgId_name_key" ON "AppGroup"("orgId", "name");
2 changes: 2 additions & 0 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ model AppGroup {
// Is monolith app and should be displayed to the user as a single app not part of a group.
isMono Boolean @default(false)
apps App[]

@@unique([orgId, name])
}

model Deployment {
Expand Down
1 change: 1 addition & 0 deletions backend/src/db/models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,7 @@ export interface AppCreate {
namespace: string;
clusterUsername: string;
projectId: string;
configId: number;
}

export interface RepoImportState {
Expand Down
1 change: 1 addition & 0 deletions backend/src/db/repo/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export class AppRepo {
id: appData.orgId,
},
},
config: { connect: { id: appData.configId } },

// This cluster username will be used to automatically update the app after a build job or webhook payload
// TODO: make this a setting in the UI
Expand Down
55 changes: 33 additions & 22 deletions backend/src/db/repo/deployment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,37 @@ export class DeploymentRepo {
return deployment;
}

async createConfig(config: WorkloadConfigCreate | HelmConfigCreate) {
const configClone = structuredClone(config);
const appType = configClone.appType;
if (appType === "workload") {
delete configClone.appType;
} else if (appType === "helm") {
delete configClone.appType;
delete configClone.source;
}

const cfg = await this.client.deploymentConfig.create({
data: {
appType: appType,
...(appType === "workload"
? {
workloadConfig: {
create: this.encryptEnv(configClone),
},
}
: {
helmConfig: {
create: configClone,
},
}),
},
select: { id: true },
});

return cfg.id;
}

async create({
appId,
config,
Expand All @@ -94,32 +125,12 @@ export class DeploymentRepo {
workflowRunId?: number;
status?: DeploymentStatus;
}): Promise<Deployment> {
const configClone = structuredClone(config);
const appType = configClone.appType;
if (appType === "workload") {
delete configClone.appType;
} else if (appType === "helm") {
delete configClone.appType;
delete configClone.source;
}
const configId = await this.createConfig(config);
return await this.client.deployment.create({
data: {
app: { connect: { id: appId } },
config: {
create: {
appType: appType,
...(appType === "workload"
? {
workloadConfig: {
create: this.encryptEnv(configClone),
},
}
: {
helmConfig: {
create: configClone,
},
}),
},
connect: { id: configId },
},
commitMessage,
workflowRunId,
Expand Down
30 changes: 21 additions & 9 deletions backend/src/handlers/createApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ export const createAppHandler: HandlerMap["createApp"] = async (
req: AuthenticatedRequest,
res,
) => {
let appId: number;
let createFirstDeployment: () => Promise<void>;
try {
const appId = await createAppService.createApp(
const res = await createAppService.createApp(
ctx.request.requestBody,
req.user.id,
);
return json(200, res, { id: appId });
appId = res.appId;
createFirstDeployment = res.createFirstDeployment;
} catch (e) {
if (e instanceof OrgNotFoundError) {
return json(400, res, { code: 400, message: "Organization not found" });
Expand All @@ -27,13 +30,6 @@ export const createAppHandler: HandlerMap["createApp"] = async (
code: 400,
message: e.message,
});
} else if (e instanceof DeploymentError) {
// The app was created, but a Deployment couldn't be created
logger.error(e, "Failed to create app's first deloyment");
return json(500, res, {
code: 500,
message: "Failed to create a deployment for your app.",
});
} else {
logger.error(e, "Failed to create app");
return json(500, res, {
Expand All @@ -42,4 +38,20 @@ export const createAppHandler: HandlerMap["createApp"] = async (
});
}
}

// Always respond with 200 OK after this since an app was created,
// although the deployment may fail for other reasons

try {
await createFirstDeployment();
} catch (e) {
if (e instanceof DeploymentError) {
// The app was created, but a Deployment couldn't be created
logger.error(e, "Failed to create app's first deployment");
} else {
logger.error(e, "Failed to create app");
}
}

return json(200, res, { id: appId });
};
2 changes: 1 addition & 1 deletion backend/src/service/common/deploymentConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export class DeploymentConfigService {
*/
populateImageTag(
config: GitConfigCreate | HelmConfigCreate | WorkloadConfigCreate,
app: App,
app: Pick<App, "imageRepo">,
) {
if (config.source === "GIT") {
return {
Expand Down
76 changes: 53 additions & 23 deletions backend/src/service/createApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ConflictError } from "../db/errors/index.ts";
import type { App } from "../db/models.ts";
import type { AppRepo } from "../db/repo/app.ts";
import type { AppGroupRepo } from "../db/repo/appGroup.ts";
import type { DeploymentRepo } from "../db/repo/deployment.ts";
import type { OrganizationRepo } from "../db/repo/organization.ts";
import type { UserRepo } from "../db/repo/user.ts";
import type { components } from "../generated/openapi.ts";
Expand All @@ -28,6 +29,7 @@ export class CreateAppService {
private appRepo: AppRepo;
private appGroupRepo: AppGroupRepo;
private userRepo: UserRepo;
private deploymentRepo: DeploymentRepo;
private appService: AppService;
private deploymentService: DeploymentService;
private deploymentConfigService: DeploymentConfigService;
Expand All @@ -37,6 +39,7 @@ export class CreateAppService {
appRepo: AppRepo,
appGroupRepo: AppGroupRepo,
userRepo: UserRepo,
deploymentRepo: DeploymentRepo,
appService: AppService,
deploymentService: DeploymentService,
deploymentConfigService: DeploymentConfigService,
Expand All @@ -45,6 +48,7 @@ export class CreateAppService {
this.appRepo = appRepo;
this.appGroupRepo = appGroupRepo;
this.userRepo = userRepo;
this.deploymentRepo = deploymentRepo;
this.appService = appService;
this.deploymentService = deploymentService;
this.deploymentConfigService = deploymentConfigService;
Expand Down Expand Up @@ -83,22 +87,42 @@ export class CreateAppService {

case "create-new": {
this.appService.validateAppGroupName(appData.appGroup.name);
appGroupId = await this.appGroupRepo.create(
appData.orgId,
appData.appGroup.name,
false,
);
try {
appGroupId = await this.appGroupRepo.create(
appData.orgId,
appData.appGroup.name,
false,
);
} catch (e) {
if (e instanceof ConflictError) {
throw new ValidationError(
"An app group already exists with that name.",
{ cause: e },
);
}
throw e;
}
break;
}

case "standalone": {
const groupName = `${appData.name.substring(0, MAX_GROUPNAME_LEN - RANDOM_TAG_LEN - 1)}-${getRandomTag()}`;
this.appService.validateAppGroupName(groupName);
appGroupId = await this.appGroupRepo.create(
appData.orgId,
groupName,
true,
);
try {
appGroupId = await this.appGroupRepo.create(
appData.orgId,
groupName,
true,
);
} catch (e) {
if (e instanceof ConflictError) {
throw new ValidationError(
"An app group already exists with that name.",
{ cause: e },
);
}
throw e;
}
break;
}

Expand All @@ -110,13 +134,15 @@ export class CreateAppService {
let deploymentConfig = config;

try {
const configId = await this.deploymentRepo.createConfig(deploymentConfig);
app = await this.appRepo.create({
orgId: appData.orgId,
appGroupId: appGroupId,
name: appData.name,
clusterUsername: user.clusterUsername,
projectId: appData.projectId,
namespace: appData.namespace,
configId,
});

logger.info({ orgId: appData.orgId, appId: app.id }, "App created");
Expand All @@ -133,18 +159,22 @@ export class CreateAppService {
throw err;
}

try {
await this.deploymentService.create({
appId: app.id,
commitMessage,
config: deploymentConfig,
});
} catch (err) {
const span = trace.getActiveSpan();
span?.recordException(err as Error);
span?.setStatus({ code: SpanStatusCode.ERROR });
throw new DeploymentError(err as Error);
}
return app.id;
return {
appId: app.id,
createFirstDeployment: async () => {
try {
await this.deploymentService.create({
appId: app.id,
commitMessage,
config: deploymentConfig,
});
} catch (err) {
const span = trace.getActiveSpan();
span?.recordException(err as Error);
span?.setStatus({ code: SpanStatusCode.ERROR });
throw new DeploymentError(err as Error);
}
},
};
}
}
29 changes: 17 additions & 12 deletions backend/src/service/createAppGroup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ConflictError } from "../db/errors/index.ts";
import type { App } from "../db/models.ts";
import type { AppRepo } from "../db/repo/app.ts";
import type { AppGroupRepo } from "../db/repo/appGroup.ts";
import type { DeploymentRepo } from "../db/repo/deployment.ts";
import type { OrganizationRepo } from "../db/repo/organization.ts";
import type { UserRepo } from "../db/repo/user.ts";
import type { components } from "../generated/openapi.ts";
Expand All @@ -24,6 +25,7 @@ export class CreateAppGroupService {
private appRepo: AppRepo;
private appGroupRepo: AppGroupRepo;
private userRepo: UserRepo;
private deploymentRepo: DeploymentRepo;
private appService: AppService;
private deploymentService: DeploymentService;
private deploymentConfigService: DeploymentConfigService;
Expand All @@ -33,6 +35,7 @@ export class CreateAppGroupService {
appRepo: AppRepo,
appGroupRepo: AppGroupRepo,
userRepo: UserRepo,
deploymentRepo: DeploymentRepo,
appService: AppService,
deploymentService: DeploymentService,
deploymentConfigService: DeploymentConfigService,
Expand All @@ -41,6 +44,7 @@ export class CreateAppGroupService {
this.appRepo = appRepo;
this.appGroupRepo = appGroupRepo;
this.userRepo = userRepo;
this.deploymentRepo = deploymentRepo;
this.appService = appService;
this.deploymentService = deploymentService;
this.deploymentConfigService = deploymentConfigService;
Expand Down Expand Up @@ -85,31 +89,32 @@ export class CreateAppGroupService {
metadata: validationResults[idx],
}));

const groupId = await this.appGroupRepo.create(orgId, groupName, false);
// let groupId: number;
// try {
// groupId = await this.appGroupRepo.create(orgId, groupName, false);
// } catch (e) {
// if (e instanceof ConflictError) {
// throw new ValidationError(
// "An app group already exists with the same name.",
// );
// }
// throw e;
// }
let groupId: number;
try {
groupId = await this.appGroupRepo.create(orgId, groupName, false);
} catch (e) {
if (e instanceof ConflictError) {
throw new ValidationError(
"An app group already exists with the same name.",
);
}
throw e;
}

for (const { appData, metadata } of appsWithMetadata) {
const { config: _config, commitMessage } = metadata;
let config = _config;
let app: App;
try {
const initialConfigId = await this.deploymentRepo.createConfig(config);
app = await this.appRepo.create({
orgId: appData.orgId,
appGroupId: groupId,
name: appData.name,
clusterUsername: user.clusterUsername,
projectId: appData.projectId,
namespace: appData.namespace,
configId: initialConfigId,
});
config = this.deploymentConfigService.populateImageTag(config, app);
} catch (err) {
Expand Down
Loading
Loading