From 3878ebba83a1f22b68853f193675ef63502653f2 Mon Sep 17 00:00:00 2001 From: Mate Vago Date: Thu, 23 May 2024 14:19:35 +0200 Subject: [PATCH] feat(web): add option to disable deployment copy on version increase (#978) --- web/crux-ui/locales/en/versions.json | 1 + .../components/projects/edit-project-card.tsx | 2 +- .../projects/versions/edit-version-card.tsx | 49 ++++-- web/crux-ui/src/models/version.ts | 7 +- .../[projectId]/versions/[versionId].tsx | 1 + .../migration.sql | 2 + web/crux/prisma/schema.prisma | 21 +-- web/crux/src/app/version/version.dto.ts | 17 +- web/crux/src/app/version/version.mapper.ts | 1 + web/crux/src/app/version/version.service.ts | 136 +++++++++------- web/crux/src/domain/version-increase.ts | 149 ++++++++++++++++++ 11 files changed, 300 insertions(+), 86 deletions(-) create mode 100644 web/crux/prisma/migrations/20240522094419_version_copy_deployments/migration.sql create mode 100644 web/crux/src/domain/version-increase.ts diff --git a/web/crux-ui/locales/en/versions.json b/web/crux-ui/locales/en/versions.json index 8afc3905c6..c1086e731b 100644 --- a/web/crux-ui/locales/en/versions.json +++ b/web/crux-ui/locales/en/versions.json @@ -9,6 +9,7 @@ "type": "Type", "incremental": "Incremental", "rolling": "Rolling", + "copyDeployments": "Copy deployments while increasing", "noItems": "You haven't added a version to this project yet. Click on 'Add Version'.", "noDeployments": "You haven't added a deployment to this version. Click 'Add deployment' to browse images you can add.", diff --git a/web/crux-ui/src/components/projects/edit-project-card.tsx b/web/crux-ui/src/components/projects/edit-project-card.tsx index 3b7000c754..971e04ece8 100644 --- a/web/crux-ui/src/components/projects/edit-project-card.tsx +++ b/web/crux-ui/src/components/projects/edit-project-card.tsx @@ -111,7 +111,7 @@ const EditProjectCard = (props: EditProjectCardProps) => { value={formik.values.description} /> - {editing ? null : ( + {!editing && ( { name: '', changelog: '', type: 'incremental', - increasable: true, + autoCopyDeployments: true, audit: null, }, ) @@ -57,6 +59,7 @@ const EditVersionCard = (props: EditVersionCardProps) => { t, onSubmit: async (values, { setFieldError }) => { const body: CreateVersion | UpdateVersion = values + body.autoCopyDeployments = values.type === 'incremental' ? body.autoCopyDeployments : null const res = !editing ? await sendForm('POST', routes.project.versions(project.id).api.list(), body as CreateVersion) @@ -116,23 +119,35 @@ const EditVersionCard = (props: EditVersionCardProps) => { message={formik.errors.changelog} /> - {editing ? null : ( - <> - {t('type')} - - t(it)} - onSelectionChange={async (type): Promise => { - await formik.setFieldValue('type', type, false) - }} - qaLabel={chipsQALabelFromValue} +
+ {!editing && ( +
+ {t('type')} + + t(it)} + onSelectionChange={async (type): Promise => { + await formik.setFieldValue('type', type, false) + }} + qaLabel={chipsQALabelFromValue} + /> +
+ )} + + {formik.values.type === 'incremental' && ( + - - )} + )} +
diff --git a/web/crux-ui/src/models/version.ts b/web/crux-ui/src/models/version.ts index b01dd1980b..0116bc579d 100644 --- a/web/crux-ui/src/models/version.ts +++ b/web/crux-ui/src/models/version.ts @@ -16,17 +16,20 @@ export type Version = BasicVersion & { changelog?: string default: boolean increasable: boolean + autoCopyDeployments?: boolean audit: Audit } -export type EditableVersion = Omit +export type EditableVersion = Omit export type IncreaseVersion = { name: string changelog?: string } -export type UpdateVersion = IncreaseVersion +export type UpdateVersion = IncreaseVersion & { + autoCopyDeployments?: boolean +} export type CreateVersion = UpdateVersion & { type: VersionType diff --git a/web/crux-ui/src/pages/[teamSlug]/projects/[projectId]/versions/[versionId].tsx b/web/crux-ui/src/pages/[teamSlug]/projects/[projectId]/versions/[versionId].tsx index 561bfd3238..da21d6b6bd 100644 --- a/web/crux-ui/src/pages/[teamSlug]/projects/[projectId]/versions/[versionId].tsx +++ b/web/crux-ui/src/pages/[teamSlug]/projects/[projectId]/versions/[versionId].tsx @@ -58,6 +58,7 @@ const VersionDetailsPage = (props: VersionDetailsPageProps) => { newAllVersion[index] = { ...newVersion, default: oldVersion.default, + increasable: oldVersion.increasable, } setAllVersions(newAllVersion) } diff --git a/web/crux/prisma/migrations/20240522094419_version_copy_deployments/migration.sql b/web/crux/prisma/migrations/20240522094419_version_copy_deployments/migration.sql new file mode 100644 index 0000000000..b5e8b4c420 --- /dev/null +++ b/web/crux/prisma/migrations/20240522094419_version_copy_deployments/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Version" ADD COLUMN "autoCopyDeployments" BOOLEAN NOT NULL DEFAULT true; diff --git a/web/crux/prisma/schema.prisma b/web/crux/prisma/schema.prisma index 3aa23fa232..b5acb3c103 100644 --- a/web/crux/prisma/schema.prisma +++ b/web/crux/prisma/schema.prisma @@ -157,16 +157,17 @@ model Project { } model Version { - id String @id @default(uuid()) @db.Uuid - createdAt DateTime @default(now()) @db.Timestamptz(6) - createdBy String @db.Uuid - updatedAt DateTime @updatedAt @db.Timestamptz(6) - updatedBy String? @db.Uuid - name String @db.VarChar(70) - changelog String? - default Boolean @default(false) - type VersionTypeEnum @default(incremental) - projectId String @db.Uuid + id String @id @default(uuid()) @db.Uuid + createdAt DateTime @default(now()) @db.Timestamptz(6) + createdBy String @db.Uuid + updatedAt DateTime @updatedAt @db.Timestamptz(6) + updatedBy String? @db.Uuid + name String @db.VarChar(70) + changelog String? + default Boolean @default(false) + type VersionTypeEnum @default(incremental) + autoCopyDeployments Boolean @default(true) + projectId String @db.Uuid project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) images Image[] diff --git a/web/crux/src/app/version/version.dto.ts b/web/crux/src/app/version/version.dto.ts index 98932a0696..ee91c2c668 100644 --- a/web/crux/src/app/version/version.dto.ts +++ b/web/crux/src/app/version/version.dto.ts @@ -42,6 +42,10 @@ export class UpdateVersionDto { @IsString() @IsOptional() changelog?: string + + @IsBoolean() + @IsOptional() + autoCopyDeployments?: boolean } export class CreateVersionDto extends UpdateVersionDto { @@ -53,7 +57,14 @@ export class CreateVersionDto extends UpdateVersionDto { type: VersionTypeDto } -export class IncreaseVersionDto extends UpdateVersionDto {} +export class IncreaseVersionDto { + @IsString() + name: string + + @IsString() + @IsOptional() + changelog?: string +} export class VersionDetailsDto extends VersionDto { @IsBoolean() @@ -62,6 +73,10 @@ export class VersionDetailsDto extends VersionDto { @IsBoolean() deletable: boolean + @IsBoolean() + @IsOptional() + autoCopyDeployments?: boolean + @Type(() => ImageDto) images: ImageDto[] diff --git a/web/crux/src/app/version/version.mapper.ts b/web/crux/src/app/version/version.mapper.ts index 41a11e7501..2ad2c4712c 100644 --- a/web/crux/src/app/version/version.mapper.ts +++ b/web/crux/src/app/version/version.mapper.ts @@ -50,6 +50,7 @@ export default class VersionMapper { mutable: versionIsMutable(version), deletable: versionIsDeletable(version), increasable: versionIsIncreasable(version), + autoCopyDeployments: version.autoCopyDeployments, images: version.images.map(it => this.imageMapper.toDto(it)), deployments: version.deployments.map(it => this.deployMapper.toDeploymentWithBasicNodeDto(it, nodeStatusLookup.get(it.nodeId)), diff --git a/web/crux/src/app/version/version.service.ts b/web/crux/src/app/version/version.service.ts index 10f86907a3..f767d20c13 100644 --- a/web/crux/src/app/version/version.service.ts +++ b/web/crux/src/app/version/version.service.ts @@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common' import { Identity } from '@ory/kratos-client' import { DeploymentStatusEnum, Prisma } from '@prisma/client' import { VersionMessage } from 'src/domain/notification-templates' +import { increaseIncrementalVersion } from 'src/domain/version-increase' import DomainNotificationService from 'src/services/domain.notification.service' import PrismaService from 'src/services/prisma.service' import AgentService from '../agent/agent.service' @@ -170,6 +171,7 @@ export default class VersionService { name: req.name, changelog: req.changelog, type: req.type, + autoCopyDeployments: req.type === 'incremental' && req.autoCopyDeployments, default: !defaultVersion, createdBy: identity.id, }, @@ -281,6 +283,15 @@ export default class VersionService { } async updateVersion(versionId: string, req: UpdateVersionDto, identity: Identity): Promise { + const version = await this.prisma.version.findUniqueOrThrow({ + where: { + id: versionId, + }, + select: { + type: true, + }, + }) + await this.prisma.version.update({ where: { id: versionId, @@ -288,6 +299,7 @@ export default class VersionService { data: { name: req.name, changelog: req.changelog, + autoCopyDeployments: version.type === 'incremental' && req.autoCopyDeployments, updatedBy: identity.id, }, }) @@ -331,8 +343,9 @@ export default class VersionService { /** * Increasing an existing Version means copying the whole Version object - * with Images and connected ImageConfigs and with Deployments and connected - * Instances and InstanceConfigs. + * with Images and connected ImageConfigs, + * and - when autoCopyDeployments is true - with Deployments + * with their connected Instances and InstanceConfigs. * * @async * @method @@ -375,16 +388,23 @@ export default class VersionService { }, }) - const increasedVersion = await this.prisma.$transaction(async prisma => { - const version = await prisma.version.create({ - data: { - projectId: parentVersion.projectId, - name: request.name, - changelog: request.changelog, - default: false, - createdBy: identity.id, - type: parentVersion.type, + const increased = increaseIncrementalVersion(parentVersion, request.name, request.changelog) + + const newVersionData: Prisma.VersionCreateInput = { + ...increased, + createdBy: identity.id, + images: undefined, + deployments: undefined, + project: { + connect: { + id: parentVersion.projectId, }, + }, + } + + const newVersion = await this.prisma.$transaction(async prisma => { + const version = await prisma.version.create({ + data: newVersionData, include: { children: { select: { @@ -400,69 +420,75 @@ export default class VersionService { }, }) - const images: [string, string][] = await Promise.all( - // Iterate through the version images - parentVersion.images.map(async image => { + // Create images + const imageIdEntries: [string, string][] = await Promise.all( + increased.images.map(async image => { + const { originalId } = image + delete image.originalId + const createdImage = await prisma.image.create({ data: { - name: image.name, - tag: image.tag, - order: image.order, - registryId: image.registryId, + ...image, versionId: version.id, createdBy: identity.id, + config: { + create: this.imageMapper.dbContainerConfigToCreateImageStatement({ + ...image.config, + id: undefined, + imageId: undefined, + }), + }, }, }) - await prisma.containerConfig.create({ - data: { - ...this.imageMapper.dbContainerConfigToCreateImageStatement(image.config), - id: undefined, - imageId: createdImage.id, - }, - }) - - return [image.id, createdImage.id] + return [originalId, createdImage.id] }), ) - const imageMap = new Map(images) + // Create deployments + const imageIdMap = new Map(imageIdEntries) await Promise.all( - // Iterate through the deployments images - parentVersion.deployments.map(async deployment => { - const createdDeploy = await prisma.deployment.create({ + increased.deployments.map(async deployment => { + const newDeployment = await prisma.deployment.create({ data: { + ...deployment, createdBy: identity.id, - note: deployment.note, - prefix: deployment.prefix, - // Default status for deployments is preparing - status: DeploymentStatusEnum.preparing, - environment: deployment.environment ?? [], - nodeId: deployment.nodeId, versionId: version.id, + instances: undefined, }, }) await Promise.all( deployment.instances.map(async instance => { - const imageId = imageMap.get(instance.imageId) + const { originalImageId } = instance + delete instance.originalImageId - const createdInstance = await prisma.instance.create({ + const newImageId = imageIdMap.get(originalImageId) + + await prisma.instance.create({ data: { - deploymentId: createdDeploy.id, - imageId, + ...instance, + deployment: { + connect: { + id: newDeployment.id, + }, + }, + image: { + connect: { + id: newImageId, + }, + }, + config: !instance.config + ? undefined + : { + create: this.imageMapper.dbContainerConfigToCreateImageStatement({ + ...instance.config, + id: undefined, + instanceId: undefined, + }), + }, }, }) - - if (instance.config) { - await prisma.instanceContainerConfig.create({ - data: { - ...this.imageMapper.dbContainerConfigToCreateImageStatement(instance.config), - id: undefined, - instanceId: createdInstance.id, - }, - }) - } }), ) }), @@ -480,16 +506,16 @@ export default class VersionService { }) // End of Prisma transaction await this.notificationService.sendNotification({ - teamId: increasedVersion.project.teamId, + teamId: newVersion.project.teamId, messageType: 'version', message: { - subject: increasedVersion.project.name, - version: increasedVersion.name, + subject: newVersion.project.name, + version: newVersion.name, owner: identity, } as VersionMessage, }) - return this.mapper.toDto(increasedVersion) + return this.mapper.toDto(newVersion) } async onEditorJoined( diff --git a/web/crux/src/domain/version-increase.ts b/web/crux/src/domain/version-increase.ts new file mode 100644 index 0000000000..370da66c1c --- /dev/null +++ b/web/crux/src/domain/version-increase.ts @@ -0,0 +1,149 @@ +import { + ContainerConfig, + Deployment, + DeploymentStatusEnum, + Image, + Instance, + InstanceContainerConfig, + Version, +} from '@prisma/client' + +type ImageWithConfig = Image & { + config: ContainerConfig +} + +type InstanceWithConfig = Instance & { + config: InstanceContainerConfig | null +} + +type DeploymentWithInstances = Deployment & { + instances: InstanceWithConfig[] +} + +export type IncreasableVersion = Version & { + images: ImageWithConfig[] + deployments: DeploymentWithInstances[] +} + +type CopiedImageWithConfig = Omit & { + originalId: string + config: Omit +} + +type CopiedInstanceWithConfig = Omit & { + originalImageId: string + config: Omit +} + +type CopiedDeploymentWithInstances = Omit & { + instances: CopiedInstanceWithConfig[] +} + +export type IncreasedVersion = Omit & { + images: CopiedImageWithConfig[] + deployments: CopiedDeploymentWithInstances[] +} + +const copyInstance = (instance: InstanceWithConfig): CopiedInstanceWithConfig => { + const newInstance: CopiedInstanceWithConfig = { + originalImageId: instance.imageId, + updatedAt: undefined, + config: null, + } + + if (instance.config) { + const config = { + ...instance.config, + } + + delete config.id + delete config.instanceId + + newInstance.config = config + } + + return newInstance +} + +const copyDeployment = (deployment: DeploymentWithInstances): CopiedDeploymentWithInstances => { + const newDeployment: CopiedDeploymentWithInstances = { + note: deployment.note, + prefix: deployment.prefix, + // default status for deployments is preparing + status: DeploymentStatusEnum.preparing, + environment: deployment.environment ?? [], + nodeId: deployment.nodeId, + protected: deployment.protected, + tries: 0, + instances: [], + updatedAt: undefined, + updatedBy: undefined, + } + + deployment.instances.forEach(instance => { + const newInstance = copyInstance(instance) + + newDeployment.instances.push(newInstance) + }) + + return newDeployment +} + +const copyImage = (image: ImageWithConfig): CopiedImageWithConfig => { + const config = { + ...image.config, + } + + delete config.id + delete config.imageId + + const newImage: CopiedImageWithConfig = { + originalId: image.id, + name: image.name, + tag: image.tag, + order: image.order, + registryId: image.registryId, + labels: image.labels, + config, + updatedAt: undefined, + updatedBy: undefined, + } + + return newImage +} + +export const increaseIncrementalVersion = ( + parentVersion: IncreasableVersion, + name: string, + changelog: string, +): IncreasedVersion => { + const newVersion: IncreasedVersion = { + name, + changelog, + default: false, + type: parentVersion.type, + autoCopyDeployments: parentVersion.autoCopyDeployments, + images: [], + deployments: [], + updatedAt: undefined, + updatedBy: undefined, + } + + // copy images + parentVersion.images.forEach(image => { + const newImage = copyImage(image) + + newVersion.images.push(newImage) + }) + + if (parentVersion.autoCopyDeployments) { + // copy deployments + parentVersion.deployments.forEach(deployment => { + const newDeployment = copyDeployment(deployment) + + newVersion.deployments.push(newDeployment) + }) + } + + return newVersion +}