From beabb811db1cbcc1fe1623abd9df4ee85499543e Mon Sep 17 00:00:00 2001 From: Mate Vago Date: Wed, 6 Nov 2024 10:16:58 +0100 Subject: [PATCH 01/32] refactor: container configurations --- .../use-config-bundle-details-state.tsx | 14 +- .../projects/versions/use-version-state.ts | 29 +- web/crux-ui/src/models/config-bundle.ts | 25 +- web/crux-ui/src/models/container-config.ts | 13 + web/crux-ui/src/models/deployment.ts | 45 +- web/crux-ui/src/models/image.ts | 12 +- web/crux-ui/src/models/index.ts | 1 + .../migration.sql | 195 +++++ web/crux/prisma/schema.prisma | 194 +++-- .../app/config.bundle/config.bundle.dto.ts | 10 +- .../config.bundle.http.controller.ts | 4 +- .../app/config.bundle/config.bundle.mapper.ts | 19 +- .../config.bundle/config.bundle.message.ts | 17 - .../app/config.bundle/config.bundle.module.ts | 5 +- .../config.bundle/config.bundle.service.ts | 29 +- .../container-config.domain-event.listener.ts | 22 + .../app/container/container-config.message.ts | 15 + .../app/container/container-config.service.ts | 218 ++++++ .../container-config.ws.gateway.ts} | 68 +- web/crux/src/app/container/container.const.ts | 45 ++ web/crux/src/app/container/container.dto.ts | 43 +- .../src/app/container/container.mapper.ts | 262 ++++--- .../src/app/container/container.module.ts | 18 +- .../deploy/deploy.domain-event.listener.ts | 100 +++ web/crux/src/app/deploy/deploy.dto.ts | 86 +-- .../src/app/deploy/deploy.http.controller.ts | 6 +- web/crux/src/app/deploy/deploy.mapper.spec.ts | 110 +-- web/crux/src/app/deploy/deploy.mapper.ts | 340 ++++++--- web/crux/src/app/deploy/deploy.message.ts | 47 +- web/crux/src/app/deploy/deploy.module.ts | 4 + web/crux/src/app/deploy/deploy.service.ts | 595 +++++++--------- web/crux/src/app/deploy/deploy.ws.gateway.ts | 128 +--- .../interceptors/deploy.patch.interceptor.ts | 4 +- .../interceptors/deploy.start.interceptor.ts | 204 +++--- web/crux/src/app/image/image.const.ts | 46 -- web/crux/src/app/image/image.dto.ts | 7 +- web/crux/src/app/image/image.event.service.ts | 37 - web/crux/src/app/image/image.event.ts | 14 - web/crux/src/app/image/image.mapper.ts | 173 +---- web/crux/src/app/image/image.module.ts | 4 +- web/crux/src/app/image/image.service.ts | 108 +-- web/crux/src/app/package/package.module.ts | 4 +- web/crux/src/app/package/package.service.ts | 28 +- .../storage.delete.interceptor.ts | 15 +- web/crux/src/app/storage/storage.mapper.ts | 3 +- web/crux/src/app/storage/storage.service.ts | 10 +- web/crux/src/app/template/template.service.ts | 30 +- .../version/version.domain-event.listener.ts | 40 ++ web/crux/src/app/version/version.mapper.ts | 14 + web/crux/src/app/version/version.message.ts | 18 +- web/crux/src/app/version/version.module.ts | 5 +- web/crux/src/app/version/version.service.ts | 171 +++-- .../src/app/version/version.ws.gateway.ts | 47 +- web/crux/src/domain/container-conflict.ts | 673 ++++++++++++++++++ web/crux/src/domain/container-merge.spec.ts | 528 ++++++++++++++ web/crux/src/domain/container-merge.ts | 272 +++++++ web/crux/src/domain/container.ts | 40 +- web/crux/src/domain/deployment.ts | 40 +- web/crux/src/domain/domain-events.ts | 58 ++ web/crux/src/domain/start-deployment.ts | 122 ++++ web/crux/src/domain/utils.ts | 16 + web/crux/src/domain/version-increase.ts | 74 +- .../interceptors/prisma-error-interceptor.ts | 1 - web/crux/src/shared/domain-event.ts | 4 + web/crux/src/websockets/common.ts | 1 + web/crux/src/websockets/namespace.ts | 5 + 66 files changed, 3761 insertions(+), 1774 deletions(-) create mode 100644 web/crux-ui/src/models/container-config.ts create mode 100644 web/crux/prisma/migrations/20241017094935_config_rework/migration.sql delete mode 100644 web/crux/src/app/config.bundle/config.bundle.message.ts create mode 100644 web/crux/src/app/container/container-config.domain-event.listener.ts create mode 100644 web/crux/src/app/container/container-config.message.ts create mode 100644 web/crux/src/app/container/container-config.service.ts rename web/crux/src/app/{config.bundle/config.bundle.ws.gateway.ts => container/container-config.ws.gateway.ts} (67%) create mode 100644 web/crux/src/app/container/container.const.ts create mode 100644 web/crux/src/app/deploy/deploy.domain-event.listener.ts delete mode 100644 web/crux/src/app/image/image.event.service.ts delete mode 100644 web/crux/src/app/image/image.event.ts create mode 100644 web/crux/src/app/version/version.domain-event.listener.ts create mode 100644 web/crux/src/domain/container-conflict.ts create mode 100644 web/crux/src/domain/container-merge.spec.ts create mode 100644 web/crux/src/domain/container-merge.ts create mode 100644 web/crux/src/domain/domain-events.ts create mode 100644 web/crux/src/domain/start-deployment.ts create mode 100644 web/crux/src/shared/domain-event.ts diff --git a/web/crux-ui/src/components/config-bundles/use-config-bundle-details-state.tsx b/web/crux-ui/src/components/config-bundles/use-config-bundle-details-state.tsx index 1b9cc8c9b2..3834d549b2 100644 --- a/web/crux-ui/src/components/config-bundles/use-config-bundle-details-state.tsx +++ b/web/crux-ui/src/components/config-bundles/use-config-bundle-details-state.tsx @@ -6,21 +6,21 @@ import { ConfigBundleDetails, ConfigBundleUpdatedMessage, PatchConfigBundleMessage, - WS_TYPE_CONFIG_BUNDLE_UPDATED, - WS_TYPE_PATCH_CONFIG_BUNDLE, UniqueKeyValue, WS_TYPE_CONFIG_BUNDLE_PATCH_RECEIVED, + WS_TYPE_CONFIG_BUNDLE_UPDATED, + WS_TYPE_PATCH_CONFIG_BUNDLE, WebSocketSaveState, } from '@app/models' -import { useEffect, useState } from 'react' -import useEditorState from '../editor/use-editor-state' -import useItemEditorState, { ItemEditorState } from '../editor/use-item-editor-state' import { toastWarning } from '@app/utils' -import { useRouter } from 'next/router' +import { configBundlePatchSchema, getValidationError } from '@app/validations' import useTranslation from 'next-translate/useTranslation' +import { useRouter } from 'next/router' +import { useEffect, useState } from 'react' import { ValidationError } from 'yup' -import { getValidationError, configBundlePatchSchema } from '@app/validations' import EditorBadge from '../editor/editor-badge' +import useEditorState from '../editor/use-editor-state' +import useItemEditorState, { ItemEditorState } from '../editor/use-item-editor-state' export type ConfigBundleStateOptions = { configBundle: ConfigBundleDetails diff --git a/web/crux-ui/src/components/projects/versions/use-version-state.ts b/web/crux-ui/src/components/projects/versions/use-version-state.ts index 6c6c496b0b..f9719e8824 100644 --- a/web/crux-ui/src/components/projects/versions/use-version-state.ts +++ b/web/crux-ui/src/components/projects/versions/use-version-state.ts @@ -14,9 +14,8 @@ import { ImageMessage, ImagesAddedMessage, ImagesWereReorderedMessage, - ImageUpdateMessage, + ImageTagMessage, OrderImagesMessage, - PatchImageMessage, PatchVersionImage, RegistryImages, RegistryImageTags, @@ -28,12 +27,13 @@ import { WS_TYPE_ADD_IMAGES, WS_TYPE_GET_IMAGE, WS_TYPE_IMAGE, + WS_TYPE_IMAGE_DELETED, + WS_TYPE_IMAGE_SET_TAG, + WS_TYPE_IMAGE_TAG_UPDATED, WS_TYPE_IMAGES_ADDED, WS_TYPE_IMAGES_WERE_REORDERED, - WS_TYPE_IMAGE_DELETED, - WS_TYPE_IMAGE_UPDATED, WS_TYPE_ORDER_IMAGES, - WS_TYPE_PATCH_IMAGE, + WS_TYPE_PATCH_CONFIG, WS_TYPE_PATCH_RECEIVED, WS_TYPE_REGISTRY_FETCH_IMAGE_TAGS, WS_TYPE_REGISTRY_IMAGE_TAGS, @@ -159,7 +159,7 @@ export const useVersionState = (options: VersionStateOptions): [VerionState, Ver onOpen: viewMode !== 'tile' ? null : () => setSaveState('connected'), onClose: viewMode !== 'tile' ? null : () => setSaveState('disconnected'), onSend: message => { - if (message.type === WS_TYPE_PATCH_IMAGE) { + if (message.type === WS_TYPE_IMAGE_SET_TAG || message.type === WS_TYPE_PATCH_CONFIG) { setSaveState('saving') } }, @@ -217,17 +217,20 @@ export const useVersionState = (options: VersionStateOptions): [VerionState, Ver setVersion({ ...version, images: newImages }) }) - versionSock.on(WS_TYPE_IMAGE_UPDATED, (message: ImageUpdateMessage) => { - const index = version.images.findIndex(it => it.id === message.id) + versionSock.on(WS_TYPE_IMAGE_TAG_UPDATED, (message: ImageTagMessage) => { + const index = version.images.findIndex(it => it.id === message.imageId) if (index < 0) { versionSock.send(WS_TYPE_GET_IMAGE, { - id: message.id, + id: message.imageId, } as GetImageMessage) return } const oldImage = version.images[index] - const image = mergeImagePatch(oldImage, message) + + const image = mergeImagePatch(oldImage, { + tag: message.tag, + }) const newImages = [...version.images] newImages[index] = image @@ -314,10 +317,10 @@ export const useVersionState = (options: VersionStateOptions): [VerionState, Ver setVersion({ ...version, images: newImages }) - versionSock.send(WS_TYPE_PATCH_IMAGE, { - id: image.id, + versionSock.send(WS_TYPE_IMAGE_SET_TAG, { + imageId: image.id, tag, - } as PatchImageMessage) + } as ImageTagMessage) } const updateImageConfig = (image: VersionImage, config: Partial) => { diff --git a/web/crux-ui/src/models/config-bundle.ts b/web/crux-ui/src/models/config-bundle.ts index baf1957a9b..627e1c98f9 100644 --- a/web/crux-ui/src/models/config-bundle.ts +++ b/web/crux-ui/src/models/config-bundle.ts @@ -1,4 +1,4 @@ -import { UniqueKeyValue } from './container' +import { ContainerConfigData, UniqueKeyValue } from './container' export type BasicConfigBundle = { id: string @@ -10,7 +10,7 @@ export type ConfigBundle = BasicConfigBundle & { } export type ConfigBundleDetails = ConfigBundle & { - environment: UniqueKeyValue[] + config: ContainerConfigData } export type CreateConfigBundle = { @@ -18,25 +18,14 @@ export type CreateConfigBundle = { description?: string } -export type UpdateConfigBundle = CreateConfigBundle & { - environment: UniqueKeyValue[] -} - -export type ConfigBundleOption = BasicConfigBundle - -// ws -export const WS_TYPE_PATCH_CONFIG_BUNDLE = 'patch-config-bundle' -export type PatchConfigBundleMessage = { +export type PatchConfigBundle = { name?: string description?: string - environment?: UniqueKeyValue[] + config?: ContainerConfigData } -export const WS_TYPE_CONFIG_BUNDLE_UPDATED = 'config-bundle-updated' -export type ConfigBundleUpdatedMessage = { - name?: string - description?: string - environment?: UniqueKeyValue[] +export type UpdateConfigBundle = CreateConfigBundle & { + environment: UniqueKeyValue[] } -export const WS_TYPE_CONFIG_BUNDLE_PATCH_RECEIVED = 'patch-received' +export type ConfigBundleOption = BasicConfigBundle diff --git a/web/crux-ui/src/models/container-config.ts b/web/crux-ui/src/models/container-config.ts new file mode 100644 index 0000000000..ac8b56de60 --- /dev/null +++ b/web/crux-ui/src/models/container-config.ts @@ -0,0 +1,13 @@ +import { ContainerConfigData } from './container' +import { ImageConfigProperty } from './image' + +export const WS_TYPE_PATCH_CONFIG = 'patch-config' +export type PatchConfigMessage = { + config?: ContainerConfigData + resetSection?: ImageConfigProperty +} + +export const WS_TYPE_CONFIG_UPDATED = 'config-updated' +export type ConfigUpdatedMessage = ContainerConfigData & { + id: string +} diff --git a/web/crux-ui/src/models/deployment.ts b/web/crux-ui/src/models/deployment.ts index 1ae42fc703..d00ede68dc 100644 --- a/web/crux-ui/src/models/deployment.ts +++ b/web/crux-ui/src/models/deployment.ts @@ -1,7 +1,8 @@ import { Audit } from './audit' import { DeploymentStatus, DyoApiError, slugify } from './common' -import { ContainerIdentifier, ContainerState, InstanceContainerConfigData, UniqueKeyValue } from './container' -import { ImageConfigProperty, ImageDeletedMessage } from './image' +import { ConfigBundleDetails } from './config-bundle' +import { ContainerIdentifier, ContainerState, UniqueKeyValue } from './container' +import { ImageDeletedMessage, VersionImage } from './image' import { Instance } from './instance' import { DyoNode } from './node' import { BasicProject, ProjectDetails } from './project' @@ -137,30 +138,9 @@ export type StartDeployment = { } // ws - -export const WS_TYPE_PATCH_DEPLOYMENT_ENV = 'patch-deployment-env' -export type PatchDeploymentEnvMessage = { - environment?: UniqueKeyValue[] - configBundleIds?: string[] -} - -export const WS_TYPE_DEPLOYMENT_ENV_UPDATED = 'deployment-env-updated' -export type DeploymentEnvUpdatedMessage = { - environment?: UniqueKeyValue[] - configBundleIds?: string[] - configBundleEnvironment: EnvironmentToConfigBundleNameMap -} - -export const WS_TYPE_PATCH_INSTANCE = 'patch-instance' -export type PatchInstanceMessage = { - instanceId: string - config?: Partial - resetSection?: ImageConfigProperty -} - -export const WS_TYPE_INSTANCE_UPDATED = 'instance-updated' -export type InstanceUpdatedMessage = InstanceContainerConfigData & { - instanceId: string +export const WS_TYPE_DEPLOYMENT_BUNDLES_UPDATED = 'deployment-bundles-updated' +export type DeploymentBundlesUpdatedMessage = { + bundles: ConfigBundleDetails[] } export const WS_TYPE_GET_INSTANCE = 'get-instance' @@ -172,7 +152,18 @@ export const WS_TYPE_INSTANCE = 'instance' export type InstanceMessage = Instance & {} export const WS_TYPE_INSTANCES_ADDED = 'instances-added' -export type InstancesAddedMessage = Instance[] +type InstanceCreatedMessage = { + id: string + configId: string + image: VersionImage +} +export type InstancesAddedMessage = InstanceCreatedMessage[] + +export const WS_TYPE_INSTANCE_DELETED = 'instance-deleted' +export type InstanceDeletedMessage = { + instanceId: string + configId: string +} export type DeploymentEditEventMessage = InstancesAddedMessage | ImageDeletedMessage diff --git a/web/crux-ui/src/models/image.ts b/web/crux-ui/src/models/image.ts index 6f7285b64f..3404cf6eaa 100644 --- a/web/crux-ui/src/models/image.ts +++ b/web/crux-ui/src/models/image.ts @@ -35,7 +35,6 @@ export type AddImages = { } // ws - export const WS_TYPE_ADD_IMAGES = 'add-images' export type AddImagesMessage = { registryImages: RegistryImages[] @@ -56,9 +55,11 @@ export type ImagesAddedMessage = { images: VersionImage[] } -export const WS_TYPE_PATCH_IMAGE = 'patch-image' -export type PatchImageMessage = PatchVersionImage & { - id: string +export const WS_TYPE_IMAGE_SET_TAG = 'image-set-tag' +export const WS_TYPE_IMAGE_TAG_UPDATED = 'image-tag-updated' +export type ImageTagMessage = { + imageId: string + tag: string } export const WS_TYPE_ORDER_IMAGES = 'order-images' @@ -67,9 +68,6 @@ export type OrderImagesMessage = string[] export const WS_TYPE_IMAGES_WERE_REORDERED = 'images-were-reordered' export type ImagesWereReorderedMessage = string[] -export const WS_TYPE_IMAGE_UPDATED = 'image-updated' -export type ImageUpdateMessage = PatchImageMessage - export const WS_TYPE_GET_IMAGE = 'get-image' export type GetImageMessage = { id: string diff --git a/web/crux-ui/src/models/index.ts b/web/crux-ui/src/models/index.ts index 11643c799a..6fcc022b8d 100644 --- a/web/crux-ui/src/models/index.ts +++ b/web/crux-ui/src/models/index.ts @@ -4,6 +4,7 @@ export * from './common' export * from './config-bundle' export * from './package' export * from './container' +export * from './container-config' export * from './dashboard' export * from './deployment' export * from './editor' diff --git a/web/crux/prisma/migrations/20241017094935_config_rework/migration.sql b/web/crux/prisma/migrations/20241017094935_config_rework/migration.sql new file mode 100644 index 0000000000..eb812731c4 --- /dev/null +++ b/web/crux/prisma/migrations/20241017094935_config_rework/migration.sql @@ -0,0 +1,195 @@ +-- CreateEnum +CREATE TYPE "ContainerConfigType" AS ENUM ('image', 'instance', 'deployment', 'configBundle'); + +-- DropForeignKey +ALTER TABLE "ContainerConfig" DROP CONSTRAINT "ContainerConfig_imageId_fkey"; + +-- DropForeignKey +ALTER TABLE "InstanceContainerConfig" DROP CONSTRAINT "InstanceContainerConfig_instanceId_fkey"; + +-- DropForeignKey +ALTER TABLE "InstanceContainerConfig" DROP CONSTRAINT "InstanceContainerConfig_storageId_fkey"; + +-- DropIndex +DROP INDEX "ContainerConfig_imageId_key"; + +-- ContainerConfig and Image +-- reverse ContainerConfig -> Image relation +ALTER TABLE "Image" ADD COLUMN "configId" UUID; + +UPDATE "Image" +SET "configId" = "cc"."id" +FROM (SELECT DISTINCT "id", "imageId" FROM "ContainerConfig") AS "cc" +WHERE "cc"."imageId" = "Image"."id"; + +-- add ContainerConfigType +ALTER TABLE "ContainerConfig" +ADD COLUMN "type" "ContainerConfigType"; + +UPDATE "ContainerConfig" +SET "type" = 'image'::"ContainerConfigType"; + +ALTER TABLE "ContainerConfig" +ALTER COLUMN "type" SET NOT NULL; + +-- add audit fields +ALTER TABLE "ContainerConfig" +ADD COLUMN "updatedAt" TIMESTAMPTZ(6), +ADD COLUMN "updatedBy" TEXT; + +UPDATE "ContainerConfig" +SET "updatedAt" = "i"."updatedAt", + "updatedBy" = "i"."updatedBy" +FROM (SELECT "id", "updatedAt", "updatedBy" FROM "Image") AS "i" +WHERE "i"."id" = "ContainerConfig"."imageId"; + +ALTER TABLE "ContainerConfig" +ALTER COLUMN "updatedAt" SET NOT NULL; + +-- drop imageId +ALTER TABLE "ContainerConfig" DROP COLUMN "imageId"; + +-- drop not nulls and defaults +ALTER TABLE "ContainerConfig" +ALTER COLUMN "name" DROP NOT NULL, +ALTER COLUMN "expose" DROP NOT NULL, +ALTER COLUMN "user" DROP NOT NULL, +ALTER COLUMN "user" DROP DEFAULT, +ALTER COLUMN "restartPolicy" DROP NOT NULL, +ALTER COLUMN "networkMode" DROP NOT NULL, +ALTER COLUMN "deploymentStrategy" DROP NOT NULL, +ALTER COLUMN "proxyHeaders" DROP NOT NULL, +ALTER COLUMN "tty" DROP NOT NULL, +ALTER COLUMN "useLoadBalancer" DROP NOT NULL; + + +-- ConfigBundle +ALTER TABLE "ConfigBundle" +ADD COLUMN "configId" UUID; + +UPDATE "ConfigBundle" +SET "configId" = gen_random_uuid() +WHERE "data" IS NOT NULL; + +INSERT INTO "ContainerConfig" +("id", "type", "updatedAt", "updatedBy", "environment") +SELECT "configId", 'configBundle'::"ContainerConfigType", "updatedAt", "updatedBy", "data" +FROM "ConfigBundle" +WHERE "data" IS NOT NULL; + +ALTER TABLE "ConfigBundle" +DROP COLUMN "data"; + + +-- Deployment +ALTER TABLE "Deployment" +ADD COLUMN "configId" UUID; + +UPDATE "Deployment" +SET "configId" = gen_random_uuid() +WHERE "environment" IS NOT NULL; + +INSERT INTO "ContainerConfig" +("id", "type", "updatedAt", "updatedBy", "environment") +SELECT "configId", 'deployment'::"ContainerConfigType", "updatedAt", "updatedBy", "environment" +FROM "Deployment" +WHERE "environment" IS NOT NULL; + + +ALTER TABLE "Deployment" +DROP COLUMN "environment"; + + +-- Instance +ALTER TABLE "Instance" +ADD COLUMN "configId" UUID; + +UPDATE "Instance" +SET "configId" = "i"."id" +FROM (SELECT DISTINCT "id", "instanceId" FROM "InstanceContainerConfig") AS "i" +WHERE "i"."instanceId" = "Instance"."id"; + + +-- fix instance config +UPDATE "InstanceContainerConfig" SET "name" = null WHERE "name" = 'null'; +UPDATE "InstanceContainerConfig" SET "environment" = null WHERE "environment" = 'null'; +UPDATE "InstanceContainerConfig" SET "secrets" = null WHERE "secrets" = 'null'; +UPDATE "InstanceContainerConfig" SET "capabilities" = null WHERE "capabilities" = 'null'; +UPDATE "InstanceContainerConfig" SET "configContainer" = null WHERE "configContainer" = 'null'; +UPDATE "InstanceContainerConfig" SET "ports" = null WHERE "ports" = 'null'; +UPDATE "InstanceContainerConfig" SET "portRanges" = null WHERE "portRanges" = 'null'; +UPDATE "InstanceContainerConfig" SET "volumes" = null WHERE "volumes" = 'null'; +UPDATE "InstanceContainerConfig" SET "commands" = null WHERE "commands" = 'null'; +UPDATE "InstanceContainerConfig" SET "args" = null WHERE "args" = 'null'; +UPDATE "InstanceContainerConfig" SET "initContainers" = null WHERE "initContainers" = 'null'; +UPDATE "InstanceContainerConfig" SET "logConfig" = null WHERE "logConfig" = 'null'; +UPDATE "InstanceContainerConfig" SET "networks" = null WHERE "networks" = 'null'; +UPDATE "InstanceContainerConfig" SET "dockerLabels" = null WHERE "dockerLabels" = 'null'; +UPDATE "InstanceContainerConfig" SET "healthCheckConfig" = null WHERE "healthCheckConfig" = 'null'; +UPDATE "InstanceContainerConfig" SET "resourceConfig" = null WHERE "resourceConfig" = 'null'; +UPDATE "InstanceContainerConfig" SET "extraLBAnnotations" = null WHERE "extraLBAnnotations" = 'null'; +UPDATE "InstanceContainerConfig" SET "customHeaders" = null WHERE "customHeaders" = 'null'; +UPDATE "InstanceContainerConfig" SET "annotations" = null WHERE "annotations" = 'null'; +UPDATE "InstanceContainerConfig" SET "labels" = null WHERE "labels" = 'null'; +UPDATE "InstanceContainerConfig" SET "storageConfig" = null WHERE "storageConfig" = 'null'; +UPDATE "InstanceContainerConfig" SET "routing" = null WHERE "routing" = 'null'; +UPDATE "InstanceContainerConfig" SET "metrics" = null WHERE "metrics" = 'null'; +UPDATE "InstanceContainerConfig" SET "workingDirectory" = null WHERE "workingDirectory" = 'null'; +UPDATE "InstanceContainerConfig" SET "expectedState" = null WHERE "expectedState" = 'null'; + + +INSERT INTO "ContainerConfig" ( + "id", "updatedAt", "updatedBy", "type", + -- common + "name", "environment", "secrets", "capabilities", "expose", "routing", "configContainer", "user", + "workingDirectory", "tty", "ports", "portRanges", "volumes", "commands", "args", "initContainers", + "storageId", "storageSet", "storageConfig", "expectedState", + -- dagent + "logConfig", "restartPolicy", "networkMode", "networks", "dockerLabels", + -- crane + "deploymentStrategy", "healthCheckConfig", "resourceConfig", "proxyHeaders", "useLoadBalancer", + "extraLBAnnotations", "customHeaders", "annotations", "labels", "metrics" +) +SELECT + "InstanceContainerConfig"."id", "i"."updatedAt", "d"."updatedBy", 'instance'::"ContainerConfigType", + -- common + "name", "environment", "secrets", "capabilities", "expose", "routing", "configContainer", "user", + "workingDirectory", "tty", "ports", "portRanges", "volumes", "commands", "args", "initContainers", + "storageId", "storageSet", "storageConfig", "expectedState", + -- dagent + "logConfig", "restartPolicy", "networkMode", "networks", "dockerLabels", + -- crane + "deploymentStrategy", "healthCheckConfig", "resourceConfig", "proxyHeaders", "useLoadBalancer", + "extraLBAnnotations", "customHeaders", "annotations", "labels", "metrics" +FROM "InstanceContainerConfig" +INNER JOIN "Instance" AS "i" ON "i"."id" = "InstanceContainerConfig"."instanceId" +INNER JOIN "Deployment" AS "d" ON "d"."id" = "i"."deploymentId"; + +ALTER TABLE "Instance" +DROP COLUMN "updatedAt"; + +DROP TABLE "InstanceContainerConfig"; + +-- CreateIndex +CREATE UNIQUE INDEX "ConfigBundle_configId_key" ON "ConfigBundle"("configId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Deployment_configId_key" ON "Deployment"("configId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Image_configId_key" ON "Image"("configId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Instance_configId_key" ON "Instance"("configId"); + +-- AddForeignKey +ALTER TABLE "Image" ADD CONSTRAINT "Image_configId_fkey" FOREIGN KEY ("configId") REFERENCES "ContainerConfig"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Deployment" ADD CONSTRAINT "Deployment_configId_fkey" FOREIGN KEY ("configId") REFERENCES "ContainerConfig"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Instance" ADD CONSTRAINT "Instance_configId_fkey" FOREIGN KEY ("configId") REFERENCES "ContainerConfig"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "ConfigBundle" ADD CONSTRAINT "ConfigBundle_configId_fkey" FOREIGN KEY ("configId") REFERENCES "ContainerConfig"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/web/crux/prisma/schema.prisma b/web/crux/prisma/schema.prisma index a093091c2f..ece7a7ba44 100644 --- a/web/crux/prisma/schema.prisma +++ b/web/crux/prisma/schema.prisma @@ -203,22 +203,27 @@ model VersionsOnParentVersion { } model Image { - id String @id @default(uuid()) @db.Uuid - name String - tag String? - order Int - versionId String @db.Uuid - registryId String @db.Uuid - config ContainerConfig? - instances Instance[] - createdAt DateTime @default(now()) @db.Timestamptz(6) - createdBy String @db.Uuid - updatedAt DateTime @updatedAt @db.Timestamptz(6) - updatedBy String? @db.Uuid - labels Json? + 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 - registry Registry @relation(fields: [registryId], references: [id], onDelete: Cascade) - version Version @relation(fields: [versionId], references: [id], onDelete: Cascade) + name String + tag String? + order Int + labels Json? + + registry Registry @relation(fields: [registryId], references: [id], onDelete: Cascade) + registryId String @db.Uuid + + version Version @relation(fields: [versionId], references: [id], onDelete: Cascade) + versionId String @db.Uuid + + config ContainerConfig? @relation(fields: [configId], references: [id], onDelete: SetNull) + configId String? @unique @db.Uuid + + instances Instance[] } enum NetworkMode { @@ -245,20 +250,31 @@ enum ExposeStrategy { exposeWithTls } +enum ContainerConfigType { + image + instance + deployment + configBundle +} + model ContainerConfig { - id String @id @default(uuid()) @db.Uuid + id String @id @default(uuid()) @db.Uuid + updatedAt DateTime @updatedAt @db.Timestamptz(6) + updatedBy String? + + type ContainerConfigType //Common - name String + name String? environment Json? secrets Json? capabilities Json? - expose ExposeStrategy + expose ExposeStrategy? routing Json? configContainer Json? - user Int @default(-1) + user Int? workingDirectory String? - tty Boolean + tty Boolean? ports Json? portRanges Json? volumes Json? @@ -266,62 +282,68 @@ model ContainerConfig { args Json? initContainers Json? storageSet Boolean? - storageId String? @db.Uuid storageConfig Json? expectedState Json? //Dagent logConfig Json? - restartPolicy RestartPolicy - networkMode NetworkMode + restartPolicy RestartPolicy? + networkMode NetworkMode? networks Json? dockerLabels Json? //Crane - deploymentStrategy DeploymentStrategy + deploymentStrategy DeploymentStrategy? healthCheckConfig Json? resourceConfig Json? - proxyHeaders Boolean - useLoadBalancer Boolean + proxyHeaders Boolean? + useLoadBalancer Boolean? extraLBAnnotations Json? customHeaders Json? annotations Json? labels Json? metrics Json? - image Image @relation(fields: [imageId], references: [id], onDelete: Cascade) - imageId String @unique @db.Uuid + image Image? + instance Instance? + deployment Deployment? + configBundle ConfigBundle? - storage Storage? @relation(fields: [storageId], references: [id], onDelete: Cascade) + storage Storage? @relation(fields: [storageId], references: [id], onDelete: Cascade) + storageId String? @db.Uuid } model Deployment { - 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 - note String? - prefix String? - status DeploymentStatusEnum - environment Json? - versionId String @db.Uuid - nodeId String @db.Uuid - tries Int @default(0) - protected Boolean @default(false) - - version Version @relation(fields: [versionId], references: [id], onDelete: Cascade) - node Node @relation(fields: [nodeId], references: [id], onDelete: Cascade) + 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 + + note String? + prefix String? + status DeploymentStatusEnum + tries Int @default(0) + protected Boolean @default(false) + + version Version @relation(fields: [versionId], references: [id], onDelete: Cascade) + versionId String @db.Uuid + + node Node @relation(fields: [nodeId], references: [id], onDelete: Cascade) + nodeId String @db.Uuid + + config ContainerConfig? @relation(fields: [configId], references: [id], onDelete: SetNull) + configId String? @unique @db.Uuid instances Instance[] events DeploymentEvent[] - tokens DeploymentToken[] + token DeploymentToken? configBundles ConfigBundleOnDeployments[] } model DeploymentToken { id String @id @default(uuid()) @db.Uuid - deploymentId String @db.Uuid + deploymentId String @unique @db.Uuid createdBy String @db.Uuid createdAt DateTime @default(now()) @db.Timestamptz(6) name String @@ -331,68 +353,20 @@ model DeploymentToken { deployment Deployment @relation(fields: [deploymentId], references: [id], onDelete: Cascade) AuditLog AuditLog[] - @@unique([deploymentId]) @@unique([deploymentId, nonce]) } model Instance { - id String @id @default(uuid()) @db.Uuid - updatedAt DateTime @updatedAt @db.Timestamptz(6) - deploymentId String @db.Uuid - imageId String @db.Uuid - - deployment Deployment @relation(fields: [deploymentId], references: [id], onDelete: Cascade) - image Image @relation(fields: [imageId], references: [id], onDelete: Cascade) - config InstanceContainerConfig? -} - -model InstanceContainerConfig { - id String @id @default(uuid()) @db.Uuid - instanceId String @unique @db.Uuid - - //Common - name String? - environment Json? - secrets Json? - capabilities Json? - expose ExposeStrategy? - routing Json? - configContainer Json? - user Int? - workingDirectory String? - tty Boolean? - ports Json? - portRanges Json? - volumes Json? - commands Json? - args Json? - initContainers Json? - storageSet Boolean? - storageId String? @unique @db.Uuid - storageConfig Json? - expectedState Json? + id String @id @default(uuid()) @db.Uuid - //Dagent - logConfig Json? - restartPolicy RestartPolicy? - networkMode NetworkMode? - networks Json? - dockerLabels Json? + deployment Deployment @relation(fields: [deploymentId], references: [id], onDelete: Cascade) + deploymentId String @db.Uuid - //Crane - deploymentStrategy DeploymentStrategy? - healthCheckConfig Json? - resourceConfig Json? - proxyHeaders Boolean? - useLoadBalancer Boolean? - extraLBAnnotations Json? - customHeaders Json? - annotations Json? - labels Json? - metrics Json? + image Image @relation(fields: [imageId], references: [id], onDelete: Cascade) + imageId String @db.Uuid - instance Instance @relation(fields: [instanceId], references: [id], onDelete: Cascade) - storage Storage? @relation(fields: [storageId], references: [id], onDelete: Cascade) + config ContainerConfig? @relation(fields: [configId], references: [id], onDelete: SetNull) + configId String? @unique @db.Uuid } model DeploymentEvent { @@ -579,7 +553,6 @@ model Storage { teamId String @db.Uuid containerConfigs ContainerConfig[] - instanceConfigs InstanceContainerConfig[] @@unique([name, teamId]) } @@ -665,18 +638,21 @@ model PipelineEventWatcher { } model ConfigBundle { - id String @id @default(uuid()) @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 description String? - data Json - createdAt DateTime @default(now()) @db.Timestamptz(6) - createdBy String @db.Uuid - updatedAt DateTime @updatedAt @db.Timestamptz(6) - updatedBy String? @db.Uuid team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) teamId String @db.Uuid + config ContainerConfig? @relation(fields: [configId], references: [id], onDelete: SetNull) + configId String? @unique @db.Uuid + deployments ConfigBundleOnDeployments[] @@unique([name, teamId]) diff --git a/web/crux/src/app/config.bundle/config.bundle.dto.ts b/web/crux/src/app/config.bundle/config.bundle.dto.ts index 0a0b2ef9b1..8b36b4062b 100644 --- a/web/crux/src/app/config.bundle/config.bundle.dto.ts +++ b/web/crux/src/app/config.bundle/config.bundle.dto.ts @@ -1,5 +1,5 @@ import { IsOptional, IsString, IsUUID, ValidateNested } from 'class-validator' -import { UniqueKeyValueDto } from '../container/container.dto' +import { ContainerConfigDto } from '../container/container.dto' class BasicConfigBundleDto { @IsUUID() @@ -16,8 +16,8 @@ export class ConfigBundleDto extends BasicConfigBundleDto { } export class ConfigBundleDetailsDto extends ConfigBundleDto { - @ValidateNested({ each: true }) - environment: UniqueKeyValueDto[] + @ValidateNested() + config: ContainerConfigDto } export class CreateConfigBundleDto { @@ -38,9 +38,9 @@ export class PatchConfigBundleDto { @IsOptional() description?: string - @ValidateNested({ each: true }) + @ValidateNested() @IsOptional() - environment?: UniqueKeyValueDto[] + config?: ContainerConfigDto } export class ConfigBundleOptionDto extends BasicConfigBundleDto {} diff --git a/web/crux/src/app/config.bundle/config.bundle.http.controller.ts b/web/crux/src/app/config.bundle/config.bundle.http.controller.ts index bc8eb36d1d..64de578b4e 100644 --- a/web/crux/src/app/config.bundle/config.bundle.http.controller.ts +++ b/web/crux/src/app/config.bundle/config.bundle.http.controller.ts @@ -6,8 +6,8 @@ import { HttpCode, HttpStatus, Param, + Patch, Post, - Put, UseGuards, UseInterceptors, } from '@nestjs/common' @@ -128,7 +128,7 @@ export default class ConfigBundlesHttpController { } } - @Put(ROUTE_CONFIG_BUNDLE_ID) + @Patch(ROUTE_CONFIG_BUNDLE_ID) @HttpCode(HttpStatus.NO_CONTENT) @ApiOperation({ description: 'Updates a config bundle. Request must include `id`, `name`, and `data`', diff --git a/web/crux/src/app/config.bundle/config.bundle.mapper.ts b/web/crux/src/app/config.bundle/config.bundle.mapper.ts index e68b5cadde..ca33e84f59 100644 --- a/web/crux/src/app/config.bundle/config.bundle.mapper.ts +++ b/web/crux/src/app/config.bundle/config.bundle.mapper.ts @@ -1,10 +1,13 @@ import { Injectable } from '@nestjs/common' -import { ConfigBundle } from '@prisma/client' -import { UniqueKeyValue } from 'src/domain/container' +import { ConfigBundle, ContainerConfig } from '@prisma/client' +import { ContainerConfigData } from 'src/domain/container' +import ContainerMapper from '../container/container.mapper' import { ConfigBundleDetailsDto, ConfigBundleDto } from './config.bundle.dto' @Injectable() export default class ConfigBundleMapper { + constructor(private readonly containerMapper: ContainerMapper) {} + listItemToDto(configBundle: ConfigBundle): ConfigBundleDto { return { id: configBundle.id, @@ -13,10 +16,18 @@ export default class ConfigBundleMapper { } } - detailsToDto(configBundle: ConfigBundle): ConfigBundleDetailsDto { + detailsToDto(configBundle: ConfigBundleDetails): ConfigBundleDetailsDto { return { ...this.listItemToDto(configBundle), - environment: configBundle.data as UniqueKeyValue[], + config: this.containerMapper.configDataToDto( + configBundle.id, + 'configBundle', + configBundle.config as any as ContainerConfigData, + ), } } } + +type ConfigBundleDetails = ConfigBundle & { + config?: ContainerConfig +} diff --git a/web/crux/src/app/config.bundle/config.bundle.message.ts b/web/crux/src/app/config.bundle/config.bundle.message.ts deleted file mode 100644 index 884a6e8497..0000000000 --- a/web/crux/src/app/config.bundle/config.bundle.message.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { UniqueKeyValue } from 'src/domain/container' - -export const WS_TYPE_PATCH_CONFIG_BUNDLE = 'patch-config-bundle' -export type PatchConfigBundleEnvMessage = { - name?: string - description?: string - environment?: UniqueKeyValue[] -} - -export const WS_TYPE_CONFIG_BUNDLE_UPDATED = 'config-bundle-updated' -export type ConfigBundleEnvUpdatedMessage = { - name?: string - description?: string - environment?: UniqueKeyValue[] -} - -export const WS_TYPE_PATCH_RECEIVED = 'patch-received' diff --git a/web/crux/src/app/config.bundle/config.bundle.module.ts b/web/crux/src/app/config.bundle/config.bundle.module.ts index bb8aaf90be..ccd9208e3e 100644 --- a/web/crux/src/app/config.bundle/config.bundle.module.ts +++ b/web/crux/src/app/config.bundle/config.bundle.module.ts @@ -3,16 +3,16 @@ import { Module } from '@nestjs/common' import KratosService from 'src/services/kratos.service' import PrismaService from 'src/services/prisma.service' import AuditLoggerModule from '../audit.logger/audit.logger.module' +import ContainerModule from '../container/container.module' import EditorModule from '../editor/editor.module' import TeamModule from '../team/team.module' import TeamRepository from '../team/team.repository' import ConfigBundlesHttpController from './config.bundle.http.controller' import ConfigBundleMapper from './config.bundle.mapper' import ConfigBundleService from './config.bundle.service' -import ConfigBundleWebSocketGateway from './config.bundle.ws.gateway' @Module({ - imports: [HttpModule, TeamModule, AuditLoggerModule, EditorModule], + imports: [HttpModule, TeamModule, AuditLoggerModule, EditorModule, ContainerModule], exports: [ConfigBundleMapper, ConfigBundleService], controllers: [ConfigBundlesHttpController], providers: [ @@ -21,7 +21,6 @@ import ConfigBundleWebSocketGateway from './config.bundle.ws.gateway' ConfigBundleMapper, TeamRepository, KratosService, - ConfigBundleWebSocketGateway, ], }) export default class ConfigBundleModule {} diff --git a/web/crux/src/app/config.bundle/config.bundle.service.ts b/web/crux/src/app/config.bundle/config.bundle.service.ts index 44667504e7..87a08bc8a5 100644 --- a/web/crux/src/app/config.bundle/config.bundle.service.ts +++ b/web/crux/src/app/config.bundle/config.bundle.service.ts @@ -1,7 +1,7 @@ import { Injectable, Logger } from '@nestjs/common' import { Identity } from '@ory/kratos-client' -import { toPrismaJson } from 'src/domain/utils' import PrismaService from 'src/services/prisma.service' +import ContainerConfigService from '../container/container-config.service' import { EditorLeftMessage, EditorMessage } from '../editor/editor.message' import EditorServiceProvider from '../editor/editor.service.provider' import TeamRepository from '../team/team.repository' @@ -19,9 +19,10 @@ export default class ConfigBundleService { private readonly logger = new Logger(ConfigBundleService.name) constructor( - private teamRepository: TeamRepository, - private prisma: PrismaService, - private mapper: ConfigBundleMapper, + private readonly teamRepository: TeamRepository, + private readonly prisma: PrismaService, + private readonly mapper: ConfigBundleMapper, + private readonly containerConfigService: ContainerConfigService, private readonly editorServices: EditorServiceProvider, ) {} @@ -58,8 +59,13 @@ export default class ConfigBundleService { data: { name: req.name, description: req.description, - data: [], - teamId, + config: { + create: { + type: 'configBundle', + updatedBy: identity.id, + }, + }, + team: { connect: { id: teamId } }, createdBy: identity.id, }, }) @@ -68,6 +74,16 @@ export default class ConfigBundleService { } async patchConfigBundle(id: string, req: PatchConfigBundleDto, identity: Identity): Promise { + if (req.config) { + await this.containerConfigService.patchConfig( + req.config.id, + { + config: req.config, + }, + identity, + ) + } + await this.prisma.configBundle.update({ where: { id, @@ -75,7 +91,6 @@ export default class ConfigBundleService { data: { name: req.name ?? undefined, description: req.description ?? undefined, - data: req.environment ? toPrismaJson(req.environment) : undefined, updatedBy: identity.id, }, }) diff --git a/web/crux/src/app/container/container-config.domain-event.listener.ts b/web/crux/src/app/container/container-config.domain-event.listener.ts new file mode 100644 index 0000000000..81f391d605 --- /dev/null +++ b/web/crux/src/app/container/container-config.domain-event.listener.ts @@ -0,0 +1,22 @@ +import { Injectable } from '@nestjs/common' +import { OnEvent } from '@nestjs/event-emitter' +import { filter, Observable, Subject } from 'rxjs' +import { CONTAINER_CONFIG_EVENT_UPDATE, ContainerConfigUpdatedEvent } from 'src/domain/domain-events' +import { DomainEvent } from 'src/shared/domain-event' + +@Injectable() +export default class ContainerConfigDomainEventListener { + private readonly configUpdatedEvents = new Subject>() + + watchEvents(configId: string): Observable> { + return this.configUpdatedEvents.pipe(filter(it => it.event.id === configId)) + } + + @OnEvent(CONTAINER_CONFIG_EVENT_UPDATE) + onContainerConfigUpdatedEvent(event: ContainerConfigUpdatedEvent) { + this.configUpdatedEvents.next({ + type: CONTAINER_CONFIG_EVENT_UPDATE, + event, + }) + } +} diff --git a/web/crux/src/app/container/container-config.message.ts b/web/crux/src/app/container/container-config.message.ts new file mode 100644 index 0000000000..011415187f --- /dev/null +++ b/web/crux/src/app/container/container-config.message.ts @@ -0,0 +1,15 @@ +import { ContainerConfigData } from 'src/domain/container' +import { ContainerConfigProperty } from './container.const' + +export const WS_TYPE_PATCH_CONFIG = 'patch-config' +export type PatchConfigMessage = { + config?: ContainerConfigData + resetSection?: ContainerConfigProperty +} + +export const WS_TYPE_PATCH_RECEIVED = 'patch-received' + +export const WS_TYPE_CONFIG_UPDATED = 'config-updated' +export type ConfigUpdatedMessage = ContainerConfigData & { + id: string +} diff --git a/web/crux/src/app/container/container-config.service.ts b/web/crux/src/app/container/container-config.service.ts new file mode 100644 index 0000000000..968f111825 --- /dev/null +++ b/web/crux/src/app/container/container-config.service.ts @@ -0,0 +1,218 @@ +import { Injectable, Logger } from '@nestjs/common' +import { EventEmitter2 } from '@nestjs/event-emitter' +import { Identity } from '@ory/kratos-client' +import { Prisma } from '@prisma/client' +import { Observable, filter, map } from 'rxjs' +import { ContainerConfigData } from 'src/domain/container' +import { checkDeploymentMutability } from 'src/domain/deployment' +import { CONTAINER_CONFIG_EVENT_UPDATE, ContainerConfigUpdatedEvent } from 'src/domain/domain-events' +import { checkVersionMutability } from 'src/domain/version' +import { CruxBadRequestException } from 'src/exception/crux-exception' +import PrismaService from 'src/services/prisma.service' +import { DomainEvent } from 'src/shared/domain-event' +import { WsMessage } from 'src/websockets/common' +import { EditorLeftMessage, EditorMessage } from '../editor/editor.message' +import EditorServiceProvider from '../editor/editor.service.provider' +import ContainerConfigDomainEventListener from './container-config.domain-event.listener' +import { ConfigUpdatedMessage, PatchConfigMessage, WS_TYPE_CONFIG_UPDATED } from './container-config.message' +import ContainerMapper from './container.mapper' + +@Injectable() +export default class ContainerConfigService { + private readonly logger = new Logger(ContainerConfigService.name) + + constructor( + private readonly prisma: PrismaService, + private readonly mapper: ContainerMapper, + private readonly editorServices: EditorServiceProvider, + private readonly domainEventListener: ContainerConfigDomainEventListener, + private readonly events: EventEmitter2, + ) {} + + async checkConfigIsInTeam(teamSlug: string, configId: string, identity: Identity): Promise { + const teamWhere: Prisma.TeamWhereInput = { + slug: teamSlug, + users: { + some: { + userId: identity.id, + }, + }, + } + + const versionWhere: Prisma.VersionWhereInput = { + project: { + team: teamWhere, + }, + } + + const deploymentWhere: Prisma.DeploymentWhereInput = { + version: versionWhere, + } + + const configs = await this.prisma.containerConfig.count({ + where: { + id: configId, + OR: [ + { + image: { + version: versionWhere, + }, + }, + { + instance: { + deployment: deploymentWhere, + }, + }, + { + deployment: deploymentWhere, + }, + { + configBundle: { + team: teamWhere, + }, + }, + ], + }, + }) + + return configs > 0 + } + + subscribeToDomainEvents(configId: string): Observable { + return this.domainEventListener.watchEvents(configId).pipe( + map(it => this.transformDomainEventToWsMessage(it)), + filter(it => !!it), + ) + } + + async patchConfig(configId: string, message: PatchConfigMessage, identity: Identity): Promise { + const mutable = await this.checkMutability(configId) + if (!mutable) { + throw new CruxBadRequestException({ + message: 'Container config is immutable', + property: 'configId', + value: configId, + }) + } + + const data: ContainerConfigData = message.config ?? {} + if (message.resetSection) { + data[message.resetSection] = null + } + + await this.prisma.containerConfig.update({ + where: { + id: configId, + }, + data: { + ...this.mapper.configDataToDbPatch(data), + updatedAt: new Date(), + updatedBy: identity.id, + }, + }) + + await this.events.emitAsync(CONTAINER_CONFIG_EVENT_UPDATE, { + id: configId, + patch: data, + } as ContainerConfigUpdatedEvent) + + return { + id: configId, + ...data, + } + } + + async onEditorJoined( + configId: string, + clientToken: string, + identity: Identity, + ): Promise<[EditorMessage, EditorMessage[]]> { + const editors = await this.editorServices.getOrCreateService(configId) + + const me = editors.onClientJoin(clientToken, identity) + + return [me, editors.getEditors()] + } + + async onEditorLeft(configId: string, clientToken: string): Promise { + const editors = await this.editorServices.getOrCreateService(configId) + const message = editors.onClientLeft(clientToken) + + if (editors.editorCount < 1) { + this.logger.verbose(`All editors left removing ${configId}`) + this.editorServices.free(configId) + } + + return message + } + + private async checkMutability(configId: string): Promise { + const deploymentSelect: Prisma.DeploymentSelect = { + status: true, + version: { + select: { + type: true, + }, + }, + } + + const config = await this.prisma.containerConfig.findUnique({ + where: { + id: configId, + }, + select: { + type: true, + image: { + select: { + version: { + select: { + id: true, + type: true, + children: { + select: { + versionId: true, + }, + }, + deployments: { + select: { + status: true, + }, + }, + }, + }, + }, + }, + instance: { select: { deployment: { select: deploymentSelect } } }, + deployment: { select: deploymentSelect }, + configBundle: {}, + }, + }) + + switch (config.type) { + case 'image': + return checkVersionMutability(config.image.version) + case 'deployment': + return checkDeploymentMutability(config.deployment.status, config.deployment.version.type) + case 'instance': + return checkDeploymentMutability(config.instance.deployment.status, config.instance.deployment.version.type) + case 'configBundle': + return true + default: + return false + } + } + + private transformDomainEventToWsMessage(ev: DomainEvent): WsMessage { + switch (ev.type) { + case CONTAINER_CONFIG_EVENT_UPDATE: + return { + type: WS_TYPE_CONFIG_UPDATED, + data: this.mapper.configUpdatedEventToMessage(ev.event as ContainerConfigUpdatedEvent), + } + default: { + this.logger.error(`Unhandled domain event ${ev.type}`) + return null + } + } + } +} diff --git a/web/crux/src/app/config.bundle/config.bundle.ws.gateway.ts b/web/crux/src/app/container/container-config.ws.gateway.ts similarity index 67% rename from web/crux/src/app/config.bundle/config.bundle.ws.gateway.ts rename to web/crux/src/app/container/container-config.ws.gateway.ts index f7da49363d..6865086902 100644 --- a/web/crux/src/app/config.bundle/config.bundle.ws.gateway.ts +++ b/web/crux/src/app/container/container-config.ws.gateway.ts @@ -1,5 +1,6 @@ import { SubscribeMessage, WebSocketGateway } from '@nestjs/websockets' import { Identity } from '@ory/kratos-client' +import { takeUntil } from 'rxjs' import { AuditLogLevel } from 'src/decorators/audit-logger.decorator' import { WsAuthorize, WsClient, WsMessage, WsSubscribe, WsSubscription, WsUnsubscribe } from 'src/websockets/common' import SocketClient from 'src/websockets/decorators/ws.client.decorator' @@ -26,48 +27,47 @@ import { } from '../editor/editor.message' import EditorServiceProvider from '../editor/editor.service.provider' import { IdentityFromSocket } from '../token/jwt-auth.guard' -import { PatchConfigBundleDto } from './config.bundle.dto' -import { - ConfigBundleEnvUpdatedMessage, - PatchConfigBundleEnvMessage, - WS_TYPE_CONFIG_BUNDLE_UPDATED, - WS_TYPE_PATCH_CONFIG_BUNDLE, - WS_TYPE_PATCH_RECEIVED, -} from './config.bundle.message' -import ConfigBundleService from './config.bundle.service' +import { PatchConfigMessage, WS_TYPE_PATCH_CONFIG, WS_TYPE_PATCH_RECEIVED } from './container-config.message' +import ContainerConfigService from './container-config.service' const TeamSlug = () => WsParam('teamSlug') -const ConfigBundleId = () => WsParam('configBundleId') +const ConfigId = () => WsParam('configId') @WebSocketGateway({ - namespace: ':teamSlug/config-bundles/:configBundleId', + namespace: ':teamSlug/container-configurations/:configId', }) @UseGlobalWsFilters() @UseGlobalWsGuards() @UseGlobalWsInterceptors() -export default class ConfigBundleWebSocketGateway { +export default class ContainerConfigWebSocketGateway { constructor( - private readonly service: ConfigBundleService, + private readonly service: ContainerConfigService, private readonly editorServices: EditorServiceProvider, ) {} @WsAuthorize() async onAuthorize( @TeamSlug() teamSlug: string, - @ConfigBundleId() configBundleId: string, + @ConfigId() configId: string, @IdentityFromSocket() identity: Identity, ): Promise { - return await this.service.checkConfigBundleIsInTeam(teamSlug, configBundleId, identity) + return await this.service.checkConfigIsInTeam(teamSlug, configId, identity) } @WsSubscribe() async onSubscribe( @SocketClient() client: WsClient, - @ConfigBundleId() configBundleId: string, + @ConfigId() configId: string, @IdentityFromSocket() identity, @SocketSubscription() subscription: WsSubscription, ): Promise> { - const [me, editors] = await this.service.onEditorJoined(configBundleId, client.token, identity) + const [me, editors] = await this.service.onEditorJoined(configId, client.token, identity) + + this.service + .subscribeToDomainEvents(configId) + .pipe(takeUntil(subscription.getCompleter(client.token))) + .subscribe(message => subscription.sendToAll(message)) + subscription.sendToAllExcept(client, { type: WS_TYPE_EDITOR_JOINED, data: me, @@ -85,35 +85,25 @@ export default class ConfigBundleWebSocketGateway { @WsUnsubscribe() async onUnsubscribe( @SocketClient() client: WsClient, - @ConfigBundleId() configBundleId: string, + @ConfigId() configId: string, @SocketSubscription() subscription: WsSubscription, ): Promise { - const data = await this.service.onEditorLeft(configBundleId, client.token) + const data = await this.service.onEditorLeft(configId, client.token) const message: WsMessage = { type: WS_TYPE_EDITOR_LEFT, data, } + subscription.sendToAllExcept(client, message) } - @SubscribeMessage(WS_TYPE_PATCH_CONFIG_BUNDLE) - async patchConfigBundleEnvironment( - @ConfigBundleId() configBundleId: string, - @SocketMessage() message: PatchConfigBundleEnvMessage, + @SubscribeMessage(WS_TYPE_PATCH_CONFIG) + async patchConfig( + @ConfigId() configId: string, + @SocketMessage() message: PatchConfigMessage, @IdentityFromSocket() identity: Identity, - @SocketClient() client: WsClient, - @SocketSubscription() subscription: WsSubscription, ): Promise> { - const cruxReq: PatchConfigBundleDto = { - ...message, - } - - await this.service.patchConfigBundle(configBundleId, cruxReq, identity) - - subscription.sendToAllExcept(client, { - type: WS_TYPE_CONFIG_BUNDLE_UPDATED, - data: message, - } as WsMessage) + await this.service.patchConfig(configId, message, identity) return { type: WS_TYPE_PATCH_RECEIVED, @@ -125,11 +115,11 @@ export default class ConfigBundleWebSocketGateway { @SubscribeMessage(WS_TYPE_FOCUS_INPUT) async onFocusInput( @SocketClient() client: WsClient, - @ConfigBundleId() configBundleId: string, + @ConfigId() configId: string, @SocketMessage() message: InputFocusMessage, @SocketSubscription() subscription: WsSubscription, ): Promise { - const editors = await this.editorServices.getService(configBundleId) + const editors = await this.editorServices.getService(configId) if (!editors) { return } @@ -148,11 +138,11 @@ export default class ConfigBundleWebSocketGateway { @SubscribeMessage(WS_TYPE_BLUR_INPUT) async onBlurInput( @SocketClient() client: WsClient, - @ConfigBundleId() configBundleId: string, + @ConfigId() configId: string, @SocketMessage() message: InputFocusMessage, @SocketSubscription() subscription: WsSubscription, ): Promise { - const editors = await this.editorServices.getService(configBundleId) + const editors = await this.editorServices.getService(configId) if (!editors) { return } diff --git a/web/crux/src/app/container/container.const.ts b/web/crux/src/app/container/container.const.ts new file mode 100644 index 0000000000..2eefc9f9ef --- /dev/null +++ b/web/crux/src/app/container/container.const.ts @@ -0,0 +1,45 @@ +export const COMMON_CONFIG_PROPERTIES = [ + 'name', + 'environment', + 'secrets', + 'routing', + 'expose', + 'user', + 'tty', + 'configContainer', + 'ports', + 'portRanges', + 'volumes', + 'commands', + 'args', + 'initContainers', + 'storage', +] as const + +export const CRANE_CONFIG_PROPERTIES = [ + 'deploymentStrategy', + 'customHeaders', + 'proxyHeaders', + 'useLoadBalancer', + 'extraLBAnnotations', + 'healthCheckConfig', + 'resourceConfig', + 'labels', + 'annotations', +] as const + +export const DAGENT_CONFIG_PROPERTIES = [ + 'logConfig', + 'restartPolicy', + 'networkMode', + 'networks', + 'dockerLabels', +] as const + +export const ALL_CONFIG_PROPERTIES = [ + ...COMMON_CONFIG_PROPERTIES, + ...CRANE_CONFIG_PROPERTIES, + ...DAGENT_CONFIG_PROPERTIES, +] as const + +export type ContainerConfigProperty = (typeof ALL_CONFIG_PROPERTIES)[number] diff --git a/web/crux/src/app/container/container.dto.ts b/web/crux/src/app/container/container.dto.ts index 57911d52fb..f8a5b2bf37 100644 --- a/web/crux/src/app/container/container.dto.ts +++ b/web/crux/src/app/container/container.dto.ts @@ -1,4 +1,4 @@ -import { ApiProperty, PartialType } from '@nestjs/swagger' +import { ApiProperty, OmitType, PartialType } from '@nestjs/swagger' import { IsBoolean, IsIn, @@ -31,6 +31,9 @@ import { } from 'src/domain/container' import { UID_MAX } from 'src/shared/const' +export const CONTAINER_CONFIG_TYPE_VALUES = ['image', 'instance', 'deployment', 'config-bundle'] as const +export type ContainerConfigTypeDto = (typeof CONTAINER_CONFIG_TYPE_VALUES)[number] + export class UniqueKeyDto { @IsUUID() id: string @@ -304,9 +307,18 @@ export class ExpectedContainerStateDto { } export class ContainerConfigDto { + @IsString() + id: string + + @ApiProperty({ enum: CONTAINER_CONFIG_TYPE_VALUES }) + @IsIn(CONTAINER_CONFIG_TYPE_VALUES) + @IsOptional() + type: ContainerConfigTypeDto + // common @IsString() - name: string + @IsOptional() + name?: string @IsOptional() @ValidateNested({ each: true }) @@ -322,7 +334,8 @@ export class ContainerConfigDto { @ApiProperty({ enum: CONTAINER_EXPOSE_STRATEGY_VALUES }) @IsIn(CONTAINER_EXPOSE_STRATEGY_VALUES) - expose: ContainerExposeStrategy + @IsOptional() + expose?: ContainerExposeStrategy @IsOptional() @IsInt() @@ -335,7 +348,8 @@ export class ContainerConfigDto { workingDirectory?: string @IsBoolean() - tty: boolean + @IsOptional() + tty?: boolean @IsOptional() @ValidateNested() @@ -380,11 +394,13 @@ export class ContainerConfigDto { @ApiProperty({ enum: CONTAINER_RESTART_POLICY_TYPE_VALUES }) @IsIn(CONTAINER_RESTART_POLICY_TYPE_VALUES) - restartPolicy: ContainerRestartPolicyType + @IsOptional() + restartPolicy?: ContainerRestartPolicyType @ApiProperty({ enum: CONTAINER_NETWORK_MODE_VALUES }) @IsIn(CONTAINER_NETWORK_MODE_VALUES) - networkMode: ContainerNetworkMode + @IsOptional() + networkMode?: ContainerNetworkMode @IsOptional() @ValidateNested({ each: true }) @@ -401,17 +417,20 @@ export class ContainerConfigDto { // crane @ApiProperty({ enum: CONTAINER_DEPLOYMENT_STRATEGY_VALUES }) @IsIn(CONTAINER_DEPLOYMENT_STRATEGY_VALUES) - deploymentStrategy: ContainerDeploymentStrategyType + @IsOptional() + deploymentStrategy?: ContainerDeploymentStrategyType @IsOptional() @ValidateNested({ each: true }) customHeaders?: UniqueKeyDto[] @IsBoolean() - proxyHeaders: boolean + @IsOptional() + proxyHeaders?: boolean @IsBoolean() - useLoadBalancer: boolean + @IsOptional() + useLoadBalancer?: boolean @IsOptional() @ValidateNested({ each: true }) @@ -438,7 +457,11 @@ export class ContainerConfigDto { metrics?: MetricsDto } -export class PartialContainerConfigDto extends PartialType(ContainerConfigDto) {} +export class ConcreteContainerConfigDto extends OmitType(PartialType(ContainerConfigDto), ['secrets']) { + @IsOptional() + @ValidateNested({ each: true }) + secrets?: UniqueSecretKeyValueDto[] +} export class ContainerIdentifierDto { @IsString() diff --git a/web/crux/src/app/container/container.mapper.ts b/web/crux/src/app/container/container.mapper.ts index 3131051e2f..27ebf5a1ce 100644 --- a/web/crux/src/app/container/container.mapper.ts +++ b/web/crux/src/app/container/container.mapper.ts @@ -1,26 +1,31 @@ import { Injectable } from '@nestjs/common' -import { ContainerConfig } from '@prisma/client' -import { - ContainerConfigData, - InstanceContainerConfigData, - MergedContainerConfigData, - Metrics, - UniqueKeyValue, - UniqueSecretKey, - UniqueSecretKeyValue, -} from 'src/domain/container' -import { toPrismaJson } from 'src/domain/utils' -import { ContainerConfigDto, PartialContainerConfigDto, UniqueKeyValueDto } from './container.dto' +import { ContainerConfig, ContainerConfigType } from '@prisma/client' +import { ContainerConfigData } from 'src/domain/container' +import { ContainerConfigUpdatedEvent } from 'src/domain/domain-events' +import { toNullableBoolean, toNullableNumber, toPrismaJson } from 'src/domain/utils' +import { ConfigUpdatedMessage } from './container-config.message' +import { ContainerConfigDto, ContainerConfigTypeDto } from './container.dto' @Injectable() export default class ContainerMapper { - uniqueKeyValueDtoToDb(it: UniqueKeyValueDto): UniqueKeyValue { - return it + typeToDto(type: ContainerConfigType): ContainerConfigTypeDto { + switch (type) { + case 'configBundle': + return 'config-bundle' + default: + return type + } } - configDataToDto(config: ContainerConfigData): ContainerConfigDto { + configDataToDto(id: string, type: ContainerConfigType, config: ContainerConfigData): ContainerConfigDto { + if (!config) { + return null + } + return { ...config, + id, + type: this.typeToDto(type), capabilities: null, storage: !config.storageSet ? null @@ -32,51 +37,62 @@ export default class ContainerMapper { } } - configDtoToConfigData(current: ContainerConfigData, patch: PartialContainerConfigDto): ContainerConfigData { - const storagePatch = - 'storage' in patch - ? { - storageSet: !!patch.storage?.storageId, - storageId: patch.storage?.storageId ?? null, - storageConfig: patch.storage?.storageId - ? { - path: patch.storage.path, - bucket: patch.storage.bucket, - } - : null, - } - : undefined - - return { + configDtoToConfigData(current: ContainerConfigData, patch: ContainerConfigDto): ContainerConfigData { + let result: ContainerConfigData = { ...current, ...patch, - capabilities: undefined, // TODO (@m8vago, @nandor-magyar): Remove this line, when capabilites are ready - annotations: !patch.annotations - ? current.annotations - : { - ...(current.annotations ?? {}), - ...patch.annotations, - }, - labels: !patch.labels - ? current.labels - : { - ...(current.labels ?? {}), - ...patch.labels, - }, - ...storagePatch, } + + if ('storage' in patch) { + result = { + ...result, + storageSet: true, + storageId: patch.storage?.storageId ?? null, + storageConfig: patch.storage?.storageId + ? { + path: patch.storage.path, + bucket: patch.storage.bucket, + } + : null, + } + } + + if ('annotations' in patch) { + result = { + ...result, + annotations: { + ...(current.annotations ?? {}), + ...patch.annotations, + }, + } + } + + if ('labels' in patch) { + result = { + ...result, + labels: { + ...(current.labels ?? {}), + ...patch.labels, + }, + } + } + + return result } - configDataToDb(config: Partial): Omit { + dbConfigToCreateConfigStatement( + config: Omit, + ): Omit { return { - name: config.name ?? undefined, - expose: config.expose ?? undefined, + type: config.type, + // common + name: config.name ?? null, + expose: config.expose ?? null, routing: toPrismaJson(config.routing), - configContainer: toPrismaJson(config.configContainer), - // Set user to the given value, if not null or use 0 if specifically 0, otherwise set to default -1 - user: config.user ?? (config.user === 0 ? 0 : -1), - workingDirectory: config.workingDirectory ?? undefined, - tty: config.tty !== null ? config.tty : false, + configContainer: toPrismaJson(config.configContainer) ?? null, + user: toNullableNumber(config.user), + workingDirectory: config.workingDirectory ?? null, + tty: toNullableBoolean(config.tty), ports: toPrismaJson(config.ports), portRanges: toPrismaJson(config.portRanges), volumes: toPrismaJson(config.volumes), @@ -86,23 +102,23 @@ export default class ContainerMapper { secrets: toPrismaJson(config.secrets), initContainers: toPrismaJson(config.initContainers), logConfig: toPrismaJson(config.logConfig), - storageSet: config.storageSet ?? undefined, - storageId: config.storageId ?? undefined, + storageSet: toNullableBoolean(config.storageSet), + storageId: config.storageId ?? null, storageConfig: toPrismaJson(config.storageConfig), // dagent - restartPolicy: config.restartPolicy ?? undefined, - networkMode: config.networkMode ?? undefined, + restartPolicy: config.restartPolicy ?? null, + networkMode: config.networkMode ?? null, networks: toPrismaJson(config.networks), dockerLabels: toPrismaJson(config.dockerLabels), expectedState: toPrismaJson(config.expectedState), // crane - deploymentStrategy: config.deploymentStrategy ?? undefined, + deploymentStrategy: config.deploymentStrategy ?? null, healthCheckConfig: toPrismaJson(config.healthCheckConfig), resourceConfig: toPrismaJson(config.resourceConfig), - proxyHeaders: config.proxyHeaders !== null ? config.proxyHeaders : false, - useLoadBalancer: config.useLoadBalancer !== null ? config.useLoadBalancer : false, + proxyHeaders: toNullableBoolean(config.proxyHeaders), + useLoadBalancer: toNullableBoolean(config.useLoadBalancer), customHeaders: toPrismaJson(config.customHeaders), extraLBAnnotations: toPrismaJson(config.extraLBAnnotations), capabilities: toPrismaJson(config.capabilities), @@ -112,94 +128,56 @@ export default class ContainerMapper { } } - mergeSecrets(instanceSecrets: UniqueSecretKeyValue[], imageSecrets: UniqueSecretKey[]): UniqueSecretKeyValue[] { - imageSecrets = imageSecrets ?? [] - instanceSecrets = instanceSecrets ?? [] - - const overriddenIds: Set = new Set(instanceSecrets?.map(it => it.id)) - - const missing: UniqueSecretKeyValue[] = imageSecrets - .filter(it => !overriddenIds.has(it.id)) - .map(it => ({ - ...it, - value: '', - encrypted: false, - publicKey: null, - })) + configDataToDbPatch(config: ContainerConfigData): ContainerConfigDbPatch { + return { + name: 'name' in config ? config.name ?? null : undefined, + expose: 'expose' in config ? config.expose ?? null : undefined, + routing: 'routing' in config ? toPrismaJson(config.routing) : undefined, + configContainer: 'configContainer' in config ? toPrismaJson(config.configContainer) : undefined, + user: 'user' in config ? toNullableNumber(config.user) : undefined, + workingDirectory: 'workingDirectory' in config ? config.workingDirectory ?? null : undefined, + tty: 'tty' in config ? toNullableBoolean(config.tty) : undefined, + ports: 'ports' in config ? toPrismaJson(config.ports) : undefined, + portRanges: 'portRanges' in config ? toPrismaJson(config.portRanges) : undefined, + volumes: 'volumes' in config ? toPrismaJson(config.volumes) : undefined, + commands: 'commands' in config ? toPrismaJson(config.commands) : undefined, + args: 'args' in config ? toPrismaJson(config.args) : undefined, + environment: 'environment' in config ? toPrismaJson(config.environment) : undefined, + secrets: 'secrets' in config ? toPrismaJson(config.secrets) : undefined, + initContainers: 'initContainers' in config ? toPrismaJson(config.initContainers) : undefined, + logConfig: 'logConfig' in config ? toPrismaJson(config.logConfig) : undefined, + storageSet: 'storageSet' in config ? toNullableBoolean(config.storageSet) : undefined, + storageId: 'storageId' in config ? config.storageId ?? null : undefined, + storageConfig: 'storageConfig' in config ? toPrismaJson(config.storageConfig) : undefined, - return [...missing, ...instanceSecrets] - } + // dagent + restartPolicy: 'restartPolicy' in config ? config.restartPolicy ?? null : undefined, + networkMode: 'networkMode' in config ? config.networkMode ?? null : undefined, + networks: 'networks' in config ? toPrismaJson(config.networks) : undefined, + dockerLabels: 'dockerLabels' in config ? toPrismaJson(config.dockerLabels) : undefined, + expectedState: 'expectedState' in config ? toPrismaJson(config.expectedState) : undefined, - mergeMetrics(instance: Metrics, image: Metrics): Metrics { - if (!instance) { - return image?.enabled ? image : null + // crane + deploymentStrategy: 'deploymentStrategy' in config ? config.deploymentStrategy ?? null : undefined, + healthCheckConfig: 'healthCheckConfig' in config ? toPrismaJson(config.healthCheckConfig) : undefined, + resourceConfig: 'resourceConfig' in config ? toPrismaJson(config.resourceConfig) : undefined, + proxyHeaders: 'proxyHeaders' in config ? toNullableBoolean(config.proxyHeaders) : undefined, + useLoadBalancer: 'useLoadBalancer' in config ? toNullableBoolean(config.useLoadBalancer) : undefined, + customHeaders: 'customHeaders' in config ? toPrismaJson(config.customHeaders) : undefined, + extraLBAnnotations: 'extraLBAnnotations' in config ? toPrismaJson(config.extraLBAnnotations) : undefined, + capabilities: 'capabilities' in config ? toPrismaJson(config.capabilities) : undefined, + annotations: 'annotations' in config ? toPrismaJson(config.annotations) : undefined, + labels: 'labels' in config ? toPrismaJson(config.labels) : undefined, + metrics: 'metrics' in config ? toPrismaJson(config.metrics) : undefined, } - - return instance } - mergeConfigs(image: ContainerConfigData, instance: InstanceContainerConfigData): MergedContainerConfigData { + configUpdatedEventToMessage(event: ContainerConfigUpdatedEvent): ConfigUpdatedMessage { return { - // common - name: instance.name ?? image.name, - environment: instance.environment ?? image.environment, - secrets: this.mergeSecrets(instance.secrets, image.secrets), - user: instance.user ?? image.user, - workingDirectory: instance.workingDirectory ?? image.workingDirectory, - tty: instance.tty ?? image.tty, - portRanges: instance.portRanges ?? image.portRanges, - args: instance.args ?? image.args, - commands: instance.commands ?? image.commands, - expose: instance.expose ?? image.expose, - configContainer: instance.configContainer ?? image.configContainer, - routing: instance.routing ?? image.routing, - volumes: instance.volumes ?? image.volumes, - initContainers: instance.initContainers ?? image.initContainers, - capabilities: [], // TODO (@m8vago, @nandor-magyar): capabilities feature is still missing - ports: instance.ports ?? image.ports, - storageSet: instance.storageSet || image.storageSet, - storageId: instance.storageSet ? instance.storageId : image.storageId, - storageConfig: instance.storageSet ? instance.storageConfig : image.storageConfig, - - // crane - customHeaders: instance.customHeaders ?? image.customHeaders, - proxyHeaders: instance.proxyHeaders ?? image.proxyHeaders, - extraLBAnnotations: instance.extraLBAnnotations ?? image.extraLBAnnotations, - healthCheckConfig: instance.healthCheckConfig ?? image.healthCheckConfig, - resourceConfig: instance.resourceConfig ?? image.resourceConfig, - useLoadBalancer: instance.useLoadBalancer ?? image.useLoadBalancer, - deploymentStrategy: instance.deploymentStrategy ?? image.deploymentStrategy, - labels: - instance.labels || image.labels - ? { - deployment: instance.labels?.deployment ?? image.labels?.deployment ?? [], - service: instance.labels?.service ?? image.labels?.service ?? [], - ingress: instance.labels?.ingress ?? image.labels?.ingress ?? [], - } - : null, - annotations: - image.annotations || instance.annotations - ? { - deployment: instance.annotations?.deployment ?? image.annotations?.deployment ?? [], - service: instance.annotations?.service ?? image.annotations?.service ?? [], - ingress: instance.annotations?.ingress ?? image.annotations?.ingress ?? [], - } - : null, - metrics: this.mergeMetrics(instance.metrics, image.metrics), - - // dagent - logConfig: instance.logConfig ?? image.logConfig, - networkMode: instance.networkMode ?? image.networkMode, - restartPolicy: instance.restartPolicy ?? image.restartPolicy, - networks: instance.networks ?? image.networks, - dockerLabels: instance.dockerLabels ?? image.dockerLabels, - expectedState: - !!image.expectedState || !!instance.expectedState - ? { - ...image.expectedState, - ...instance.expectedState, - } - : null, + ...event.patch, + id: event.id, } } } + +export type ContainerConfigDbPatch = Omit diff --git a/web/crux/src/app/container/container.module.ts b/web/crux/src/app/container/container.module.ts index 6a2bb27452..e47bb7eb42 100644 --- a/web/crux/src/app/container/container.module.ts +++ b/web/crux/src/app/container/container.module.ts @@ -1,10 +1,22 @@ import { Module } from '@nestjs/common' +import PrismaService from 'src/services/prisma.service' +import AuditLoggerModule from '../audit.logger/audit.logger.module' +import EditorModule from '../editor/editor.module' +import ContainerConfigDomainEventListener from './container-config.domain-event.listener' +import ContainerConfigService from './container-config.service' +import ContainerConfigWebSocketGateway from './container-config.ws.gateway' import ContainerMapper from './container.mapper' @Module({ - imports: [], - exports: [ContainerMapper], + imports: [AuditLoggerModule, EditorModule], + exports: [ContainerMapper, ContainerConfigService], controllers: [], - providers: [ContainerMapper], + providers: [ + PrismaService, + ContainerMapper, + ContainerConfigService, + ContainerConfigDomainEventListener, + ContainerConfigWebSocketGateway, + ], }) export default class ContainerModule {} diff --git a/web/crux/src/app/deploy/deploy.domain-event.listener.ts b/web/crux/src/app/deploy/deploy.domain-event.listener.ts new file mode 100644 index 0000000000..c7d657cb2b --- /dev/null +++ b/web/crux/src/app/deploy/deploy.domain-event.listener.ts @@ -0,0 +1,100 @@ +import { Injectable } from '@nestjs/common' +import { OnEvent } from '@nestjs/event-emitter' +import { filter, Observable, Subject } from 'rxjs' +import { + DEPLOYMENT_EVENT_CONFIG_BUNDLES_UPDATE, + DEPLOYMENT_EVENT_INSTACE_CREATE, + DEPLOYMENT_EVENT_INSTACE_DELETE, + DeploymentConfigBundlesUpdatedEvent, + DeploymentEditEvent, + IMAGE_EVENT_ADD, + IMAGE_EVENT_DELETE, + ImageDeletedEvent, + ImagesAddedEvent, + InstanceDeletedEvent, + InstancesCreatedEvent, +} from 'src/domain/domain-events' +import PrismaService from 'src/services/prisma.service' +import { DomainEvent } from 'src/shared/domain-event' + +@Injectable() +export default class DeployDomainEventListener { + private deploymentEvents = new Subject>() + + constructor(private prisma: PrismaService) {} + + watchEvents(deploymentId: string): Observable> { + return this.deploymentEvents.pipe(filter(it => it.event.deploymentId === deploymentId)) + } + + @OnEvent(IMAGE_EVENT_ADD, { async: true }) + async onImagesAdded(event: ImagesAddedEvent) { + const deployments = await this.prisma.deployment.findMany({ + select: { + id: true, + }, + where: { + versionId: event.versionId, + }, + }) + + const createEvents: InstancesCreatedEvent[] = await Promise.all( + deployments.map(async deployment => { + const instances = await Promise.all( + event.images.map(it => + this.prisma.instance.create({ + select: { + configId: true, + id: true, + image: { + include: { + config: true, + registry: true, + }, + }, + }, + data: { + deployment: { connect: { id: deployment.id } }, + image: { connect: { id: it.id } }, + config: { create: { type: 'instance' } }, + }, + }), + ), + ) + + return { + deploymentId: deployment.id, + instances, + } + }), + ) + + this.sendEditEvents(DEPLOYMENT_EVENT_INSTACE_CREATE, createEvents) + } + + @OnEvent(IMAGE_EVENT_DELETE) + onImageDeleted(event: ImageDeletedEvent) { + const deleteEvents: InstanceDeletedEvent[] = event.instances + + this.sendEditEvents(DEPLOYMENT_EVENT_INSTACE_DELETE, deleteEvents) + } + + @OnEvent(DEPLOYMENT_EVENT_CONFIG_BUNDLES_UPDATE) + onConfigBundlesUpdated(event: DeploymentConfigBundlesUpdatedEvent) { + const editEvent: DomainEvent = { + type: DEPLOYMENT_EVENT_CONFIG_BUNDLES_UPDATE, + event, + } + + this.deploymentEvents.next(editEvent) + } + + private sendEditEvents(type: string, events: DeploymentEditEvent[]) { + events.forEach(it => + this.deploymentEvents.next({ + type, + event: it, + }), + ) + } +} diff --git a/web/crux/src/app/deploy/deploy.dto.ts b/web/crux/src/app/deploy/deploy.dto.ts index 217e7b49d5..14dc982462 100644 --- a/web/crux/src/app/deploy/deploy.dto.ts +++ b/web/crux/src/app/deploy/deploy.dto.ts @@ -1,5 +1,5 @@ -import { ApiProperty, OmitType, PartialType } from '@nestjs/swagger' -import { Deployment, DeploymentToken, Instance, InstanceContainerConfig, Node, Project, Version } from '@prisma/client' +import { ApiProperty } from '@nestjs/swagger' +import { Deployment, Node, Project, Version } from '@prisma/client' import { Type } from 'class-transformer' import { IsBoolean, @@ -8,7 +8,6 @@ import { IsInt, IsJWT, IsNumber, - IsObject, IsOptional, IsString, IsUUID, @@ -20,15 +19,9 @@ import { CONTAINER_STATE_VALUES, ContainerState } from 'src/domain/container' import { PaginatedList, PaginationQuery } from 'src/shared/dtos/paginating' import { BasicProperties } from '../../shared/dtos/shared.dto' import { AuditDto } from '../audit/audit.dto' -import { - ContainerConfigDto, - ContainerIdentifierDto, - UniqueKeyValueDto, - UniqueSecretKeyValueDto, -} from '../container/container.dto' +import { ConfigBundleDetailsDto } from '../config.bundle/config.bundle.dto' +import { ConcreteContainerConfigDto, ContainerIdentifierDto } from '../container/container.dto' import { ImageDto } from '../image/image.dto' -import { ImageEvent } from '../image/image.event' -import { ImageDetails } from '../image/image.mapper' import { BasicNodeDto, BasicNodeWithStatus } from '../node/node.dto' import { BasicProjectDto } from '../project/project.dto' import { BasicVersionDto } from '../version/version.dto' @@ -36,8 +29,6 @@ import { BasicVersionDto } from '../version/version.dto' const DEPLOYMENT_STATUS_VALUES = ['preparing', 'in-progress', 'successful', 'failed', 'obsolete'] as const export type DeploymentStatusDto = (typeof DEPLOYMENT_STATUS_VALUES)[number] -export type EnvironmentToConfigBundleNameMap = Record - export class BasicDeploymentDto { @IsUUID() id: string @@ -84,12 +75,6 @@ export class DeploymentWithBasicNodeDto extends BasicDeploymentDto { node: BasicNodeWithStatus } -export class InstanceContainerConfigDto extends OmitType(PartialType(ContainerConfigDto), ['secrets']) { - @IsOptional() - @ValidateNested({ each: true }) - secrets?: UniqueSecretKeyValueDto[] -} - export class InstanceDto { @IsUUID() id: string @@ -103,7 +88,17 @@ export class InstanceDto { @IsOptional() @ValidateNested() - config?: InstanceContainerConfigDto | null + config?: ConcreteContainerConfigDto +} + +export class PatchInstanceDto { + @IsString() + @IsOptional() + tag?: string | null + + @IsOptional() + @ValidateNested() + config?: ConcreteContainerConfigDto | null } export class DeploymentTokenDto { @@ -143,11 +138,9 @@ export class DeploymentTokenCreatedDto extends DeploymentTokenDto { } export class DeploymentDetailsDto extends DeploymentDto { - @ValidateNested({ each: true }) - environment: UniqueKeyValueDto[] - - @IsObject() - configBundleEnvironment: EnvironmentToConfigBundleNameMap + @ValidateNested() + @IsOptional() + config?: ConcreteContainerConfigDto @IsString() @IsOptional() @@ -165,7 +158,7 @@ export class DeploymentDetailsDto extends DeploymentDto { @IsString({ each: true }) @IsOptional() - configBundleIds: string[] + configBundles: ConfigBundleDetailsDto[] } export class CreateDeploymentDto { @@ -186,31 +179,18 @@ export class CreateDeploymentDto { note?: string | null } -export class PatchDeploymentDto { +export class UpdateDeploymentDto { @IsString() @IsOptional() note?: string | null @IsString() - @IsOptional() - prefix?: string | null + prefix: string @IsBoolean() - @IsOptional() - protected?: boolean - - @IsOptional() - @ValidateNested({ each: true }) - environment?: UniqueKeyValueDto[] | null - - @IsString({ each: true }) - @IsOptional() - configBundleIds?: string[] -} + protected: boolean -export class PatchInstanceDto { - @ValidateNested() - config: InstanceContainerConfigDto + configBundles: string[] } export class CopyDeploymentDto { @@ -324,11 +304,6 @@ export class StartDeploymentDto { instances?: string[] } -export type DeploymentImageEvent = ImageEvent & { - deploymentIds?: string[] - instances?: InstanceDetails[] -} - export type DeploymentWithNode = Deployment & { node: Pick } @@ -338,18 +313,3 @@ export type DeploymentWithNodeVersion = DeploymentWithNode & { project: Pick } } - -export type InstanceDetails = Instance & { - image: ImageDetails - config?: InstanceContainerConfig -} - -export type DeploymentDetails = DeploymentWithNodeVersion & { - tokens: Pick[] - instances: InstanceDetails[] - configBundles: { - configBundle: { - id: string - } - }[] -} diff --git a/web/crux/src/app/deploy/deploy.http.controller.ts b/web/crux/src/app/deploy/deploy.http.controller.ts index 5bd22a191b..e0d4e4f836 100644 --- a/web/crux/src/app/deploy/deploy.http.controller.ts +++ b/web/crux/src/app/deploy/deploy.http.controller.ts @@ -40,9 +40,9 @@ import { DeploymentTokenCreatedDto, InstanceDto, InstanceSecretsDto, - PatchDeploymentDto, PatchInstanceDto, StartDeploymentDto, + UpdateDeploymentDto, } from './deploy.dto' import DeployService from './deploy.service' import DeployCreateTeamAccessGuard from './guards/deploy.create.team-access.guard' @@ -194,10 +194,10 @@ export default class DeployHttpController { async patchDeployment( @TeamSlug() _: string, @DeploymentId() deploymentId: string, - @Body() request: PatchDeploymentDto, + @Body() request: UpdateDeploymentDto, @IdentityFromRequest() identity: Identity, ): Promise { - await this.service.patchDeployment(deploymentId, request, identity) + await this.service.updateDeployment(deploymentId, request, identity) } @Patch(`${ROUTE_DEPLOYMENT_ID}/${ROUTE_INSTANCES}/${ROUTE_INSTANCE_ID}`) diff --git a/web/crux/src/app/deploy/deploy.mapper.spec.ts b/web/crux/src/app/deploy/deploy.mapper.spec.ts index c2daf48fc3..bb23bcf771 100644 --- a/web/crux/src/app/deploy/deploy.mapper.spec.ts +++ b/web/crux/src/app/deploy/deploy.mapper.spec.ts @@ -1,11 +1,12 @@ import { DeploymentStatusEnum, NodeTypeEnum, ProjectTypeEnum, Storage, VersionTypeEnum } from '.prisma/client' import { Test, TestingModule } from '@nestjs/testing' -import { ContainerConfigData, InstanceContainerConfigData, MergedContainerConfigData } from 'src/domain/container' +import { ConcreteContainerConfigData, ContainerConfigData } from 'src/domain/container' import { CommonContainerConfig, DagentContainerConfig, ImportContainer } from 'src/grpc/protobuf/proto/agent' import { DriverType, NetworkMode, RestartPolicy } from 'src/grpc/protobuf/proto/common' import EncryptionService from 'src/services/encryption.service' import AgentService from '../agent/agent.service' import AuditMapper from '../audit/audit.mapper' +import ConfigBundleMapper from '../config.bundle/config.bundle.mapper' import ContainerMapper from '../container/container.mapper' import ImageMapper from '../image/image.mapper' import NodeMapper from '../node/node.mapper' @@ -16,7 +17,6 @@ import { DeploymentDto, DeploymentWithNodeVersion, PatchInstanceDto } from './de import DeployMapper from './deploy.mapper' describe('DeployMapper', () => { - let containerMapper: ContainerMapper = null let deployMapper: DeployMapper = null beforeEach(async () => { @@ -32,6 +32,7 @@ describe('DeployMapper', () => { NodeMapper, ImageMapper, DeployMapper, + ConfigBundleMapper, { provide: EncryptionService, useValue: jest.mocked(EncryptionService), @@ -43,7 +44,6 @@ describe('DeployMapper', () => { ], }).compile() - containerMapper = module.get(ContainerMapper) deployMapper = module.get(DeployMapper) }) @@ -268,7 +268,7 @@ describe('DeployMapper', () => { expectedState: null, } - const fullInstance: InstanceContainerConfigData = { + const fullInstance: ConcreteContainerConfigData = { name: 'instance.img', capabilities: [], deploymentStrategy: 'recreate', @@ -492,8 +492,8 @@ describe('DeployMapper', () => { expectedState: null, } - const generateUndefinedInstance = (): InstanceContainerConfigData => { - const instance: InstanceContainerConfigData = {} + const generateUndefinedInstance = (): ConcreteContainerConfigData => { + const instance: ConcreteContainerConfigData = {} Object.keys(fullImage).forEach(key => { instance[key] = undefined }) @@ -501,85 +501,6 @@ describe('DeployMapper', () => { return instance } - describe('mergeConfigs', () => { - it('should use the instance variables when available', () => { - const merged = containerMapper.mergeConfigs(fullImage, fullInstance) - - expect(merged).toEqual(fullInstance) - }) - - it('should use the image variables when instance is not available', () => { - const merged = containerMapper.mergeConfigs(fullImage, {}) - - const expected: InstanceContainerConfigData = { - ...fullImage, - secrets: [ - { - id: 'secret1', - key: 'secret1', - required: false, - encrypted: false, - value: '', - publicKey: null, - }, - ], - } - - expect(merged).toEqual(expected) - }) - - it('should use the instance only when available', () => { - const instance: InstanceContainerConfigData = { - ports: fullInstance.ports, - labels: { - deployment: [ - { - id: 'instance.labels.deployment', - key: 'instance.labels.deployment', - value: 'instance.labels.deployment', - }, - ], - }, - annotations: { - service: [ - { - id: 'instance.annotations.service', - key: 'instance.annotations.service', - value: 'instance.annotations.service', - }, - ], - }, - } - - const expected: InstanceContainerConfigData = { - ...fullImage, - ports: fullInstance.ports, - labels: { - ...fullImage.labels, - deployment: instance.labels.deployment, - }, - annotations: { - ...fullImage.annotations, - service: instance.annotations.service, - }, - secrets: [ - { - id: 'secret1', - key: 'secret1', - required: false, - encrypted: false, - value: '', - publicKey: null, - }, - ], - } - - const merged = containerMapper.mergeConfigs(fullImage, instance) - - expect(merged).toEqual(expected) - }) - }) - describe('instanceConfigToInstanceContainerConfigData', () => { it('should overwrite the specified properties only', () => { const patch: PatchInstanceDto = { @@ -676,7 +597,7 @@ describe('DeployMapper', () => { }, } - const instance: InstanceContainerConfigData = { + const instance: ConcreteContainerConfigData = { labels: fullInstance.labels, annotations: fullInstance.annotations, } @@ -725,7 +646,7 @@ describe('DeployMapper', () => { type: ProjectTypeEnum.versionless, }, }, - environment: {}, + configId: 'deployment-config-id', versionId: 'deployment-version-id', nodeId: 'deployment-node-id', tries: 1, @@ -768,7 +689,8 @@ describe('DeployMapper', () => { describe('commonConfigToAgentProto', () => { it('the function storageToImportContainer should add https by default if protocol is missing', () => { const config = deployMapper.commonConfigToAgentProto( - { + { + storageSet: true, storageId: 'test-1234', storageConfig: { path: 'test', @@ -791,7 +713,8 @@ describe('DeployMapper', () => { it('the function storageToImportContainer should leave http prefix untouched', () => { const config = deployMapper.commonConfigToAgentProto( - { + { + storageSet: true, storageId: 'test-1234', storageConfig: { path: 'test', @@ -814,7 +737,8 @@ describe('DeployMapper', () => { it('the function storageToImportContainer should add https prefix untouched', () => { const config = deployMapper.commonConfigToAgentProto( - { + { + storageSet: true, storageId: 'test-1234', storageConfig: { path: 'test', @@ -838,7 +762,7 @@ describe('DeployMapper', () => { describe('dagentConfigToAgentProto logConfig', () => { it('undefined logConfig should return no log driver', () => { - const config = deployMapper.dagentConfigToAgentProto({ + const config = deployMapper.dagentConfigToAgentProto({ networks: [], networkMode: 'host', restartPolicy: 'always', @@ -856,7 +780,7 @@ describe('DeployMapper', () => { }) it('node default driver type should return no log driver', () => { - const config = deployMapper.dagentConfigToAgentProto({ + const config = deployMapper.dagentConfigToAgentProto({ networks: [], networkMode: 'host', restartPolicy: 'always', @@ -877,7 +801,7 @@ describe('DeployMapper', () => { }) it('none driver type should return none log driver', () => { - const config = deployMapper.dagentConfigToAgentProto({ + const config = deployMapper.dagentConfigToAgentProto({ networks: [], networkMode: 'host', restartPolicy: 'always', diff --git a/web/crux/src/app/deploy/deploy.mapper.ts b/web/crux/src/app/deploy/deploy.mapper.ts index 7a658e886c..f3370bb80e 100644 --- a/web/crux/src/app/deploy/deploy.mapper.ts +++ b/web/crux/src/app/deploy/deploy.mapper.ts @@ -1,22 +1,37 @@ import { Inject, Injectable, forwardRef } from '@nestjs/common' import { + ConfigBundle, + ContainerConfig, Deployment, DeploymentEvent, DeploymentEventTypeEnum, DeploymentStatusEnum, - InstanceContainerConfig, + DeploymentStrategy, + DeploymentToken, + ExposeStrategy, + Instance, + NetworkMode, + RestartPolicy, Storage, } from '@prisma/client' import { + ConcreteContainerConfigData, ContainerConfigData, + ContainerLogDriverType, ContainerState, + ContainerVolumeType, InitContainer, - InstanceContainerConfigData, - MergedContainerConfigData, UniqueKey, UniqueKeyValue, + Volume, } from 'src/domain/container' +import { mergeMarkers, mergeSecrets } from 'src/domain/container-merge' import { deploymentLogLevelToDb, deploymentStatusToDb } from 'src/domain/deployment' +import { + DeploymentConfigBundlesUpdatedEvent, + InstanceDeletedEvent, + InstancesCreatedEvent, +} from 'src/domain/domain-events' import { CruxInternalServerErrorException } from 'src/exception/crux-exception' import { InitContainer as AgentInitContainer, @@ -25,27 +40,36 @@ import { DagentContainerConfig, ImportContainer, InstanceConfig, + Volume as ProtoVolume, } from 'src/grpc/protobuf/proto/agent' import { DeploymentStatusMessage, + DriverType, KeyValue, ListSecretsResponse, ContainerState as ProtoContainerState, DeploymentStrategy as ProtoDeploymentStrategy, ExposeStrategy as ProtoExposeStrategy, + NetworkMode as ProtoNetworkMode, + RestartPolicy as ProtoRestartPolicy, + VolumeType as ProtoVolumeType, containerStateToJSON, + driverTypeFromJSON, + networkModeFromJSON, + volumeTypeFromJSON, } from 'src/grpc/protobuf/proto/common' import EncryptionService from 'src/services/encryption.service' import AuditMapper from '../audit/audit.mapper' +import ConfigBundleMapper from '../config.bundle/config.bundle.mapper' +import { ConcreteContainerConfigDto, ContainerConfigDto } from '../container/container.dto' import ContainerMapper from '../container/container.mapper' -import ImageMapper from '../image/image.mapper' +import ImageMapper, { ImageDetails } from '../image/image.mapper' import { NodeConnectionStatus } from '../node/node.dto' import NodeMapper from '../node/node.mapper' import ProjectMapper from '../project/project.mapper' import VersionMapper from '../version/version.mapper' import { BasicDeploymentDto, - DeploymentDetails, DeploymentDetailsDto, DeploymentDto, DeploymentEventDto, @@ -56,28 +80,31 @@ import { DeploymentWithBasicNodeDto, DeploymentWithNode, DeploymentWithNodeVersion, - EnvironmentToConfigBundleNameMap, - InstanceContainerConfigDto, - InstanceDetails, InstanceDto, InstanceSecretsDto, } from './deploy.dto' -import { DeploymentEventMessage } from './deploy.message' +import { + DeploymentBundlesUpdatedMessage, + DeploymentEventMessage, + InstanceDeletedMessage, + InstancesAddedMessage, +} from './deploy.message' @Injectable() export default class DeployMapper { constructor( @Inject(forwardRef(() => ImageMapper)) - private imageMapper: ImageMapper, - private containerMapper: ContainerMapper, + private readonly imageMapper: ImageMapper, + private readonly containerMapper: ContainerMapper, @Inject(forwardRef(() => ProjectMapper)) - private projectMapper: ProjectMapper, - private auditMapper: AuditMapper, + private readonly projectMapper: ProjectMapper, + private readonly auditMapper: AuditMapper, @Inject(forwardRef(() => VersionMapper)) - private versionMapper: VersionMapper, + private readonly versionMapper: VersionMapper, @Inject(forwardRef(() => NodeMapper)) - private nodeMapper: NodeMapper, - private encryptionService: EncryptionService, + private readonly nodeMapper: NodeMapper, + private readonly configBundleMapper: ConfigBundleMapper, + private readonly encryptionService: EncryptionService, ) {} statusToDto(it: DeploymentStatusEnum): DeploymentStatusDto { @@ -125,29 +152,24 @@ export default class DeployMapper { } } - toDetailsDto( - deployment: DeploymentDetails, - publicKey?: string, - configBundleEnvironment?: EnvironmentToConfigBundleNameMap, - ): DeploymentDetailsDto { + toDetailsDto(deployment: DeploymentDetails, publicKey?: string): DeploymentDetailsDto { return { ...this.toDto(deployment), - token: deployment.tokens.length > 0 ? deployment.tokens[0] : null, + token: deployment.token ?? null, lastTry: deployment.tries, publicKey, - configBundleIds: deployment.configBundles.map(it => it.configBundle.id), - environment: deployment.environment as UniqueKeyValue[], + configBundles: deployment.configBundles.map(it => this.configBundleMapper.detailsToDto(it.configBundle)), + config: this.instanceConfigToDto(deployment.config), instances: deployment.instances.map(it => this.instanceToDto(it)), - configBundleEnvironment: configBundleEnvironment ?? {}, } } instanceToDto(it: InstanceDetails): InstanceDto { return { id: it.id, - updatedAt: it.updatedAt, + updatedAt: it.config?.updatedAt ?? it.image.config?.updatedAt ?? it.image.updatedAt, image: this.imageMapper.toDto(it.image), - config: this.instanceConfigToDto(it.config as any as InstanceContainerConfigData), + config: this.instanceConfigToDto(it.config), } } @@ -162,63 +184,60 @@ export default class DeployMapper { } } - instanceConfigToDto(it?: InstanceContainerConfigData): InstanceContainerConfigDto { - if (!it) { - return null - } + instanceConfigToDto(it: ContainerConfig): ConcreteContainerConfigDto { + const concreteConf = it as any as ConcreteContainerConfigData return { - ...this.containerMapper.configDataToDto(it as ContainerConfigData), - secrets: it.secrets, + ...this.containerMapper.configDataToDto(it.id, 'instance', it as any as ContainerConfigData), + secrets: concreteConf.secrets, } } - instanceConfigDtoToInstanceContainerConfigData( - imageConfig: ContainerConfigData, - currentConfig: InstanceContainerConfigData, - patch: InstanceContainerConfigDto, - ): InstanceContainerConfigData { - const config = this.containerMapper.configDtoToConfigData(currentConfig as ContainerConfigData, patch) - - if (config.labels) { - const currentLabels = currentConfig.labels ?? imageConfig.labels ?? {} - - config.labels = { - deployment: config.labels.deployment ?? currentLabels.deployment, - ingress: config.labels.ingress ?? currentLabels.ingress, - service: config.labels.service ?? currentLabels.service, - } + dbDeploymentToCreateDeploymentStatement( + deployment: Deployment, + ): Omit { + const result = { + ...deployment, } - if (config.annotations) { - const currentAnnotations = currentConfig.annotations ?? imageConfig.annotations ?? {} + delete result.id + delete result.nodeId + delete result.versionId + delete result.configId - config.annotations = { - deployment: config.annotations.deployment ?? currentAnnotations.deployment, - ingress: config.annotations.ingress ?? currentAnnotations.ingress, - service: config.annotations.service ?? currentAnnotations.service, - } - } + return result + } - let secrets = !patch.secrets ? currentConfig.secrets : patch.secrets - if (secrets && !currentConfig.secrets && imageConfig.secrets) { - secrets = this.containerMapper.mergeSecrets(secrets, imageConfig.secrets) + instanceConfigDtoToInstanceContainerConfigData( + imageConfig: ContainerConfigData, + instanceConfig: ConcreteContainerConfigData, + patch: ConcreteContainerConfigDto, + ): ConcreteContainerConfigData { + const config = this.containerMapper.configDtoToConfigData( + instanceConfig as ContainerConfigData, + patch as ContainerConfigDto, + ) + + if ('labels' in patch) { + const currentLabels = instanceConfig.labels ?? imageConfig.labels ?? {} + config.labels = mergeMarkers(config.labels, currentLabels) } - return { - ...config, - secrets, + if ('annotations' in patch) { + const currentAnnotations = instanceConfig.annotations ?? imageConfig.annotations ?? {} + config.annotations = mergeMarkers(config.annotations, currentAnnotations) } - } - instanceConfigDataToDb(config: InstanceContainerConfigData): Omit { - const imageConfig = this.containerMapper.configDataToDb(config) - return { - ...imageConfig, - tty: config.tty, - useLoadBalancer: config.useLoadBalancer, - proxyHeaders: config.proxyHeaders, + if ('secrets' in patch) { + // when they are already overridden, we simply use the patch + // otherwise we need to merge with them with the image secrets + return { + ...config, + secrets: instanceConfig.secrets ? patch.secrets : mergeSecrets(patch.secrets, imageConfig.secrets), + } } + + return config as ConcreteContainerConfigData } eventTypeToDto(it: DeploymentEventTypeEnum): DeploymentEventTypeDto { @@ -310,7 +329,7 @@ export default class DeployMapper { return events } - deploymentToAgentInstanceConfig(deployment: Deployment, mergedEnvironment: UniqueKeyValue[]): InstanceConfig { + instanceConfigToAgent(deployment: Deployment, mergedEnvironment: UniqueKeyValue[]): InstanceConfig { const environmentMap = this.mapKeyValueToMap(mergedEnvironment) return { @@ -323,29 +342,28 @@ export default class DeployMapper { return state ? (containerStateToJSON(state).toLowerCase() as ContainerState) : null } - commonConfigToAgentProto(config: MergedContainerConfigData, storage?: Storage): CommonContainerConfig { + commonConfigToAgentProto(config: ConcreteContainerConfigData, storage: Storage): CommonContainerConfig { return { name: config.name, environment: this.mapKeyValueToMap(config.environment), secrets: this.mapKeyValueToMap(config.secrets), commands: this.mapUniqueKeyToStringArray(config.commands), - expose: this.imageMapper.exposeStrategyToProto(config.expose) ?? ProtoExposeStrategy.NONE_ES, + expose: this.exposeStrategyToProto(config.expose), args: this.mapUniqueKeyToStringArray(config.args), - // Set user to the given value, if not null or use 0 if specifically 0, otherwise set null - user: config.user === -1 ? null : config.user, + user: config.user, workingDirectory: config.workingDirectory, TTY: config.tty, configContainer: config.configContainer, - importContainer: config.storageId ? this.storageToImportContainer(config, storage) : null, + importContainer: config.storageSet ? this.storageToImportContainer(config, storage) : null, routing: config.routing, initContainers: this.mapInitContainerToAgent(config.initContainers), - portRanges: config.portRanges, - ports: config.ports, - volumes: this.imageMapper.volumesToProto(config.volumes ?? []), + portRanges: config.portRanges ?? [], + ports: config.ports ?? [], + volumes: this.volumesToProto(config.volumes), } } - dagentConfigToAgentProto(config: MergedContainerConfigData): DagentContainerConfig { + dagentConfigToAgentProto(config: ConcreteContainerConfigData): DagentContainerConfig { return { networks: this.mapUniqueKeyToStringArray(config.networks), logConfig: @@ -353,28 +371,28 @@ export default class DeployMapper { ? null : { ...config.logConfig, - driver: this.imageMapper.logDriverToProto(config.logConfig.driver), + driver: this.logDriverToProto(config.logConfig.driver), options: this.mapKeyValueToMap(config.logConfig.options), }, - networkMode: this.imageMapper.networkModeToProto(config.networkMode), - restartPolicy: this.imageMapper.restartPolicyToProto(config.restartPolicy), + networkMode: this.networkModeToProto(config.networkMode), + restartPolicy: this.restartPolicyToProto(config.restartPolicy), labels: this.mapKeyValueToMap(config.dockerLabels), expectedState: !config.expectedState ? null : { - state: this.imageMapper.stateToProto(config.expectedState.state), + state: this.stateToProto(config.expectedState.state), timeout: config.expectedState.timeout, exitCode: config.expectedState.exitCode, }, } } - craneConfigToAgentProto(config: MergedContainerConfigData): CraneContainerConfig { + craneConfigToAgentProto(config: ConcreteContainerConfigData): CraneContainerConfig { return { customHeaders: this.mapUniqueKeyToStringArray(config.customHeaders), extraLBAnnotations: this.mapKeyValueToMap(config.extraLBAnnotations), deploymentStrategy: - this.imageMapper.deploymentStrategyToProto(config.deploymentStrategy) ?? ProtoDeploymentStrategy.ROLLING_UPDATE, + this.deploymentStrategyToProto(config.deploymentStrategy) ?? ProtoDeploymentStrategy.ROLLING_UPDATE, healthCheckConfig: config.healthCheckConfig, proxyHeaders: config.proxyHeaders, useLoadBalancer: config.useLoadBalancer, @@ -405,10 +423,35 @@ export default class DeployMapper { } } + instancesCreatedEventToMessage(event: InstancesCreatedEvent): InstancesAddedMessage { + return event.instances.map(it => ({ + id: it.id, + configId: it.configId, + image: this.imageMapper.toDto(it.image), + })) + } + + instanceDeletedEventToMessage(event: InstanceDeletedEvent): InstanceDeletedMessage { + return { + instanceId: event.id, + configId: event.configId, + } + } + + bundlesUpdatedEventToMessage(event: DeploymentConfigBundlesUpdatedEvent): DeploymentBundlesUpdatedMessage { + return { + bundles: event.bundles.map(it => this.configBundleMapper.detailsToDto(it)), + } + } + private mapInitContainerToAgent(list: InitContainer[]): AgentInitContainer[] { + if (!list) { + return [] + } + const result: AgentInitContainer[] = [] - list?.forEach(it => { + list.forEach(it => { result.push({ ...it, environment: this.mapKeyValueToMap(it.environment as KeyValue[]), @@ -442,7 +485,7 @@ export default class DeployMapper { return list.map(it => it.key) } - private storageToImportContainer(config: MergedContainerConfigData, storage: Storage): ImportContainer { + private storageToImportContainer(config: ConcreteContainerConfigData, storage: Storage): ImportContainer { const url = /^(http)s?/.test(storage.url) ? storage.url : `https://${storage.url}` let environment: { [key: string]: string } = { RCLONE_CONFIG_S3_TYPE: 's3', @@ -463,4 +506,125 @@ export default class DeployMapper { environment, } } + + private logDriverToProto(it: ContainerLogDriverType): DriverType { + switch (it) { + case undefined: + case null: + case 'none': + return DriverType.DRIVER_TYPE_NONE + case 'json-file': + return DriverType.JSON_FILE + default: + return driverTypeFromJSON(it.toUpperCase()) + } + } + + private volumesToProto(volumes: Volume[]): ProtoVolume[] { + if (!volumes) { + return [] + } + + return volumes.map(it => ({ ...it, type: this.volumeTypeToProto(it.type) })) + } + + private volumeTypeToProto(it?: ContainerVolumeType): ProtoVolumeType { + if (!it) { + return ProtoVolumeType.RO + } + + return volumeTypeFromJSON(it.toUpperCase()) + } + + private stateToProto(state: ContainerState): ProtoContainerState { + if (!state) { + return null + } + + switch (state) { + case 'running': + return ProtoContainerState.RUNNING + case 'waiting': + return ProtoContainerState.WAITING + case 'exited': + return ProtoContainerState.EXITED + default: + return ProtoContainerState.CONTAINER_STATE_UNSPECIFIED + } + } + + private exposeStrategyToProto(type: ExposeStrategy): ProtoExposeStrategy { + if (!type) { + return ProtoExposeStrategy.NONE_ES + } + + switch (type) { + case ExposeStrategy.expose: + return ProtoExposeStrategy.EXPOSE + case ExposeStrategy.exposeWithTls: + return ProtoExposeStrategy.EXPOSE_WITH_TLS + default: + return ProtoExposeStrategy.NONE_ES + } + } + + private restartPolicyToProto(type: RestartPolicy): ProtoRestartPolicy { + if (!type) { + return null + } + + switch (type) { + case RestartPolicy.always: + return ProtoRestartPolicy.ALWAYS + case RestartPolicy.no: + return ProtoRestartPolicy.NO + case RestartPolicy.unlessStopped: + return ProtoRestartPolicy.UNLESS_STOPPED + case RestartPolicy.onFailure: + return ProtoRestartPolicy.ON_FAILURE + default: + return ProtoRestartPolicy.NO + } + } + + private deploymentStrategyToProto(type: DeploymentStrategy): ProtoDeploymentStrategy { + if (!type) { + return null + } + + switch (type) { + case DeploymentStrategy.recreate: + return ProtoDeploymentStrategy.RECREATE + case DeploymentStrategy.rolling: + return ProtoDeploymentStrategy.ROLLING_UPDATE + default: + return ProtoDeploymentStrategy.DEPLOYMENT_STRATEGY_UNSPECIFIED + } + } + + private networkModeToProto(it: NetworkMode): ProtoNetworkMode { + if (!it) { + return null + } + + return networkModeFromJSON(it?.toUpperCase()) + } +} + +type InstanceDetails = Instance & { + image: ImageDetails + config: ContainerConfig +} + +type ConfigBundleDetails = ConfigBundle & { + config: ContainerConfig +} + +type DeploymentDetails = DeploymentWithNodeVersion & { + token: Pick + instances: InstanceDetails[] + config: ContainerConfig + configBundles: { + configBundle: ConfigBundleDetails + }[] } diff --git a/web/crux/src/app/deploy/deploy.message.ts b/web/crux/src/app/deploy/deploy.message.ts index 301d1413f2..a02543c8a2 100644 --- a/web/crux/src/app/deploy/deploy.message.ts +++ b/web/crux/src/app/deploy/deploy.message.ts @@ -1,6 +1,6 @@ -import { InstanceContainerConfigData, UniqueKeyValue } from 'src/domain/container' -import { ImageConfigProperty } from '../image/image.const' -import { DeploymentEventDto, EnvironmentToConfigBundleNameMap, InstanceDetails, InstanceDto } from './deploy.dto' +import { ConfigBundleDto } from '../config.bundle/config.bundle.dto' +import { ImageDto } from '../image/image.dto' +import { DeploymentEventDto, InstanceDto } from './deploy.dto' export const WS_TYPE_FETCH_DEPLOYMENT_EVENTS = 'fetch-deployment-events' @@ -10,31 +10,9 @@ export type DeploymentEventMessage = DeploymentEventDto export const WS_TYPE_DEPLOYMENT_EVENT_LIST = 'deployment-event-list' export type DeploymentEventListMessage = DeploymentEventMessage[] -export const WS_TYPE_PATCH_INSTANCE = 'patch-instance' -export type PatchInstanceMessage = { - instanceId: string - config?: Partial - resetSection?: ImageConfigProperty -} - -export const WS_TYPE_PATCH_RECEIVED = 'patch-received' - -export const WS_TYPE_INSTANCE_UPDATED = 'instance-updated' -export type InstanceUpdatedMessage = InstanceContainerConfigData & { - instanceId: string -} - -export const WS_TYPE_PATCH_DEPLOYMENT_ENV = 'patch-deployment-env' -export type PatchDeploymentEnvMessage = { - environment?: UniqueKeyValue[] - configBundleIds?: string[] -} - -export const WS_TYPE_DEPLOYMENT_ENV_UPDATED = 'deployment-env-updated' -export type DeploymentEnvUpdatedMessage = { - environment?: UniqueKeyValue[] - configBundleIds?: string[] - configBundleEnvironment?: EnvironmentToConfigBundleNameMap +export const WS_TYPE_DEPLOYMENT_BUNDLES_UPDATED = 'deployment-bundles-updated' +export type DeploymentBundlesUpdatedMessage = { + bundles: ConfigBundleDto[] } export const WS_TYPE_GET_INSTANCE = 'get-instance' @@ -56,5 +34,16 @@ export type InstanceSecretsMessage = { keys: string[] } +type InstanceCreatedMessage = { + id: string + configId: string + image: ImageDto +} export const WS_TYPE_INSTANCES_ADDED = 'instances-added' -export type InstancesAddedMessage = InstanceDetails[] +export type InstancesAddedMessage = InstanceCreatedMessage[] + +export const WS_TYPE_INSTANCE_DELETED = 'instance-deleted' +export type InstanceDeletedMessage = { + instanceId: string + configId: string +} diff --git a/web/crux/src/app/deploy/deploy.module.ts b/web/crux/src/app/deploy/deploy.module.ts index ae94bb1a90..3565cc9665 100644 --- a/web/crux/src/app/deploy/deploy.module.ts +++ b/web/crux/src/app/deploy/deploy.module.ts @@ -7,6 +7,7 @@ import PrismaService from 'src/services/prisma.service' import AgentModule from '../agent/agent.module' import AuditLoggerModule from '../audit.logger/audit.logger.module' import AuditMapper from '../audit/audit.mapper' +import ConfigBundleModule from '../config.bundle/config.bundle.module' import ContainerModule from '../container/container.module' import EditorModule from '../editor/editor.module' import ImageModule from '../image/image.module' @@ -15,6 +16,7 @@ import ProjectMapper from '../project/project.mapper' import RegistryModule from '../registry/registry.module' import TeamRepository from '../team/team.repository' import VersionMapper from '../version/version.mapper' +import DeployDomainEventListener from './deploy.domain-event.listener' import DeployHttpController from './deploy.http.controller' import { DeployJwtStrategy } from './deploy.jwt.strategy' import DeployMapper from './deploy.mapper' @@ -29,6 +31,7 @@ import DeployWebSocketGateway from './deploy.ws.gateway' RegistryModule, ContainerModule, ConfigModule, + ConfigBundleModule, AuditLoggerModule, ...CruxJwtModuleImports, ], @@ -37,6 +40,7 @@ import DeployWebSocketGateway from './deploy.ws.gateway' providers: [ PrismaService, DeployService, + DeployDomainEventListener, DeployMapper, TeamRepository, KratosService, diff --git a/web/crux/src/app/deploy/deploy.service.ts b/web/crux/src/app/deploy/deploy.service.ts index 07ecc46346..bcf4828da4 100644 --- a/web/crux/src/app/deploy/deploy.service.ts +++ b/web/crux/src/app/deploy/deploy.service.ts @@ -1,32 +1,48 @@ import { Inject, Injectable, Logger, forwardRef } from '@nestjs/common' import { ConfigService } from '@nestjs/config' +import { EventEmitter2 } from '@nestjs/event-emitter' import { JwtService } from '@nestjs/jwt' import { Identity } from '@ory/kratos-client' -import { ConfigBundle, DeploymentStatusEnum, Prisma } from '@prisma/client' -import { EMPTY, Observable, Subject, concatAll, concatMap, filter, from, map, of } from 'rxjs' +import { ContainerConfig, DeploymentStatusEnum, Prisma } from '@prisma/client' +import { EMPTY, Observable, filter, map } from 'rxjs' import { + ConcreteContainerConfigData, ContainerConfigData, - InstanceContainerConfigData, - MergedContainerConfigData, - UniqueKeyValue, UniqueSecretKeyValue, + configIsEmpty, } from 'src/domain/container' import Deployment from 'src/domain/deployment' import { DeploymentTokenScriptGenerator } from 'src/domain/deployment-token' +import { + DEPLOYMENT_EVENT_CONFIG_BUNDLES_UPDATE, + DEPLOYMENT_EVENT_INSTACE_CREATE, + DEPLOYMENT_EVENT_INSTACE_DELETE, + DeploymentConfigBundlesUpdatedEvent, + InstanceDeletedEvent, + InstancesCreatedEvent, +} from 'src/domain/domain-events' +import { + InvalidSecrets, + collectInvalidSecrets, + deploymentConfigOf, + instanceConfigOf, +} from 'src/domain/start-deployment' import { DeploymentTokenPayload, tokenSignOptionsFor } from 'src/domain/token' import { collectChildVersionIds, collectParentVersionIds, toPrismaJson } from 'src/domain/utils' +import { copyDeployment } from 'src/domain/version-increase' import { CruxPreconditionFailedException } from 'src/exception/crux-exception' import { DeployRequest } from 'src/grpc/protobuf/proto/agent' import EncryptionService from 'src/services/encryption.service' import PrismaService from 'src/services/prisma.service' +import { DomainEvent } from 'src/shared/domain-event' +import { WsMessage } from 'src/websockets/common' import { v4 as uuid } from 'uuid' import AgentService from '../agent/agent.service' import ContainerMapper from '../container/container.mapper' import { EditorLeftMessage, EditorMessage } from '../editor/editor.message' import EditorServiceProvider from '../editor/editor.service.provider' -import { ImageEvent } from '../image/image.event' -import ImageEventService from '../image/image.event.service' import RegistryMapper from '../registry/registry.mapper' +import DeployDomainEventListener from './deploy.domain-event.listener' import { CopyDeploymentDto, CreateDeploymentDto, @@ -34,50 +50,45 @@ import { DeploymentDetailsDto, DeploymentDto, DeploymentEventDto, - DeploymentImageEvent, DeploymentLogListDto, DeploymentLogPaginationQuery, DeploymentTokenCreatedDto, - EnvironmentToConfigBundleNameMap, InstanceDto, InstanceSecretsDto, - PatchDeploymentDto, PatchInstanceDto, + UpdateDeploymentDto, } from './deploy.dto' import DeployMapper from './deploy.mapper' -import { DeploymentEventListMessage } from './deploy.message' +import { + DeploymentEventListMessage, + WS_TYPE_DEPLOYMENT_BUNDLES_UPDATED, + WS_TYPE_INSTANCES_ADDED, + WS_TYPE_INSTANCE_DELETED, +} from './deploy.message' @Injectable() export default class DeployService { private readonly logger = new Logger(DeployService.name) - private deploymentImageEvents = new Subject() - constructor( private readonly prisma: PrismaService, private readonly jwtService: JwtService, @Inject(forwardRef(() => AgentService)) private readonly agentService: AgentService, - readonly imageEventService: ImageEventService, + private readonly domainEvents: DeployDomainEventListener, private readonly mapper: DeployMapper, + private readonly events: EventEmitter2, private readonly registryMapper: RegistryMapper, private readonly containerMapper: ContainerMapper, private readonly editorServices: EditorServiceProvider, private readonly configService: ConfigService, private readonly encryptionService: EncryptionService, - ) { - imageEventService - .watchEvents() - .pipe( - concatMap(async it => { - const event = await this.transformImageEvent(it) - if (event.type === 'create') { - return from(this.onImageAddedToVersion(event)) - } - return of(event) - }), - concatAll(), - ) - .subscribe(it => this.deploymentImageEvents.next(it)) + ) {} + + subscribeToDomainEvents(deploymentId: string): Observable { + return this.domainEvents.watchEvents(deploymentId).pipe( + map(it => this.transformDomainEventToWsMessage(it)), + filter(it => !!it), + ) } async checkDeploymentIsInTeam(teamSlug: string, deploymentId: string, identity: Identity): Promise { @@ -109,9 +120,14 @@ export default class DeployService { }, include: { node: true, + config: true, configBundles: { include: { - configBundle: true, + configBundle: { + include: { + config: true, + }, + }, }, }, version: { @@ -119,7 +135,7 @@ export default class DeployService { project: true, }, }, - tokens: { + token: { select: { id: true, name: true, @@ -147,11 +163,8 @@ export default class DeployService { }) const publicKey = this.agentService.getById(deployment.nodeId)?.publicKey - const configBundleEnvironment = this.getConfigBundleEnvironmentKeys( - deployment.configBundles.map(it => it.configBundle), - ) - return this.mapper.toDetailsDto(deployment, publicKey, configBundleEnvironment) + return this.mapper.toDetailsDto(deployment, publicKey) } async getDeploymentEvents(deploymentId: string, tryCount?: number): Promise { @@ -241,7 +254,7 @@ export default class DeployService { }, }) - const previousInstances = await this.prisma.deployment.findFirst({ + const previousDeployment = await this.prisma.deployment.findFirst({ where: { prefix: request.prefix, nodeId: request.nodeId, @@ -263,8 +276,16 @@ export default class DeployService { }, }) - if (previousInstances && previousInstances.instances) { + if (previousDeployment?.instances) { instanceIds.forEach(async it => { + const secrets: UniqueSecretKeyValue[] = + (previousDeployment.instances.find(instance => instance.imageId === it.imageId).config + ?.secrets as UniqueSecretKeyValue[]) ?? [] + + if (secrets.length < 1) { + return + } + await this.prisma.instance.update({ where: { id: it.id, @@ -272,9 +293,9 @@ export default class DeployService { data: { config: { create: { - secrets: - previousInstances.instances.find(instance => instance.imageId === it.imageId).config?.secrets ?? - Prisma.JsonNull, + type: 'instance', + updatedBy: identity.id, + secrets, }, }, }, @@ -285,56 +306,43 @@ export default class DeployService { return this.mapper.toDto(deployment) } - async patchDeployment(deploymentId: string, req: PatchDeploymentDto, identity: Identity): Promise { - if (req.configBundleIds) { - const connections = await this.prisma.deployment.findFirst({ - where: { - id: deploymentId, - }, - include: { - configBundles: true, - }, - }) - - const connectedBundles = connections.configBundles.map(it => it.configBundleId) - const toConnect = req.configBundleIds.filter(it => !connectedBundles.includes(it)) - const toDisconnect = connectedBundles.filter(it => !req.configBundleIds.includes(it)) - - if (toConnect.length > 0 || toDisconnect.length > 0) { - await this.prisma.$transaction(async prisma => { - await prisma.configBundleOnDeployments.createMany({ - data: toConnect.map(it => ({ - deploymentId, - configBundleId: it, - })), - }) - - await prisma.configBundleOnDeployments.deleteMany({ - where: { - deploymentId, - configBundleId: { - in: toDisconnect, - }, - }, - }) - }) - } - } - - await this.prisma.deployment.update({ + async updateDeployment(deploymentId: string, req: UpdateDeploymentDto, identity: Identity): Promise { + const deployment = await this.prisma.deployment.update({ where: { id: deploymentId, }, data: { - note: req.note ?? undefined, - prefix: req.prefix ?? undefined, - protected: req.protected ?? undefined, - environment: req.environment - ? req.environment.map(it => this.containerMapper.uniqueKeyValueDtoToDb(it)) - : undefined, + note: req.note, + prefix: req.prefix, + protected: req.protected, + configBundles: { + deleteMany: { + deploymentId, + }, + create: req.configBundles.map(configBundleId => ({ configBundle: { connect: { id: configBundleId } } })), + }, + updatedAt: new Date(), updatedBy: identity.id, }, + select: { + configBundles: { + select: { + configBundle: { + include: { + config: true, + }, + }, + }, + }, + }, }) + + const event: DeploymentConfigBundlesUpdatedEvent = { + deploymentId, + bundles: deployment.configBundles.map(it => it.configBundle), + } + + await this.events.emitAsync(DEPLOYMENT_EVENT_CONFIG_BUNDLES_UPDATE, event) } async patchInstance( @@ -359,16 +367,21 @@ export default class DeployService { const configData = this.mapper.instanceConfigDtoToInstanceContainerConfigData( instance.image.config as any as ContainerConfigData, - (instance.config ?? {}) as any as InstanceContainerConfigData, + (instance.config ?? {}) as any as ConcreteContainerConfigData, req.config, ) - const config = this.containerMapper.configDataToDb(configData) + const config: Omit = { + ...this.containerMapper.configDataToDbPatch(configData), + type: 'deployment', + updatedAt: new Date(), + updatedBy: identity.id, + } - // We should overwrite the user in the ConfigData. This is an edge case, which is why we haven't - // implemented a new mapper for configDataToDb. However, in the long run, if there are more similar - // situations, we will have to create a different mapper for InstanceConfig. - config.user = configData.user + // // We should overwrite the user in the ConfigData. This is an edge case, which is why we haven't + // // implemented a new mapper for configDataToDb. However, in the long run, if there are more similar + // // situations, we will have to create a different mapper for InstanceConfig. + // config.user = configData.user await this.prisma.deployment.update({ where: { @@ -383,10 +396,7 @@ export default class DeployService { }, data: { config: { - upsert: { - update: config, - create: config, - }, + update: config, }, }, }, @@ -436,9 +446,14 @@ export default class DeployService { id: deploymentId, }, include: { + config: true, configBundles: { include: { - configBundle: true, + configBundle: { + include: { + config: true, + }, + }, }, }, version: { @@ -473,7 +488,7 @@ export default class DeployService { name: true, }, }, - tokens: { + token: { select: { name: true, }, @@ -492,98 +507,39 @@ export default class DeployService { }) } - const invalidSecrets = deployment.instances - .map(it => { - if (!it.config) { - return null - } - - const secrets = it.config.secrets as UniqueSecretKeyValue[] + const invalidSecrets: InvalidSecrets[] = [] - if (!secrets || secrets.every(secret => secret.publicKey === publicKey)) { - return null - } + // deployment config + const deploymentConfig = deploymentConfigOf(deployment) - return { - instanceId: it.id, - invalid: secrets.filter(secret => secret.publicKey !== publicKey).map(secret => secret.id), - secrets: secrets.map(secret => { - if (secret.publicKey === publicKey) { - return secret - } + if (deploymentConfig.secrets) { + const invalidDeploymentSecrets = collectInvalidSecrets(deployment.configId, deploymentConfig, publicKey) - return { - ...secret, - value: '', - encrypted: false, - publicKey, - } - }), - } - }) - .filter(it => it !== null) - - const invalidSecretsUpdates = invalidSecrets - .map(it => - this.prisma.instance.update({ - where: { - id: it.instanceId, - }, - data: { - config: { - update: { - secrets: it.secrets, - }, - }, - }, - }), - ) - .filter(it => it !== null) - - if (invalidSecretsUpdates.length > 0) { - await this.prisma.$transaction(invalidSecretsUpdates) - - throw new CruxPreconditionFailedException({ - message: 'Some secrets are invalid', - property: 'secrets', - value: invalidSecrets.map(it => ({ ...it, secrets: undefined })), - }) + if (invalidDeploymentSecrets) { + invalidSecrets.push(invalidDeploymentSecrets) + } } - const mergedConfigs: Map = new Map( - deployment.instances.map(it => { - /* - * If a deployment succeeds the merged config is saved as the instance config, - * so downgrading is possible even if the image config is modified. - */ - - if ( - deployment.version.type !== 'rolling' && - (deployment.status === 'successful' || deployment.status === 'obsolete') - ) { - return [ - it.id, - this.containerMapper.mergeConfigs( - {} as ContainerConfigData, - (it.config ?? {}) as InstanceContainerConfigData, - ), - ] - } + // instance config + // instanceId to instanceConfig + const instanceConfigs: Map = new Map( + deployment.instances.map(instance => { + const instanceConfig = instanceConfigOf(deployment, deploymentConfig, instance) - return [ - it.id, - this.containerMapper.mergeConfigs( - (it.image.config ?? {}) as ContainerConfigData, - (it.config ?? {}) as InstanceContainerConfigData, - ), - ] + return [instance.id, instanceConfig] }), ) - const mergedEnvironment = this.mergeEnvironments( - (deployment.environment as UniqueKeyValue[]) ?? [], - deployment.configBundles.map(it => it.configBundle), - ) + const invalidInstanceSecrets = deployment.instances + .map(it => collectInvalidSecrets(it.configId, instanceConfigs.get(it.id), publicKey)) + .filter(it => !!it) + + invalidSecrets.push(...invalidInstanceSecrets) + + // check for invalid secrets + if (invalidSecrets.length > 0) { + await this.updateInvalidSecretsAndThrow(invalidSecrets) + } const tries = deployment.tries + 1 await this.prisma.deployment.update({ @@ -595,8 +551,8 @@ export default class DeployService { }, }) - const deploy = new Deployment( - { + const deploy = new Deployment({ + request: { id: deployment.id, releaseNotes: deployment.version.changelog, versionName: deployment.version.name, @@ -605,24 +561,23 @@ export default class DeployService { const { registry } = it.image const registryUrl = this.registryMapper.pullUrlOf(registry) - const mergedConfig = mergedConfigs.get(it.id) - const storage = mergedConfig.storageId + const config = instanceConfigs.get(it.id) + const storage = config.storageSet ? await this.prisma.storage.findFirstOrThrow({ where: { - id: mergedConfig.storageId, + id: config.storageId, }, }) : undefined return { - common: this.mapper.commonConfigToAgentProto(mergedConfig, storage), - crane: this.mapper.craneConfigToAgentProto(mergedConfig), - dagent: this.mapper.dagentConfigToAgentProto(mergedConfig), + common: this.mapper.commonConfigToAgentProto(config, storage), + crane: this.mapper.craneConfigToAgentProto(config), + dagent: this.mapper.dagentConfigToAgentProto(config), id: it.id, containerName: it.image.config.name, imageName: it.image.name, tag: it.image.tag, - instanceConfig: this.mapper.deploymentToAgentInstanceConfig(deployment, mergedEnvironment), registry: registryUrl, registryAuth: !registry.user ? undefined @@ -636,17 +591,22 @@ export default class DeployService { }), ), }, - { + notification: { teamId: deployment.version.project.teamId, - actor: identity ?? (deployment.tokens.length > 0 ? deployment.tokens[0].name : null), + actor: identity ?? deployment.token?.name ?? null, projectName: deployment.version.project.name, versionName: deployment.version.name, nodeName: deployment.node.name, }, - mergedConfigs, - mergedEnvironment, + instanceConfigs: new Map( + [...instanceConfigs.entries()].filter(entry => { + const [, conf] = entry + return !configIsEmpty(conf) + }), + ), + deploymentConfig: !configIsEmpty(deploymentConfig) ? deploymentConfig : null, tries, - ) + }) this.logger.debug(`Starting deployment: ${deploy.id}`) @@ -667,7 +627,7 @@ export default class DeployService { select: { status: true, prefix: true, - environment: true, + config: true, configBundles: { include: { configBundle: true, @@ -715,6 +675,8 @@ export default class DeployService { } await this.prisma.$transaction(async prisma => { + // set other deployments to obsolate in this version + await prisma.deployment.updateMany({ data: { status: DeploymentStatusEnum.obsolete, @@ -732,6 +694,14 @@ export default class DeployService { }, }) + if (deployment.version.type === 'rolling') { + // we don't care about version parents and children + // also we don't save the concrete configs + + return + } + + // set other deployments obsolate in the parent version const parentVersionIds = await collectParentVersionIds(prisma, deployment.version.id) await prisma.deployment.updateMany({ data: { @@ -749,6 +719,7 @@ export default class DeployService { }, }) + // set other diployments in children to downgraded const childVersionIds = await collectChildVersionIds(prisma, deployment.version.id) await prisma.deployment.updateMany({ data: { @@ -763,17 +734,15 @@ export default class DeployService { }, }) - if (deployment.version.type === 'rolling') { - return - } - - if (finishedDeployment.sharedEnvironment.length > 0) { + if (finishedDeployment.deploymentConfig) { await prisma.deployment.update({ where: { id: finishedDeployment.id, }, data: { - environment: toPrismaJson(finishedDeployment.sharedEnvironment), + config: { + update: this.containerMapper.configDataToDbPatch(finishedDeployment.deploymentConfig), + }, }, }) @@ -784,21 +753,24 @@ export default class DeployService { }) } - const configUpserts = Array.from(finishedDeployment.mergedConfigs).map(it => { + const configUpserts = Array.from(finishedDeployment.instanceConfigs).map(it => { const [key, config] = it - const dbConfig = this.containerMapper.configDataToDb(config) + const data = this.containerMapper.configDataToDbPatch(config) - return prisma.instanceContainerConfig.upsert({ + return prisma.instance.update({ where: { - instanceId: key, + id: key, }, - update: { - ...dbConfig, - }, - create: { - ...dbConfig, - id: undefined, - instanceId: key, + data: { + config: { + upsert: { + update: data, + create: { + ...data, + type: 'instance', + }, + }, + }, }, }) }) @@ -831,10 +803,6 @@ export default class DeployService { return runningDeployment.watchStatus().pipe(map(it => this.mapper.progressEventToEventDto(it))) } - subscribeToDeploymentEditEvents(deploymentId: string): Observable { - return this.deploymentImageEvents.pipe(filter(it => it.deploymentIds.includes(deploymentId))) - } - async getDeployments(teamSlug: string, nodeId?: string): Promise { const deployments = await this.prisma.deployment.findMany({ where: { @@ -918,6 +886,7 @@ export default class DeployService { id: deploymentId, }, include: { + config: true, instances: { include: { config: true, @@ -926,15 +895,29 @@ export default class DeployService { }, }) + const copiedDeployment = copyDeployment(oldDeployment) + const newDeployment = await this.prisma.deployment.create({ data: { - versionId: oldDeployment.versionId, - nodeId: request.nodeId, prefix: request.prefix, note: request.note, status: DeploymentStatusEnum.preparing, createdBy: identity.id, - environment: oldDeployment.environment ?? [], + version: { + connect: { + id: copiedDeployment.versionId, + }, + }, + node: { + connect: { + id: copiedDeployment.nodeId, + }, + }, + config: !copiedDeployment.config + ? undefined + : { + create: this.containerMapper.dbConfigToCreateConfigStatement(copiedDeployment.config), + }, }, include: { node: true, @@ -952,45 +935,24 @@ export default class DeployService { oldDeployment.instances.map(it => this.prisma.instance.create({ data: { - deploymentId: newDeployment.id, - imageId: it.imageId, - config: it.config - ? { + deployment: { + connect: { + id: newDeployment.id, + }, + }, + image: { + connect: { + id: it.imageId, + }, + }, + config: !it.config + ? undefined + : { create: { - name: it.config.name, - expose: it.config.expose, - routing: toPrismaJson(it.config.routing), - configContainer: toPrismaJson(it.config.configContainer), - user: it.config.user, - tty: it.config.tty, - ports: toPrismaJson(it.config.ports), - portRanges: toPrismaJson(it.config.portRanges), - volumes: toPrismaJson(it.config.volumes), - commands: toPrismaJson(it.config.commands), - args: toPrismaJson(it.config.args), - environment: toPrismaJson(it.config.environment), + ...this.containerMapper.dbConfigToCreateConfigStatement(it.config), secrets: differentNode ? null : toPrismaJson(it.config.secrets), - initContainers: toPrismaJson(it.config.initContainers), - logConfig: toPrismaJson(it.config.logConfig), - restartPolicy: it.config.restartPolicy, - networkMode: it.config.networkMode, - networks: toPrismaJson(it.config.networks), - deploymentStrategy: it.config.deploymentStrategy, - healthCheckConfig: toPrismaJson(it.config.healthCheckConfig), - resourceConfig: toPrismaJson(it.config.resourceConfig), - proxyHeaders: it.config.proxyHeaders ?? false, - useLoadBalancer: it.config.useLoadBalancer ?? false, - customHeaders: toPrismaJson(it.config.customHeaders), - extraLBAnnotations: toPrismaJson(it.config.extraLBAnnotations), - capabilities: toPrismaJson(it.config.capabilities), - annotations: toPrismaJson(it.config.annotations), - labels: toPrismaJson(it.config.labels), - dockerLabels: toPrismaJson(it.config.dockerLabels), - storageId: it.config.storageId, - storageConfig: toPrismaJson(it.config.storageConfig), }, - } - : undefined, + }, }, }), ), @@ -1110,105 +1072,48 @@ export default class DeployService { return message } - async getConfigBundleEnvironmentById(deploymentId: string): Promise { - const deployment = await this.prisma.deployment.findUniqueOrThrow({ - where: { - id: deploymentId, - }, - include: { - configBundles: { - include: { - configBundle: true, - }, - }, - }, - }) - - return this.getConfigBundleEnvironmentKeys(deployment.configBundles.map(it => it.configBundle)) - } - - private async transformImageEvent(event: ImageEvent): Promise { - const deployments = await this.prisma.deployment.findMany({ - select: { - id: true, - }, - where: { - versionId: event.versionId, - }, - }) - - return { - ...event, - deploymentIds: deployments.map(it => it.id), + private transformDomainEventToWsMessage(ev: DomainEvent): WsMessage | null { + switch (ev.type) { + case DEPLOYMENT_EVENT_INSTACE_CREATE: + return { + type: WS_TYPE_INSTANCES_ADDED, + data: this.mapper.instancesCreatedEventToMessage(ev.event as InstancesCreatedEvent), + } + case DEPLOYMENT_EVENT_INSTACE_DELETE: + return { + type: WS_TYPE_INSTANCE_DELETED, + data: this.mapper.instanceDeletedEventToMessage(ev.event as InstanceDeletedEvent), + } + case DEPLOYMENT_EVENT_CONFIG_BUNDLES_UPDATE: + return { + type: WS_TYPE_DEPLOYMENT_BUNDLES_UPDATED, + data: this.mapper.bundlesUpdatedEventToMessage(ev.event as DeploymentConfigBundlesUpdatedEvent), + } + default: { + this.logger.error(`Unhandled domain event ${ev.type}`) + return null + } } } - private async onImageAddedToVersion(event: DeploymentImageEvent): Promise { - const versionId = event.images?.length > 0 ? event.versionId : null - const deployments = await this.prisma.deployment.findMany({ - select: { - id: true, - }, - where: { - versionId, - }, - }) - - const instances = await Promise.all( - deployments.flatMap(deployment => - event.images.map(it => - this.prisma.instance.create({ - include: { - config: true, - image: { - include: { - config: true, - registry: true, - }, - }, - }, - data: { - deploymentId: deployment.id, - imageId: it.id, - }, - }), - ), + private async updateInvalidSecretsAndThrow(secrets: InvalidSecrets[]) { + await this.prisma.$transaction( + secrets.map(it => + this.prisma.containerConfig.update({ + where: { + id: it.configId, + }, + data: { + secrets: it.secrets, + }, + }), ), ) - return { - ...event, - instances, - } - } - - private mergeEnvironments(deployment: UniqueKeyValue[], configBundles: ConfigBundle[]): UniqueKeyValue[] { - const mergedEnvironment: Record = {} - - configBundles.forEach(bundle => { - const bundleEnv = (bundle.data as UniqueKeyValue[]) ?? [] - bundleEnv.forEach(it => { - mergedEnvironment[it.key] = it - }) - }) - - deployment.forEach(it => { - mergedEnvironment[it.key] = it - }) - - return Object.values(mergedEnvironment) - } - - private getConfigBundleEnvironmentKeys(configBundles: ConfigBundle[]): EnvironmentToConfigBundleNameMap { - const envToBundle: EnvironmentToConfigBundleNameMap = {} - - configBundles.forEach(bundle => { - const bundleEnv = (bundle.data as UniqueKeyValue[]) ?? [] - bundleEnv.forEach(it => { - envToBundle[it.key] = bundle.name - }) + throw new CruxPreconditionFailedException({ + message: 'Some secrets are invalid', + property: 'secrets', + value: secrets.map(it => ({ ...it, secrets: undefined })), }) - - return envToBundle } } diff --git a/web/crux/src/app/deploy/deploy.ws.gateway.ts b/web/crux/src/app/deploy/deploy.ws.gateway.ts index 1173d47c87..204ef09736 100644 --- a/web/crux/src/app/deploy/deploy.ws.gateway.ts +++ b/web/crux/src/app/deploy/deploy.ws.gateway.ts @@ -1,6 +1,6 @@ import { SubscribeMessage, WebSocketGateway } from '@nestjs/websockets' import { Identity } from '@ory/kratos-client' -import { Observable, Subject, map, of, startWith, takeUntil } from 'rxjs' +import { Observable, map, of, startWith, takeUntil } from 'rxjs' import { AuditLogLevel } from 'src/decorators/audit-logger.decorator' import { WsAuthorize, WsClient, WsMessage, WsSubscribe, WsSubscription, WsUnsubscribe } from 'src/websockets/common' import SocketClient from 'src/websockets/decorators/ws.client.decorator' @@ -27,32 +27,19 @@ import { } from '../editor/editor.message' import EditorServiceProvider from '../editor/editor.service.provider' import { IdentityFromSocket } from '../token/jwt-auth.guard' -import { ImageDeletedMessage, WS_TYPE_IMAGE_DELETED } from '../version/version.message' -import { PatchDeploymentDto, PatchInstanceDto } from './deploy.dto' import { - DeploymentEnvUpdatedMessage, DeploymentEventListMessage, DeploymentEventMessage, GetInstanceMessage, GetInstanceSecretsMessage, InstanceMessage, InstanceSecretsMessage, - InstanceUpdatedMessage, - InstancesAddedMessage, - PatchDeploymentEnvMessage, - PatchInstanceMessage, - WS_TYPE_DEPLOYMENT_ENV_UPDATED, WS_TYPE_DEPLOYMENT_EVENT_LIST, WS_TYPE_FETCH_DEPLOYMENT_EVENTS, WS_TYPE_GET_INSTANCE, WS_TYPE_GET_INSTANCE_SECRETS, WS_TYPE_INSTANCE, - WS_TYPE_INSTANCES_ADDED, WS_TYPE_INSTANCE_SECRETS, - WS_TYPE_INSTANCE_UPDATED, - WS_TYPE_PATCH_DEPLOYMENT_ENV, - WS_TYPE_PATCH_INSTANCE, - WS_TYPE_PATCH_RECEIVED, } from './deploy.message' import DeployService from './deploy.service' @@ -66,8 +53,6 @@ const DeploymentId = () => WsParam('deploymentId') @UseGlobalWsGuards() @UseGlobalWsInterceptors() export default class DeployWebSocketGateway { - private deploymentEventCompleters = new Map>() - constructor( private readonly service: DeployService, private readonly editorServices: EditorServiceProvider, @@ -95,33 +80,10 @@ export default class DeployWebSocketGateway { data: me, }) - const key = `${client.token}-${deploymentId}` - if (this.deploymentEventCompleters.has(key)) { - this.deploymentEventCompleters.get(key).next(undefined) - this.deploymentEventCompleters.delete(key) - } - - const completer = new Subject() - this.deploymentEventCompleters.set(key, completer) - this.service - .subscribeToDeploymentEditEvents(deploymentId) - .pipe(takeUntil(completer)) - .subscribe(event => { - if (event.type === 'create' && event.instances) { - subscription.sendToAll({ - type: WS_TYPE_INSTANCES_ADDED, - data: event.instances.filter(it => it.deploymentId === deploymentId), - } as WsMessage) - } else if (event.type === 'delete') { - subscription.sendToAll({ - type: WS_TYPE_IMAGE_DELETED, - data: { - imageId: event.imageId, - }, - } as WsMessage) - } - }) + .subscribeToDomainEvents(deploymentId) + .pipe(takeUntil(subscription.getCompleter(client.token))) + .subscribe(message => subscription.sendToAll(message)) return { type: WS_TYPE_EDITOR_INIT, @@ -139,17 +101,12 @@ export default class DeployWebSocketGateway { @SocketSubscription() subscription: WsSubscription, ): Promise { const data = await this.service.onEditorLeft(deploymentId, client.token) + const message: WsMessage = { type: WS_TYPE_EDITOR_LEFT, data, } subscription.sendToAllExcept(client, message) - - const key = `${client.token}-${deploymentId}` - if (this.deploymentEventCompleters.has(key)) { - this.deploymentEventCompleters.get(key).next(undefined) - this.deploymentEventCompleters.delete(key) - } } @AuditLogLevel('disabled') @@ -184,81 +141,6 @@ export default class DeployWebSocketGateway { ) } - @SubscribeMessage(WS_TYPE_PATCH_INSTANCE) - async patchInstance( - @DeploymentId() deploymentId: string, - @SocketMessage() message: PatchInstanceMessage, - @IdentityFromSocket() identity: Identity, - @SocketClient() client: WsClient, - @SocketSubscription() subscription: WsSubscription, - ): Promise> { - const cruxReq: Pick = { - config: {}, - } - - if (message.resetSection) { - cruxReq.config[message.resetSection as string] = null - } else { - cruxReq.config = message.config - } - - await this.service.patchInstance(deploymentId, message.instanceId, cruxReq, identity) - - const updateMessage: WsMessage = { - type: WS_TYPE_INSTANCE_UPDATED, - data: { - instanceId: message.instanceId, - ...cruxReq.config, - }, - } - - subscription.sendToAllExcept(client, updateMessage) - - return { - type: WS_TYPE_PATCH_RECEIVED, - data: null, - } - } - - @SubscribeMessage(WS_TYPE_PATCH_DEPLOYMENT_ENV) - async patchDeploymentEnvironment( - @DeploymentId() deploymentId: string, - @SocketMessage() message: PatchDeploymentEnvMessage, - @SocketClient() client: WsClient, - @SocketSubscription() subscription: WsSubscription, - @IdentityFromSocket() identity: Identity, - ): Promise> { - const cruxReq: PatchDeploymentDto = { - environment: message.environment, - configBundleIds: message.configBundleIds, - } - - await this.service.patchDeployment(deploymentId, cruxReq, identity) - - const configBundleEnvironment = await this.service.getConfigBundleEnvironmentById(deploymentId) - - const response: WsMessage = { - type: WS_TYPE_DEPLOYMENT_ENV_UPDATED, - data: { - ...message, - configBundleEnvironment, - }, - } as WsMessage - - if (message.configBundleIds) { - // If config bundles change send the response to every client - // so the configBundleEnvironment will update - subscription.sendToAll(response) - } else { - subscription.sendToAllExcept(client, response) - } - - return { - type: WS_TYPE_PATCH_RECEIVED, - data: null, - } - } - @AuditLogLevel('disabled') @SubscribeMessage(WS_TYPE_GET_INSTANCE) async getInstance(@SocketMessage() message: GetInstanceMessage): Promise> { diff --git a/web/crux/src/app/deploy/interceptors/deploy.patch.interceptor.ts b/web/crux/src/app/deploy/interceptors/deploy.patch.interceptor.ts index f674601eac..27cdb67f43 100644 --- a/web/crux/src/app/deploy/interceptors/deploy.patch.interceptor.ts +++ b/web/crux/src/app/deploy/interceptors/deploy.patch.interceptor.ts @@ -4,7 +4,7 @@ import { Observable } from 'rxjs' import { checkDeploymentMutability } from 'src/domain/deployment' import { CruxConflictException, CruxPreconditionFailedException } from 'src/exception/crux-exception' import PrismaService from 'src/services/prisma.service' -import { PatchDeploymentDto } from '../deploy.dto' +import { UpdateDeploymentDto } from '../deploy.dto' @Injectable() export default class DeployPatchValidationInterceptor implements NestInterceptor { @@ -12,7 +12,7 @@ export default class DeployPatchValidationInterceptor implements NestInterceptor async intercept(context: ExecutionContext, next: CallHandler): Promise> { const req = context.switchToHttp().getRequest() - const body = req.body as PatchDeploymentDto + const body = req.body as UpdateDeploymentDto const deploymentId = req.params.deploymentId as string const deployment = await this.prisma.deployment.findUniqueOrThrow({ diff --git a/web/crux/src/app/deploy/interceptors/deploy.start.interceptor.ts b/web/crux/src/app/deploy/interceptors/deploy.start.interceptor.ts index 68862bb764..5f7f0cc2f5 100644 --- a/web/crux/src/app/deploy/interceptors/deploy.start.interceptor.ts +++ b/web/crux/src/app/deploy/interceptors/deploy.start.interceptor.ts @@ -3,15 +3,12 @@ import { Observable } from 'rxjs' import AgentService from 'src/app/agent/agent.service' import ContainerMapper from 'src/app/container/container.mapper' import { ImageValidation } from 'src/app/image/image.dto' -import { - ContainerConfigData, - InstanceContainerConfigData, - UniqueKeyValue, - UniqueSecretKey, - UniqueSecretKeyValue, -} from 'src/domain/container' +import { ConcreteContainerConfigData, ContainerConfigData, ContainerConfigDataWithId } from 'src/domain/container' +import { getConflictsForConcreteConfig } from 'src/domain/container-conflict' +import { mergeConfigsWithConcreteConfig } from 'src/domain/container-merge' import { checkDeploymentDeployability } from 'src/domain/deployment' import { parseDyrectorioEnvRules } from 'src/domain/image' +import { missingSecretsOf } from 'src/domain/start-deployment' import { createStartDeploymentSchema, yupValidate } from 'src/domain/validation' import { CruxPreconditionFailedException } from 'src/exception/crux-exception' import PrismaService from 'src/services/prisma.service' @@ -34,9 +31,14 @@ export default class DeployStartValidationInterceptor implements NestInterceptor const deployment = await this.prisma.deployment.findUniqueOrThrow({ include: { version: true, + config: true, configBundles: { include: { - configBundle: true, + configBundle: { + include: { + config: true, + }, + }, }, }, instances: { @@ -62,13 +64,7 @@ export default class DeployStartValidationInterceptor implements NestInterceptor }, }) - if (deployment.instances.length < 1) { - throw new CruxPreconditionFailedException({ - message: 'There are no instances to deploy', - property: 'instances', - }) - } - + // deployment if (!checkDeploymentDeployability(deployment.status, deployment.version.type)) { throw new CruxPreconditionFailedException({ message: 'Invalid deployment status.', @@ -77,11 +73,19 @@ export default class DeployStartValidationInterceptor implements NestInterceptor }) } + // instances + if (deployment.instances.length < 1) { + throw new CruxPreconditionFailedException({ + message: 'There are no instances to deploy', + property: 'instances', + }) + } + const instances = deployment.instances.map(it => ({ ...it, - config: this.containerMapper.mergeConfigs( - it.image.config as any as ContainerConfigData, - (it.config ?? {}) as any as InstanceContainerConfigData, + config: mergeConfigsWithConcreteConfig( + [it.image.config as any as ContainerConfigData], + it.config as any as ConcreteContainerConfigData, ), })) @@ -102,101 +106,105 @@ export default class DeployStartValidationInterceptor implements NestInterceptor yupValidate(createStartDeploymentSchema(instanceValidations), target) - const node = this.agentService.getById(deployment.nodeId) - if (!node?.ready) { + const missingSecrets = deployment.instances + .map(it => { + const imageConfig = it.image.config as any as ContainerConfigData + const instanceConfig = it.config as any as ConcreteContainerConfigData + const mergedConfig = mergeConfigsWithConcreteConfig([imageConfig], instanceConfig) + + return missingSecretsOf(it.configId, mergedConfig) + }) + .filter(it => !!it) + + if (missingSecrets.length > 0) { throw new CruxPreconditionFailedException({ - message: 'Node is busy or unreachable', - property: 'nodeId', - value: deployment.nodeId, + message: 'Required secrets must have values!', + property: 'instanceSecrets', + value: missingSecrets, }) } - const missingSecrets = deployment.instances.filter(it => { - const imageSecrets = (it.image.config.secrets as UniqueSecretKey[]) ?? [] - const requiredSecrets = imageSecrets.filter(imageSecret => imageSecret.required).map(secret => secret.key) + // config bundles + if (deployment.configBundles.length > 0) { + const configs = deployment.configBundles.map(it => it.configBundle.config as any as ContainerConfigDataWithId) + const concreteConfig = deployment.config as any as ConcreteContainerConfigData + const conflicts = getConflictsForConcreteConfig(configs, concreteConfig) + if (conflicts) { + throw new CruxPreconditionFailedException({ + message: 'Unresolved conflicts between config bundles', + property: 'configBundles', + value: conflicts, + }) + } - const instanceSecrets = (it.config?.secrets as UniqueSecretKeyValue[]) ?? [] - const hasSecrets = requiredSecrets.every(requiredSecret => { - const instanceSecret = instanceSecrets.find(secret => secret.key === requiredSecret) - if (!instanceSecret) { - return false - } + const mergedConfig = mergeConfigsWithConcreteConfig(configs, concreteConfig) + const missingInstanceSecrets = missingSecretsOf(deployment.configId, mergedConfig) + if (missingInstanceSecrets) { + throw new CruxPreconditionFailedException({ + message: 'Required secrets must have values!', + property: 'deploymentSecrets', + value: missingInstanceSecrets, + }) + } + } - return instanceSecret.encrypted && instanceSecret.value.length > 0 + // node + const node = this.agentService.getById(deployment.nodeId) + if (!node) { + throw new CruxPreconditionFailedException({ + message: 'Node is unreachable', + property: 'nodeId', + value: deployment.nodeId, }) + } - return !hasSecrets - }) - - if (missingSecrets.length > 0) { + if (!node.ready) { throw new CruxPreconditionFailedException({ - message: 'Required secrets must have values!', - property: 'instanceIds', - value: missingSecrets.map(it => ({ - id: it.id, - name: it.config?.name ?? it.image.name, - })), + message: 'Node is busy', + property: 'nodeId', + value: deployment.nodeId, }) } - if (!deployment.protected) { - const { - query: { ignoreProtected }, - } = req - - if (!ignoreProtected) { - const otherProtected = await this.prisma.deployment.findFirst({ - where: { - protected: true, - nodeId: deployment.nodeId, - prefix: deployment.prefix, - versionId: - deployment.version.type === 'incremental' - ? { - not: deployment.versionId, - } - : undefined, - }, - }) + // deployment protection + if (deployment.protected) { + // this is a protected deployment no need to check for protected prefixes - if (otherProtected) { - throw new CruxPreconditionFailedException({ - message: - deployment.version.type === 'incremental' - ? "There's a protected deployment with the same node and prefix in a different version" - : "There's a protected deployment with the same node and prefix", - property: 'protectedDeploymentId', - value: otherProtected.id, - }) - } - } + return next.handle() } - if (deployment.configBundles.length > 0) { - const deploymentEnv = (deployment.environment as UniqueKeyValue[]) ?? [] - const deploymentEnvKeys = deploymentEnv.map(it => it.key) - - const envToBundle: Record = {} // [Environment key]: config bundle name - - deployment.configBundles.forEach(it => { - const bundleEnv = (it.configBundle.data as UniqueKeyValue[]) ?? [] - - bundleEnv.forEach(env => { - if (deploymentEnvKeys.includes(env.key)) { - return - } - if (envToBundle[env.key]) { - throw new CruxPreconditionFailedException({ - message: `Environment variable ${env.key} in ${it.configBundle.name} is already defined by ${ - envToBundle[env.key] - }. Please define the key in the deployment or resolve the conflict in the bundles.`, - property: 'configBundleId', - value: it.configBundle.id, - }) - } - - envToBundle[env.key] = it.configBundle.name - }) + const { + query: { ignoreProtected }, + } = req + + if (ignoreProtected) { + // force deploy + + return next.handle() + } + + const otherProtected = await this.prisma.deployment.findFirst({ + where: { + protected: true, + nodeId: deployment.nodeId, + prefix: deployment.prefix, + versionId: + deployment.version.type === 'incremental' + ? { + not: deployment.versionId, + } + : undefined, + }, + }) + + if (otherProtected) { + throw new CruxPreconditionFailedException({ + message: + deployment.version.type === 'incremental' + ? "There's a protected deployment with the same node and prefix in a different version" + : "There's a protected deployment with the same node and prefix", + property: 'protectedDeploymentId', + value: otherProtected.id, }) } diff --git a/web/crux/src/app/image/image.const.ts b/web/crux/src/app/image/image.const.ts index 6af3725380..40d785a1b6 100644 --- a/web/crux/src/app/image/image.const.ts +++ b/web/crux/src/app/image/image.const.ts @@ -1,47 +1 @@ // TODO: move this to the container domain - -export const COMMON_CONFIG_PROPERTIES = [ - 'name', - 'environment', - 'secrets', - 'routing', - 'expose', - 'user', - 'tty', - 'configContainer', - 'ports', - 'portRanges', - 'volumes', - 'commands', - 'args', - 'initContainers', - 'storage', -] as const - -export const CRANE_CONFIG_PROPERTIES = [ - 'deploymentStrategy', - 'customHeaders', - 'proxyHeaders', - 'useLoadBalancer', - 'extraLBAnnotations', - 'healthCheckConfig', - 'resourceConfig', - 'labels', - 'annotations', -] as const - -export const DAGENT_CONFIG_PROPERTIES = [ - 'logConfig', - 'restartPolicy', - 'networkMode', - 'networks', - 'dockerLabels', -] as const - -export const ALL_CONFIG_PROPERTIES = [ - ...COMMON_CONFIG_PROPERTIES, - ...CRANE_CONFIG_PROPERTIES, - ...DAGENT_CONFIG_PROPERTIES, -] as const - -export type ImageConfigProperty = (typeof ALL_CONFIG_PROPERTIES)[number] diff --git a/web/crux/src/app/image/image.dto.ts b/web/crux/src/app/image/image.dto.ts index b55cbf6288..bb09572d35 100644 --- a/web/crux/src/app/image/image.dto.ts +++ b/web/crux/src/app/image/image.dto.ts @@ -11,7 +11,7 @@ import { ValidateNested, } from 'class-validator' import { ENVIRONMENT_VALUE_TYPES, EnvironmentValueType } from 'src/domain/image' -import { ContainerConfigDto, PartialContainerConfigDto } from '../container/container.dto' +import { ContainerConfigDto } from '../container/container.dto' import { BasicRegistryDto } from '../registry/registry.dto' export class EnvironmentRule { @@ -47,7 +47,8 @@ export class ImageDto { order: number @ValidateNested() - config: ContainerConfigDto + @IsOptional() + config?: ContainerConfigDto @Type(() => Date) @IsDate() @@ -75,5 +76,5 @@ export class PatchImageDto { @IsOptional() @ValidateNested() - config?: PartialContainerConfigDto | null + config?: ContainerConfigDto | null } diff --git a/web/crux/src/app/image/image.event.service.ts b/web/crux/src/app/image/image.event.service.ts deleted file mode 100644 index 0d6d45b9d5..0000000000 --- a/web/crux/src/app/image/image.event.service.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Injectable } from '@nestjs/common' -import { Observable, Subject } from 'rxjs' -import { ImageDto } from './image.dto' -import { ImageEvent } from './image.event' - -@Injectable() -export default class ImageEventService { - private readonly events = new Subject() - - public imagesAddedToVersion(versionId: string, images: ImageDto[]) { - this.events.next({ - type: 'create', - versionId, - images, - }) - } - - public imageUpdated(versionId: string, image: ImageDto) { - this.events.next({ - type: 'update', - versionId, - images: [image], - }) - } - - public imageDeletedFromVersion(versionId: string, imageId: string) { - this.events.next({ - type: 'delete', - versionId, - imageId, - }) - } - - public watchEvents(): Observable { - return this.events.asObservable() - } -} diff --git a/web/crux/src/app/image/image.event.ts b/web/crux/src/app/image/image.event.ts deleted file mode 100644 index 1d29571f9b..0000000000 --- a/web/crux/src/app/image/image.event.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { ImageDto } from './image.dto' - -const IMAGE_EVENT_TYPE_VALUES = ['create', 'update', 'delete'] as const -export type ImageEventType = (typeof IMAGE_EVENT_TYPE_VALUES)[number] - -export class ImageEvent { - type: ImageEventType - - versionId: string - - images?: ImageDto[] - - imageId?: string -} diff --git a/web/crux/src/app/image/image.mapper.ts b/web/crux/src/app/image/image.mapper.ts index 302a5684d0..c2013a7b08 100644 --- a/web/crux/src/app/image/image.mapper.ts +++ b/web/crux/src/app/image/image.mapper.ts @@ -4,32 +4,17 @@ import { DeploymentStrategy, ExposeStrategy, Image, - InstanceContainerConfig, NetworkMode, Registry, RestartPolicy, } from '@prisma/client' +import { ContainerConfigData } from 'src/domain/container' import { - ContainerConfigData, - ContainerLogDriverType, - ContainerState, - ContainerVolumeType, - Volume, -} from 'src/domain/container' -import { toPrismaJson } from 'src/domain/utils' -import { Volume as ProtoVolume } from 'src/grpc/protobuf/proto/agent' -import { - DriverType, - ContainerState as ProtoContainerState, DeploymentStrategy as ProtoDeploymentStrategy, ExposeStrategy as ProtoExposeStrategy, NetworkMode as ProtoNetworkMode, RestartPolicy as ProtoRestartPolicy, - VolumeType as ProtoVolumeType, - driverTypeFromJSON, - networkModeFromJSON, networkModeToJSON, - volumeTypeFromJSON, } from 'src/grpc/protobuf/proto/common' import ContainerMapper from '../container/container.mapper' import RegistryMapper from '../registry/registry.mapper' @@ -49,73 +34,23 @@ export default class ImageMapper { tag: it.tag, order: it.order, registry: this.registryMapper.toDto(it.registry), - config: this.containerMapper.configDataToDto(it.config as any as ContainerConfigData), + config: this.containerMapper.configDataToDto(it.config.id, 'image', it.config as any as ContainerConfigData), createdAt: it.createdAt, labels: it.labels as Record, } } - dbContainerConfigToCreateImageStatement( - config: ContainerConfig | InstanceContainerConfig, - ): Omit { - return { - // common - name: config.name, - expose: config.expose, - routing: toPrismaJson(config.routing), - configContainer: toPrismaJson(config.configContainer), - // Set user to the given value, if not null or use 0 if specifically 0, otherwise set to default -1 - user: config.user ?? (config.user === 0 ? 0 : -1), - workingDirectory: config.workingDirectory, - tty: config.tty ?? false, - ports: toPrismaJson(config.ports), - portRanges: toPrismaJson(config.portRanges), - volumes: toPrismaJson(config.volumes), - commands: toPrismaJson(config.commands), - args: toPrismaJson(config.args), - environment: toPrismaJson(config.environment), - secrets: toPrismaJson(config.secrets), - initContainers: toPrismaJson(config.initContainers), - logConfig: toPrismaJson(config.logConfig), - storageSet: config.storageSet, - storageId: config.storageId, - storageConfig: toPrismaJson(config.storageConfig), - - // dagent - restartPolicy: config.restartPolicy, - networkMode: config.networkMode, - networks: toPrismaJson(config.networks), - dockerLabels: toPrismaJson(config.dockerLabels), - expectedState: toPrismaJson(config.expectedState), - - // crane - deploymentStrategy: config.deploymentStrategy, - healthCheckConfig: toPrismaJson(config.healthCheckConfig), - resourceConfig: toPrismaJson(config.resourceConfig), - proxyHeaders: config.proxyHeaders ?? false, - useLoadBalancer: config.useLoadBalancer ?? false, - customHeaders: toPrismaJson(config.customHeaders), - extraLBAnnotations: toPrismaJson(config.extraLBAnnotations), - capabilities: toPrismaJson(config.capabilities), - annotations: toPrismaJson(config.annotations), - labels: toPrismaJson(config.labels), - metrics: toPrismaJson(config.metrics), + dbImageToCreateImageStatement(image: Image): Omit { + const result = { + ...image, } - } - deploymentStrategyToProto(type: DeploymentStrategy): ProtoDeploymentStrategy { - if (!type) { - return null - } + delete result.id + delete result.registryId + delete result.versionId + delete result.configId - switch (type) { - case DeploymentStrategy.recreate: - return ProtoDeploymentStrategy.RECREATE - case DeploymentStrategy.rolling: - return ProtoDeploymentStrategy.ROLLING_UPDATE - default: - return ProtoDeploymentStrategy.DEPLOYMENT_STRATEGY_UNSPECIFIED - } + return result } deploymentStrategyToDb(type: ProtoDeploymentStrategy): DeploymentStrategy { @@ -133,21 +68,6 @@ export default class ImageMapper { } } - exposeStrategyToProto(type: ExposeStrategy): ProtoExposeStrategy { - if (!type) { - return null - } - - switch (type) { - case ExposeStrategy.expose: - return ProtoExposeStrategy.EXPOSE - case ExposeStrategy.exposeWithTls: - return ProtoExposeStrategy.EXPOSE_WITH_TLS - default: - return ProtoExposeStrategy.NONE_ES - } - } - exposeStrategyToDb(type: ProtoExposeStrategy): ExposeStrategy { if (!type) { return undefined @@ -163,25 +83,6 @@ export default class ImageMapper { } } - restartPolicyToProto(type: RestartPolicy): ProtoRestartPolicy { - if (!type) { - return null - } - - switch (type) { - case RestartPolicy.always: - return ProtoRestartPolicy.ALWAYS - case RestartPolicy.no: - return ProtoRestartPolicy.NO - case RestartPolicy.unlessStopped: - return ProtoRestartPolicy.UNLESS_STOPPED - case RestartPolicy.onFailure: - return ProtoRestartPolicy.ON_FAILURE - default: - return ProtoRestartPolicy.NO - } - } - restartPolicyToDb(type: ProtoRestartPolicy): RestartPolicy { if (!type) { return undefined @@ -201,14 +102,6 @@ export default class ImageMapper { } } - networkModeToProto(it: NetworkMode): ProtoNetworkMode { - if (!it) { - return null - } - - return networkModeFromJSON(it?.toUpperCase()) - } - networkModeToDb(it: ProtoNetworkMode): NetworkMode { if (!it) { return undefined @@ -220,52 +113,6 @@ export default class ImageMapper { return networkModeToJSON(it).toLowerCase() as NetworkMode } - - logDriverToProto(it: ContainerLogDriverType): DriverType { - switch (it) { - case undefined: - case null: - case 'none': - return DriverType.DRIVER_TYPE_NONE - case 'json-file': - return DriverType.JSON_FILE - default: - return driverTypeFromJSON(it.toUpperCase()) - } - } - - volumesToProto(volumes: Volume[]): ProtoVolume[] { - if (!volumes) { - return null - } - - return volumes.map(it => ({ ...it, type: this.volumeTypeToProto(it.type) })) - } - - volumeTypeToProto(it?: ContainerVolumeType): ProtoVolumeType { - if (!it) { - return ProtoVolumeType.RO - } - - return volumeTypeFromJSON(it.toUpperCase()) - } - - stateToProto(state: ContainerState): ProtoContainerState { - if (!state) { - return null - } - - switch (state) { - case 'running': - return ProtoContainerState.RUNNING - case 'waiting': - return ProtoContainerState.WAITING - case 'exited': - return ProtoContainerState.EXITED - default: - return ProtoContainerState.CONTAINER_STATE_UNSPECIFIED - } - } } export type ImageDetails = Image & { diff --git a/web/crux/src/app/image/image.module.ts b/web/crux/src/app/image/image.module.ts index c054b5f9fc..2db99507fe 100644 --- a/web/crux/src/app/image/image.module.ts +++ b/web/crux/src/app/image/image.module.ts @@ -9,14 +9,13 @@ import RegistryClientProvider from '../registry/registry-client.provider' import RegistryMapper from '../registry/registry.mapper' import RegistryModule from '../registry/registry.module' import TeamRepository from '../team/team.repository' -import ImageEventService from './image.event.service' import ImageHttpController from './image.http.controller' import ImageMapper from './image.mapper' import ImageService from './image.service' @Module({ imports: [RegistryModule, EditorModule, ContainerModule, AuditLoggerModule], - exports: [ImageService, ImageMapper, ImageEventService], + exports: [ImageService, ImageMapper], providers: [ PrismaService, ImageService, @@ -25,7 +24,6 @@ import ImageService from './image.service' RegistryMapper, KratosService, EncryptionService, - ImageEventService, RegistryClientProvider, ], controllers: [ImageHttpController], diff --git a/web/crux/src/app/image/image.service.ts b/web/crux/src/app/image/image.service.ts index 01cc04b9dd..72f0e44104 100644 --- a/web/crux/src/app/image/image.service.ts +++ b/web/crux/src/app/image/image.service.ts @@ -1,17 +1,15 @@ import { Injectable } from '@nestjs/common' +import { EventEmitter2 } from '@nestjs/event-emitter' import { Identity } from '@ory/kratos-client' -import { ContainerConfig } from '@prisma/client' -import { ContainerConfigData, UniqueKeyValue } from 'src/domain/container' -import { containerNameFromImageName } from 'src/domain/deployment' +import { UniqueKeyValue } from 'src/domain/container' +import { IMAGE_EVENT_ADD, IMAGE_EVENT_DELETE, ImageDeletedEvent, ImagesAddedEvent } from 'src/domain/domain-events' import { EnvironmentRule, parseDyrectorioEnvRules } from 'src/domain/image' import PrismaService from 'src/services/prisma.service' -import { v4 } from 'uuid' -import ContainerMapper from '../container/container.mapper' -import EditorServiceProvider from '../editor/editor.service.provider' +import { v4 as uuid } from 'uuid' +import ContainerConfigService from '../container/container-config.service' import RegistryClientProvider from '../registry/registry-client.provider' import TeamRepository from '../team/team.repository' import { AddImagesDto, ImageDto, PatchImageDto } from './image.dto' -import ImageEventService from './image.event.service' import ImageMapper from './image.mapper' type LabelMap = Record @@ -21,11 +19,10 @@ type RegistryLabelMap = Record @Injectable() export default class ImageService { constructor( - private prisma: PrismaService, - private mapper: ImageMapper, - private containerMapper: ContainerMapper, - private editorServices: EditorServiceProvider, - private eventService: ImageEventService, + private readonly prisma: PrismaService, + private readonly mapper: ImageMapper, + private readonly containerConfigService: ContainerConfigService, + private readonly events: EventEmitter2, private readonly teamRepository: TeamRepository, private readonly registryClients: RegistryClientProvider, ) {} @@ -123,24 +120,13 @@ export default class ImageService { registry: true, }, data: { - registryId: registyImages.registryId, - versionId, + registry: { connect: { id: registyImages.registryId } }, + version: { connect: { id: versionId } }, + config: { create: { type: 'image' } }, createdBy: identity.id, name: imageName, tag: imageTag, order: order++, - config: { - create: { - name: containerNameFromImageName(imageName), - deploymentStrategy: 'recreate', - expose: 'none', - networkMode: 'bridge', - proxyHeaders: false, - restartPolicy: 'no', - tty: false, - useLoadBalancer: false, - }, - }, }, }) @@ -161,7 +147,7 @@ export default class ImageService { const defaultEnvs = Object.entries(envRules) .filter(([, rule]) => rule.required || !!rule.default) .map(([key, rule]) => ({ - id: v4(), + id: uuid(), key, value: rule.default ?? '', })) @@ -185,33 +171,44 @@ export default class ImageService { const dtos = images.map(it => this.mapper.toDto(it)) - this.eventService.imagesAddedToVersion(versionId, dtos) + const event: ImagesAddedEvent = { + versionId, + images, + } + await this.events.emitAsync(IMAGE_EVENT_ADD, event) return dtos } async patchImage(teamSlug: string, imageId: string, request: PatchImageDto, identity: Identity): Promise { - const currentConfig = await this.prisma.containerConfig.findUniqueOrThrow({ + const currentConfig = await this.prisma.containerConfig.findFirstOrThrow({ where: { - imageId, + image: { + id: imageId, + }, }, }) - const configData = this.containerMapper.configDtoToConfigData( - currentConfig as any as ContainerConfigData, - request.config ?? {}, - ) - - let labels: Record = null + if (request.config) { + await this.containerConfigService.patchConfig( + currentConfig.id, + { + config: request.config, + }, + identity, + ) + } + let labels: Record if (request.tag) { - const image = await this.prisma.image.findFirst({ + const image = await this.prisma.image.findUniqueOrThrow({ where: { id: imageId, }, select: { name: true, registryId: true, + config: true, }, }) @@ -221,12 +218,13 @@ export default class ImageService { labels = await api.client.labels(image.name, request.tag) const rules = parseDyrectorioEnvRules(labels) - configData.environment = ImageService.mergeEnvironmentsRules(configData.environment, rules) + image.config.environment = ImageService.mergeEnvironmentsRules( + image.config.environment as UniqueKeyValue[], + rules, + ) } - const config: Omit = this.containerMapper.configDataToDb(configData) - - const image = await this.prisma.image.update({ + await this.prisma.image.update({ where: { id: imageId, }, @@ -237,17 +235,10 @@ export default class ImageService { data: { labels: labels ?? undefined, tag: request.tag ?? undefined, - config: { - update: { - data: config, - }, - }, + updatedAt: new Date(), updatedBy: identity.id, }, }) - - const dto = this.mapper.toDto(image) - this.eventService.imageUpdated(image.versionId, dto) } async deleteImage(imageId: string): Promise { @@ -257,13 +248,22 @@ export default class ImageService { }, select: { versionId: true, + instances: { + select: { + id: true, + configId: true, + deploymentId: true, + }, + }, }, }) - const editors = await this.editorServices.getService(image.versionId) - editors?.onDeleteItem(imageId) - - this.eventService.imageDeletedFromVersion(image.versionId, imageId) + const event: ImageDeletedEvent = { + versionId: image.versionId, + imageId, + instances: image.instances, + } + await this.events.emitAsync(IMAGE_EVENT_DELETE, event) } async orderImages(request: string[], identity: Identity): Promise { @@ -301,7 +301,7 @@ export default class ImageService { const [key, rule] = it map[key] = { - id: currentEnv[key]?.id ?? v4(), + id: currentEnv[key]?.id ?? uuid(), key, value: currentEnv[key]?.value ?? rule.default ?? '', } diff --git a/web/crux/src/app/package/package.module.ts b/web/crux/src/app/package/package.module.ts index 550dff83a4..f1beea9d34 100644 --- a/web/crux/src/app/package/package.module.ts +++ b/web/crux/src/app/package/package.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common' import PrismaService from 'src/services/prisma.service' +import ContainerModule from '../container/container.module' import DeployModule from '../deploy/deploy.module' -import ImageModule from '../image/image.module' import NodeModule from '../node/node.module' import ProjectModule from '../project/project.module' import TeamModule from '../team/team.module' @@ -12,7 +12,7 @@ import PackageMapper from './package.mapper' import PackageService from './package.service' @Module({ - imports: [ProjectModule, VersionModule, TeamModule, NodeModule, ImageModule, DeployModule], + imports: [ProjectModule, VersionModule, TeamModule, NodeModule, ContainerModule, DeployModule], exports: [], controllers: [PackageHttpController], providers: [PrismaService, PackageService, PackageMapper, TeamRepository], diff --git a/web/crux/src/app/package/package.service.ts b/web/crux/src/app/package/package.service.ts index fa1bbf5f15..89cd34c791 100644 --- a/web/crux/src/app/package/package.service.ts +++ b/web/crux/src/app/package/package.service.ts @@ -4,9 +4,9 @@ import { DeploymentStatusEnum } from '@prisma/client' import { VersionWithDeployments } from 'src/domain/version' import { ImageWithConfig, copyDeployment } from 'src/domain/version-increase' import PrismaService from 'src/services/prisma.service' +import ContainerMapper from '../container/container.mapper' import { DeploymentDto } from '../deploy/deploy.dto' import DeployMapper from '../deploy/deploy.mapper' -import ImageMapper from '../image/image.mapper' import TeamRepository from '../team/team.repository' import { CreatePackageDeploymentDto, @@ -26,7 +26,7 @@ class PackageService { constructor( private readonly mapper: PackageMapper, private readonly deployMapper: DeployMapper, - private readonly imageMapper: ImageMapper, + private readonly containerMapper: ContainerMapper, private readonly teamRepository: TeamRepository, private readonly prisma: PrismaService, ) {} @@ -367,6 +367,7 @@ class PackageService { ], }, include: { + config: true, instances: { include: { config: true, @@ -417,12 +418,27 @@ class PackageService { sourceVersion.deployments.at(0) const copiedDeployment = copyDeployment(sourceDeployment) + const data = this.deployMapper.dbDeploymentToCreateDeploymentStatement(copiedDeployment) const newDeployment = await this.prisma.deployment.create({ data: { - ...copiedDeployment, + ...data, createdBy: identity.id, - versionId: target.id, + version: { + connect: { + id: target.id, + }, + }, + node: { + connect: { + id: copiedDeployment.nodeId, + }, + }, + config: !copiedDeployment.config + ? undefined + : { + create: this.containerMapper.dbConfigToCreateConfigStatement(copiedDeployment.config), + }, instances: undefined, }, include: { @@ -503,10 +519,8 @@ class PackageService { config: !instance.config ? undefined : { - create: this.imageMapper.dbContainerConfigToCreateImageStatement({ + create: this.containerMapper.dbConfigToCreateConfigStatement({ ...instance.config, - id: undefined, - instanceId: undefined, }), }, }, diff --git a/web/crux/src/app/storage/interceptors/storage.delete.interceptor.ts b/web/crux/src/app/storage/interceptors/storage.delete.interceptor.ts index 14616f79bf..b3d92b60f9 100644 --- a/web/crux/src/app/storage/interceptors/storage.delete.interceptor.ts +++ b/web/crux/src/app/storage/interceptors/storage.delete.interceptor.ts @@ -18,21 +18,8 @@ export default class StorageDeleteValidationInterceptor implements NestIntercept }, take: 1, }) - if (usedContainerConfig > 0) { - throw new CruxPreconditionFailedException({ - property: 'id', - value: storageId, - message: 'Storage is already in use.', - }) - } - const usedInstanceContainerConfig = await this.prisma.instanceContainerConfig.count({ - where: { - storageId, - }, - take: 1, - }) - if (usedInstanceContainerConfig > 0) { + if (usedContainerConfig > 0) { throw new CruxPreconditionFailedException({ property: 'id', value: storageId, diff --git a/web/crux/src/app/storage/storage.mapper.ts b/web/crux/src/app/storage/storage.mapper.ts index f76cf1f1a4..1d0381ec8f 100644 --- a/web/crux/src/app/storage/storage.mapper.ts +++ b/web/crux/src/app/storage/storage.mapper.ts @@ -23,7 +23,7 @@ export default class StorageMapper { return { ...this.listItemToDto(storage), public: !storage.accessKey, - inUse: storage._count.containerConfigs > 0 || storage._count.instanceConfigs > 0, + inUse: storage._count.containerConfigs > 0, } } @@ -47,6 +47,5 @@ export default class StorageMapper { type StorageWithCount = Storage & { _count: { containerConfigs: number - instanceConfigs: number } } diff --git a/web/crux/src/app/storage/storage.service.ts b/web/crux/src/app/storage/storage.service.ts index 8bb605b040..b32df2012e 100644 --- a/web/crux/src/app/storage/storage.service.ts +++ b/web/crux/src/app/storage/storage.service.ts @@ -32,8 +32,13 @@ export default class StorageService { include: { _count: { select: { - containerConfigs: true, - instanceConfigs: true, + containerConfigs: { + where: { + type: { + in: ['image', 'instance'], + }, + }, + }, }, }, }, @@ -60,7 +65,6 @@ export default class StorageService { ...storage, _count: { containerConfigs: 0, - instanceConfigs: 0, }, }) } diff --git a/web/crux/src/app/template/template.service.ts b/web/crux/src/app/template/template.service.ts index 70ddc1898e..b6bcdaa838 100644 --- a/web/crux/src/app/template/template.service.ts +++ b/web/crux/src/app/template/template.service.ts @@ -13,7 +13,7 @@ import { import PrismaService from 'src/services/prisma.service' import TemplateFileService, { TemplateContainerConfig, TemplateImage } from 'src/services/template.file.service' import { VERSIONLESS_PROJECT_VERSION_NAME } from 'src/shared/const' -import { v4 } from 'uuid' +import { v4 as uuid } from 'uuid' import ImageMapper from '../image/image.mapper' import { CreateProjectDto, ProjectDto } from '../project/project.dto' import ProjectService from '../project/project.service' @@ -95,18 +95,15 @@ export default class TemplateService { } private idify(object: T): T { - return { ...object, id: v4() } + return { ...object, id: uuid() } } - private mapTemplateConfig(config: TemplateContainerConfig): ContainerConfigData { - // TODO (polaroi8d): wait this until we'll rework the templates + private mapTemplateConfig(config: TemplateContainerConfig): Omit { + // TODO (polaroi8d): wait with this for the templates rework // TODO (@m8vago): validate containerConfigData return { ...config, - tty: config.tty ?? false, - useLoadBalancer: config.useLoadBalancer ?? false, - proxyHeaders: config.proxyHeaders ?? false, deploymentStrategy: config.deploymentStatregy ? this.imageMapper.deploymentStrategyToDb( deploymentStrategyFromJSON(config.deploymentStatregy.toLocaleUpperCase()), @@ -121,7 +118,7 @@ export default class TemplateService { expose: config.expose ? this.imageMapper.exposeStrategyToDb(exposeStrategyFromJSON(config.expose.toLocaleUpperCase())) : 'none', - networks: config.networks ? config.networks.map(it => ({ id: v4(), key: it })) : [], + networks: config.networks ? config.networks.map(it => ({ id: uuid(), key: it })) : [], ports: config.ports ? toPrismaJson(config.ports.map(it => this.idify(it))) : [], environment: config.environment ? config.environment.map(it => this.idify(it)) : [], args: config.args ? config.args.map(it => this.idify(it)) : [], @@ -196,7 +193,7 @@ export default class TemplateService { const images = templateImages.map((it, index) => { const registryId = registryLookup.find(reg => reg.name === it.registryName).id - const config: ContainerConfigData = this.mapTemplateConfig(it.config) + const config = this.mapTemplateConfig(it.config) return this.prisma.image.create({ include: { @@ -204,16 +201,25 @@ export default class TemplateService { registry: true, }, data: { - registryId, - versionId: version.id, createdBy: identity.id, name: it.image, order: index, tag: it.tag, + version: { connect: { id: version.id } }, + registry: { connect: { id: registryId } }, config: { create: { ...config, - id: undefined, + type: 'image', + updatedAt: undefined, + updatedBy: identity.id, + storage: !it.config.storageId + ? undefined + : { + connect: { + id: it.config.storageId, + }, + }, }, }, }, diff --git a/web/crux/src/app/version/version.domain-event.listener.ts b/web/crux/src/app/version/version.domain-event.listener.ts new file mode 100644 index 0000000000..7ac97f6e93 --- /dev/null +++ b/web/crux/src/app/version/version.domain-event.listener.ts @@ -0,0 +1,40 @@ +import { Injectable } from '@nestjs/common' +import { OnEvent } from '@nestjs/event-emitter' +import { filter, Observable, Subject } from 'rxjs' +import { + IMAGE_EVENT_ADD, + IMAGE_EVENT_DELETE, + ImageDeletedEvent, + ImageEvent, + ImagesAddedEvent, +} from 'src/domain/domain-events' +import { DomainEvent } from 'src/shared/domain-event' + +@Injectable() +export default class VersionDomainEventListener { + private versionEvents = new Subject>() + + watchEvents(versionId: string): Observable> { + return this.versionEvents.pipe(filter(it => it.event.versionId === versionId)) + } + + @OnEvent(IMAGE_EVENT_ADD) + onImagesAdded(event: ImagesAddedEvent) { + const editEvent: DomainEvent = { + type: IMAGE_EVENT_ADD, + event, + } + + this.versionEvents.next(editEvent) + } + + @OnEvent(IMAGE_EVENT_DELETE) + onImagesDeleted(event: ImageDeletedEvent) { + const editEvent: DomainEvent = { + type: IMAGE_EVENT_DELETE, + event, + } + + this.versionEvents.next(editEvent) + } +} diff --git a/web/crux/src/app/version/version.mapper.ts b/web/crux/src/app/version/version.mapper.ts index 1060134c6f..63e3024a8c 100644 --- a/web/crux/src/app/version/version.mapper.ts +++ b/web/crux/src/app/version/version.mapper.ts @@ -1,6 +1,7 @@ import { Version } from '.prisma/client' import { Inject, Injectable, forwardRef } from '@nestjs/common' import { ProjectTypeEnum } from '@prisma/client' +import { ImageDeletedEvent, ImagesAddedEvent } from 'src/domain/domain-events' import { versionIsDeletable, versionIsIncreasable, versionIsMutable } from 'src/domain/version' import { VersionChainWithEdges } from 'src/domain/version-chain' import { BasicProperties } from '../../shared/dtos/shared.dto' @@ -10,6 +11,7 @@ import DeployMapper from '../deploy/deploy.mapper' import ImageMapper, { ImageDetails } from '../image/image.mapper' import { NodeConnectionStatus } from '../node/node.dto' import { BasicVersionDto, VersionChainDto, VersionDetailsDto, VersionDto } from './version.dto' +import { ImageDeletedMessage, ImagesAddedMessage } from './version.message' @Injectable() export default class VersionMapper { @@ -72,6 +74,18 @@ export default class VersionMapper { }, } } + + imagesAddedEventToMessage(event: ImagesAddedEvent): ImagesAddedMessage { + return { + images: event.images.map(it => this.imageMapper.toDto(it)), + } + } + + imageDeletedEventToMessage(event: ImageDeletedEvent): ImageDeletedMessage { + return { + imageId: event.imageId, + } + } } export type VersionWithChildren = Version & { diff --git a/web/crux/src/app/version/version.message.ts b/web/crux/src/app/version/version.message.ts index 933c22163a..096464cbe5 100644 --- a/web/crux/src/app/version/version.message.ts +++ b/web/crux/src/app/version/version.message.ts @@ -1,5 +1,4 @@ -import { ImageConfigProperty } from '../image/image.const' -import { AddImagesDto, ImageDto, PatchImageDto } from '../image/image.dto' +import { AddImagesDto, ImageDto } from '../image/image.dto' export type GetImageMessage = { id: string @@ -12,6 +11,13 @@ export type AddImagesMessage = { registryImages: AddImagesDto[] } +export const WS_TYPE_IMAGE_SET_TAG = 'image-set-tag' +export const WS_TYPE_IMAGE_TAG_UPDATED = 'image-tag-updated' +export type ImageTagMessage = { + imageId: string + tag: string +} + export const WS_TYPE_IMAGES_ADDED = 'images-added' export type ImagesAddedMessage = { images: ImageDto[] @@ -26,13 +32,5 @@ export type ImageDeletedMessage = { imageId: string } -export const WS_TYPE_IMAGE_UPDATED = 'image-updated' -export type PatchImageMessage = PatchImageDto & { - id: string - resetSection?: ImageConfigProperty -} - -export const WS_TYPE_PATCH_RECEIVED = 'patch-received' - export const WS_TYPE_IMAGES_WERE_REORDERED = 'images-were-reordered' export type OrderImagesMessage = string[] diff --git a/web/crux/src/app/version/version.module.ts b/web/crux/src/app/version/version.module.ts index 3b1345852d..c0342c3a6a 100644 --- a/web/crux/src/app/version/version.module.ts +++ b/web/crux/src/app/version/version.module.ts @@ -7,23 +7,26 @@ import PrismaService from 'src/services/prisma.service' import AgentModule from '../agent/agent.module' import AuditLoggerModule from '../audit.logger/audit.logger.module' import AuditMapper from '../audit/audit.mapper' +import ContainerModule from '../container/container.module' import DeployModule from '../deploy/deploy.module' import EditorModule from '../editor/editor.module' import ImageModule from '../image/image.module' import TeamRepository from '../team/team.repository' import VersionChainHttpController from './version-chains.http.controller' +import VersionDomainEventListener from './version.domain-event.listener' import VersionHttpController from './version.http.controller' import VersionMapper from './version.mapper' import VersionService from './version.service' import VersionWebSocketGateway from './version.ws.gateway' @Module({ - imports: [ImageModule, HttpModule, DeployModule, AgentModule, EditorModule, AuditLoggerModule], + imports: [ImageModule, ContainerModule, HttpModule, DeployModule, AgentModule, EditorModule, AuditLoggerModule], exports: [VersionService, VersionMapper], controllers: [VersionHttpController, VersionChainHttpController], providers: [ VersionService, VersionMapper, + VersionDomainEventListener, PrismaService, TeamRepository, NotificationTemplateBuilder, diff --git a/web/crux/src/app/version/version.service.ts b/web/crux/src/app/version/version.service.ts index f5d32e63e1..de35ee10de 100644 --- a/web/crux/src/app/version/version.service.ts +++ b/web/crux/src/app/version/version.service.ts @@ -1,15 +1,22 @@ import { Injectable, Logger } from '@nestjs/common' import { Identity } from '@ory/kratos-client' import { DeploymentStatusEnum, Prisma } from '@prisma/client' +import { Observable, filter, map } from 'rxjs' +import { IMAGE_EVENT_ADD, IMAGE_EVENT_DELETE, ImageDeletedEvent, ImagesAddedEvent } from 'src/domain/domain-events' import { VersionMessage } from 'src/domain/notification-templates' import { versionChainMembersOf } from 'src/domain/version-chain' import { increaseIncrementalVersion } from 'src/domain/version-increase' import DomainNotificationService from 'src/services/domain.notification.service' import PrismaService from 'src/services/prisma.service' +import { DomainEvent } from 'src/shared/domain-event' +import { WsMessage } from 'src/websockets/common' import AgentService from '../agent/agent.service' +import ContainerMapper from '../container/container.mapper' +import DeployMapper from '../deploy/deploy.mapper' import { EditorLeftMessage, EditorMessage } from '../editor/editor.message' import EditorServiceProvider from '../editor/editor.service.provider' import ImageMapper from '../image/image.mapper' +import VersionDomainEventListener from './version.domain-event.listener' import { CreateVersionDto, IncreaseVersionDto, @@ -20,17 +27,21 @@ import { VersionListQuery, } from './version.dto' import VersionMapper from './version.mapper' +import { WS_TYPE_IMAGES_ADDED, WS_TYPE_IMAGE_DELETED } from './version.message' @Injectable() export default class VersionService { private readonly logger = new Logger(VersionService.name) constructor( - private prisma: PrismaService, - private mapper: VersionMapper, - private imageMapper: ImageMapper, - private notificationService: DomainNotificationService, - private agentService: AgentService, + private readonly prisma: PrismaService, + private readonly mapper: VersionMapper, + private readonly imageMapper: ImageMapper, + private readonly deployMapper: DeployMapper, + private readonly containerMapper: ContainerMapper, + private readonly domainEvents: VersionDomainEventListener, + private readonly notificationService: DomainNotificationService, + private readonly agentService: AgentService, private readonly editorServices: EditorServiceProvider, ) {} @@ -74,8 +85,15 @@ export default class VersionService { return versions > 0 } + subscribeToDomainEvents(versionId: string): Observable { + return this.domainEvents.watchEvents(versionId).pipe( + map(it => this.transformDomainEventToWsMessage(it)), + filter(it => !!it), + ) + } + async getVersionsByProjectId(projectId: string, user: Identity, query?: VersionListQuery): Promise { - const filter: Prisma.VersionWhereInput = { + const versionWhere: Prisma.VersionWhereInput = { name: query?.nameContains ? { contains: query.nameContains, @@ -92,7 +110,7 @@ export default class VersionService { project: { id: projectId, }, - ...filter, + ...versionWhere, }, }) @@ -192,6 +210,7 @@ export default class VersionService { }, deployments: { include: { + config: true, instances: { include: { config: true, @@ -248,20 +267,25 @@ export default class VersionService { if (defaultVersion) { const newImages = await Promise.all( defaultVersion.images.map(async image => { + const data = this.imageMapper.dbImageToCreateImageStatement(image) + const newImage = await prisma.image.create({ select: { id: true, }, data: { - ...image, - id: undefined, - versionId: newVersion.id, - config: { - create: { - ...this.imageMapper.dbContainerConfigToCreateImageStatement(image.config), - id: undefined, - }, - }, + ...data, + updatedAt: undefined, + updatedBy: undefined, + createdAt: undefined, + createdBy: identity.id, + registry: { connect: { id: image.registryId } }, + version: { connect: { id: newVersion.id } }, + config: !image.config + ? undefined + : { + create: this.containerMapper.dbConfigToCreateConfigStatement(image.config), + }, }, }) @@ -280,16 +304,32 @@ export default class VersionService { const deployments = await Promise.all( defaultVersion.deployments.map(async deployment => { + const data = this.deployMapper.dbDeploymentToCreateDeploymentStatement(deployment) + const newDeployment = await prisma.deployment.create({ select: { id: true, }, data: { - ...deployment, - id: undefined, + ...data, status: DeploymentStatusEnum.preparing, - versionId: newVersion.id, - environment: deployment.environment ?? [], + node: { + connect: { + id: deployment.nodeId, + }, + }, + version: { + connect: { + id: newVersion.id, + }, + }, + config: !deployment.config + ? undefined + : { + create: { + ...this.containerMapper.dbConfigToCreateConfigStatement(deployment.config), + }, + }, events: undefined, instances: undefined, protected: false, @@ -297,27 +337,32 @@ export default class VersionService { }) await Promise.all( - deployment.instances.map(it => - prisma.instance.create({ + deployment.instances.map(async it => { + await prisma.instance.create({ select: { id: true, }, data: { - ...it, - id: undefined, - deploymentId: newDeployment.id, - imageId: imageMap[it.imageId], - config: it.config - ? { + deployment: { + connect: { + id: newDeployment.id, + }, + }, + image: { + connect: { + id: imageMap[it.imageId], + }, + }, + config: !it.config + ? undefined + : { create: { - ...this.imageMapper.dbContainerConfigToCreateImageStatement(it.config), - id: undefined, + ...this.containerMapper.dbConfigToCreateConfigStatement(it.config), }, - } - : undefined, + }, }, - }), - ), + }) + }), ) }), ) @@ -432,6 +477,7 @@ export default class VersionService { ], }, include: { + config: true, instances: { include: { config: true, @@ -497,18 +543,19 @@ export default class VersionService { const { originalId } = image delete image.originalId + const data = this.imageMapper.dbImageToCreateImageStatement(image) + const createdImage = await prisma.image.create({ data: { - ...image, - versionId: version.id, + ...data, createdBy: identity.id, - config: { - create: this.imageMapper.dbContainerConfigToCreateImageStatement({ - ...image.config, - id: undefined, - imageId: undefined, - }), + registry: { + connect: { + id: image.registryId, + }, }, + version: { connect: { id: version.id } }, + config: { create: this.containerMapper.dbConfigToCreateConfigStatement(image.config) }, }, }) @@ -520,11 +567,22 @@ export default class VersionService { const imageIdMap = new Map(imageIdEntries) await Promise.all( increased.deployments.map(async deployment => { + const data = this.deployMapper.dbDeploymentToCreateDeploymentStatement(deployment) + const newDeployment = await prisma.deployment.create({ data: { - ...deployment, + ...data, createdBy: identity.id, - versionId: version.id, + node: { + connect: { + id: deployment.nodeId, + }, + }, + version: { + connect: { + id: version.id, + }, + }, instances: undefined, }, }) @@ -552,11 +610,7 @@ export default class VersionService { config: !instance.config ? undefined : { - create: this.imageMapper.dbContainerConfigToCreateImageStatement({ - ...instance.config, - id: undefined, - instanceId: undefined, - }), + create: this.containerMapper.dbConfigToCreateConfigStatement(instance.config), }, }, }) @@ -612,4 +666,23 @@ export default class VersionService { return message } + + private transformDomainEventToWsMessage(ev: DomainEvent): WsMessage { + switch (ev.type) { + case IMAGE_EVENT_ADD: + return { + type: WS_TYPE_IMAGES_ADDED, + data: this.mapper.imagesAddedEventToMessage(ev.event as ImagesAddedEvent), + } + case IMAGE_EVENT_DELETE: + return { + type: WS_TYPE_IMAGE_DELETED, + data: this.mapper.imageDeletedEventToMessage(ev.event as ImageDeletedEvent), + } + default: { + this.logger.error(`Unhandled domain event ${ev.type}`) + return null + } + } + } } diff --git a/web/crux/src/app/version/version.ws.gateway.ts b/web/crux/src/app/version/version.ws.gateway.ts index bff0791f80..79621bf52f 100644 --- a/web/crux/src/app/version/version.ws.gateway.ts +++ b/web/crux/src/app/version/version.ws.gateway.ts @@ -1,5 +1,6 @@ import { SubscribeMessage, WebSocketGateway } from '@nestjs/websockets' import { Identity } from '@ory/kratos-client' +import { takeUntil } from 'rxjs' import { AuditLogLevel } from 'src/decorators/audit-logger.decorator' import { WsAuthorize, WsClient, WsMessage, WsSubscribe, WsSubscription, WsUnsubscribe } from 'src/websockets/common' import SocketClient from 'src/websockets/decorators/ws.client.decorator' @@ -11,6 +12,7 @@ import { import WsParam from 'src/websockets/decorators/ws.param.decorator' import SocketMessage from 'src/websockets/decorators/ws.socket-message.decorator' import SocketSubscription from 'src/websockets/decorators/ws.subscription.decorator' +import { WS_TYPE_PATCH_RECEIVED } from '../container/container-config.message' import { EditorInitMessage, EditorLeftMessage, @@ -25,7 +27,6 @@ import { WS_TYPE_INPUT_FOCUSED, } from '../editor/editor.message' import EditorServiceProvider from '../editor/editor.service.provider' -import { PatchImageDto } from '../image/image.dto' import ImageService from '../image/image.service' import { IdentityFromSocket } from '../token/jwt-auth.guard' import { @@ -34,15 +35,15 @@ import { GetImageMessage, ImageDeletedMessage, ImageMessage, + ImageTagMessage, ImagesAddedMessage, OrderImagesMessage, - PatchImageMessage, WS_TYPE_IMAGE, WS_TYPE_IMAGES_ADDED, WS_TYPE_IMAGES_WERE_REORDERED, WS_TYPE_IMAGE_DELETED, - WS_TYPE_IMAGE_UPDATED, - WS_TYPE_PATCH_RECEIVED, + WS_TYPE_IMAGE_SET_TAG, + WS_TYPE_IMAGE_TAG_UPDATED, } from './version.message' import VersionService from './version.service' @@ -87,6 +88,11 @@ export default class VersionWebSocketGateway { data: me, }) + this.service + .subscribeToDomainEvents(versionId) + .pipe(takeUntil(subscription.getCompleter(client.token))) + .subscribe(message => subscription.sendToAll(message)) + return { type: WS_TYPE_EDITOR_INIT, data: { @@ -156,31 +162,26 @@ export default class VersionWebSocketGateway { subscription.sendToAll(res) } - @SubscribeMessage('patch-image') - async patchImage( + @SubscribeMessage(WS_TYPE_IMAGE_SET_TAG) + async setImageTag( @TeamSlug() teamSlug, @SocketClient() client: WsClient, - @SocketMessage() message: PatchImageMessage, + @SocketMessage() message: ImageTagMessage, @IdentityFromSocket() identity: Identity, @SocketSubscription() subscription: WsSubscription, ): Promise> { - let cruxReq: Pick = {} - - if (message.resetSection) { - cruxReq.config = {} - cruxReq.config[message.resetSection as string] = null - } else { - cruxReq = message - } - - await this.imageService.patchImage(teamSlug, message.id, cruxReq, identity) - - const res: WsMessage = { - type: WS_TYPE_IMAGE_UPDATED, - data: { - ...cruxReq, - id: message.id, + await this.imageService.patchImage( + teamSlug, + message.imageId, + { + tag: message.tag, }, + identity, + ) + + const res: WsMessage = { + type: WS_TYPE_IMAGE_TAG_UPDATED, + data: message, } subscription.sendToAllExcept(client, res) diff --git a/web/crux/src/domain/container-conflict.ts b/web/crux/src/domain/container-conflict.ts new file mode 100644 index 0000000000..6f81dfb59d --- /dev/null +++ b/web/crux/src/domain/container-conflict.ts @@ -0,0 +1,673 @@ +import { + ConcreteContainerConfigData, + ContainerConfigData, + ContainerConfigDataWithId, + ContainerPortRange, + Log, + Marker, + Port, + PortRange, + ResourceConfig, + Storage, + UniqueKeyValue, + Volume, +} from './container' + +export type ConflictedUniqueItem = { + key: string + configIds: string[] +} + +export type ConflictedPort = { + internal: number + configIds: string[] +} + +export type ConflictedPortRange = { + range: PortRange + configIds: string[] +} + +export type ConflictedLog = { + driver?: string[] + options?: ConflictedUniqueItem[] +} + +export type ConflictedResoureConfig = { + limits?: string[] + requests?: string[] +} + +export type ConflictedMarker = { + service?: ConflictedUniqueItem[] + deployment?: ConflictedUniqueItem[] + ingress?: ConflictedUniqueItem[] +} + +type StorageData = { + storageSet?: boolean + storageId?: string + storageConfig?: Storage +} + +type ConflictedLogKeys = { + driver?: boolean + options?: string[] +} + +type ConflictedResoureConfigKeys = Partial> +type ConflictedMarkerKeys = Partial> + +// config ids where the given property is present +export type ConflictedContainerConfigData = { + // common + name?: string[] + environment?: ConflictedUniqueItem[] + routing?: string[] + expose?: string[] + user?: string[] + workingDirectory?: string[] + tty?: string[] + configContainer?: string[] + ports?: number[] + portRanges?: ConflictedPortRange[] + volumes?: ConflictedUniqueItem[] + initContainers?: string[] + capabilities?: ConflictedUniqueItem[] + storage?: string[] + + // dagent + logConfig?: string[] + restartPolicy?: string[] + networkMode?: string[] + dockerLabels?: ConflictedUniqueItem[] + expectedState?: string[] + + // crane + deploymentStrategy?: string[] + proxyHeaders?: string[] + useLoadBalancer?: string[] + extraLBAnnotations?: ConflictedUniqueItem[] + healthCheckConfig?: string[] + resourceConfig?: string[] + annotations?: ConflictedMarker + labels?: ConflictedMarker + metrics?: string[] +} + +export const rangesOverlap = (one: PortRange, other: PortRange): boolean => one.from <= other.to && other.from <= one.to +export const rangesAreEqual = (one: PortRange, other: PortRange): boolean => + one.from === other.from && one.to === other.to + +const appendConflict = (conflicts: string[], oneId: string, otherId: string): string[] => { + if (!conflicts) { + return [oneId, otherId] + } + + if (!conflicts.includes(oneId)) { + conflicts.push(oneId) + } + + if (!conflicts.includes(otherId)) { + conflicts.push(otherId) + } + + return conflicts +} + +const appendUniqueItemConflicts = ( + conflicts: ConflictedUniqueItem[], + oneId: string, + otherId: string, + keys: string[], +): ConflictedUniqueItem[] => { + if (!conflicts) { + return keys.map(it => ({ + key: it, + configIds: [oneId, otherId], + })) + } + + keys.forEach(key => { + let conflict = conflicts.find(it => it.key === key) + if (!conflict) { + conflict = { + key, + configIds: null, + } + + conflicts.push(conflict) + } + + conflict.configIds = appendConflict(conflict.configIds, oneId, otherId) + }) + + return conflicts +} + +const appendPortConflicts = ( + conflicts: ConflictedPort[], + oneId: string, + otherId: string, + internalPorts: number[], +): ConflictedPort[] => { + if (!conflicts) { + return internalPorts.map(it => ({ + internal: it, + configIds: [oneId, otherId], + })) + } + + internalPorts.forEach(internalPort => { + let conflict = conflicts.find(it => internalPort === it.internal) + if (!conflict) { + conflict = { + internal: internalPort, + configIds: null, + } + + conflicts.push(conflict) + } + + conflict.configIds = appendConflict(conflict.configIds, oneId, otherId) + }) + + return conflicts +} + +const appendPortRangeConflicts = ( + conflicts: ConflictedPortRange[], + oneId: string, + otherId: string, + ranges: PortRange[], +): ConflictedPortRange[] => { + if (!conflicts) { + return ranges.map(it => ({ + range: it, + configIds: [oneId, otherId], + })) + } + + ranges.forEach(range => { + let conflict = conflicts.find(it => rangesAreEqual(it.range, range)) + if (!conflict) { + conflict = { + range, + configIds: null, + } + + conflicts.push(conflict) + } + + conflict.configIds = appendConflict(conflict.configIds, oneId, otherId) + }) + + return conflicts +} + +const appendLogConflict = ( + conflicts: ConflictedLog, + oneId: string, + otherId: string, + keys: ConflictedLogKeys, +): ConflictedLog => { + if (!conflicts) { + conflicts = {} + } + + if (keys.driver) { + conflicts.driver = appendConflict(conflicts.driver, oneId, otherId) + } + + if (keys.options) { + conflicts.options = appendUniqueItemConflicts(conflicts.options, oneId, otherId, keys.options) + } + + return conflicts +} + +const appendResourceConfigConflict = ( + conflicts: ConflictedResoureConfig, + oneId: string, + otherId: string, + keys: ConflictedResoureConfigKeys, +): ConflictedResoureConfig => { + if (!conflicts) { + conflicts = {} + } + + if (keys.limits) { + conflicts.limits = appendConflict(conflicts.limits, oneId, otherId) + } + + if (keys.requests) { + conflicts.requests = appendConflict(conflicts.requests, oneId, otherId) + } + + return conflicts +} + +const appendMarkerConflict = ( + conflicts: ConflictedMarker, + oneId: string, + otherId: string, + keys: ConflictedMarkerKeys, +): ConflictedMarker => { + if (!conflicts) { + conflicts = {} + } + + if (keys.deployment) { + conflicts.deployment = appendUniqueItemConflicts(conflicts.deployment, oneId, otherId, keys.deployment) + } + + if (keys.ingress) { + conflicts.ingress = appendUniqueItemConflicts(conflicts.ingress, oneId, otherId, keys.ingress) + } + + if (keys.service) { + conflicts.service = appendUniqueItemConflicts(conflicts.service, oneId, otherId, keys.service) + } + + return conflicts +} + +const stringsConflict = (one: string, other: string): boolean => one && other && one !== other +const booleansConflict = (one: boolean, other: boolean): boolean => { + if (typeof one !== 'boolean' || typeof other !== 'boolean') { + // some of them are null or uninterpretable + return false + } + + return one === other +} + +const numbersConflict = (one: number, other: number): boolean => { + if (typeof one !== 'number' || typeof other !== 'number') { + // some of them are null or uninterpretable + return false + } + + return one === other +} + +const objectsConflict = (one: object, other: object): boolean => { + if (typeof one !== 'object' || typeof other !== 'object') { + // some of them are null or uninterpretable + return false + } + + return JSON.stringify(one) === JSON.stringify(other) +} + +// returns the conflicting keys +const uniqueKeyValuesConflict = (one: UniqueKeyValue[], other: UniqueKeyValue[]): string[] | null => { + if (!one || !other) { + return null + } + + const conflicts = one + .filter(item => { + const otherItem = other.find(it => it.key === item.key) + + return item.value !== otherItem.value + }) + .map(it => it.key) + + if (conflicts.length < 1) { + return null + } + + return conflicts +} + +// returns the conflicting internal ports +const portsConflict = (one: Port[], other: Port[]): number[] | null => { + if (!one || !other) { + return null + } + + const conflicts = one.filter(item => other.find(it => it.internal === item.internal)).map(it => it.internal) + + if (conflicts.length < 1) { + return null + } + + return conflicts +} + +// returns the conflicting internal ranges +const portRangesConflict = (one: ContainerPortRange[], other: ContainerPortRange[]): PortRange[] | null => { + if (!one || !other) { + return null + } + + const conflicts = one + .filter(item => + other.find(it => rangesOverlap(item.internal, it.internal) || rangesOverlap(item.external, item.internal)), + ) + .map(it => it.internal) + + if (conflicts.length < 1) { + return null + } + + return conflicts +} + +// returns the conflicting paths +const volumesConflict = (one: Volume[], other: Volume[]): string[] | null => { + if (!one || !other) { + return null + } + + const conflicts = one + .filter(item => { + const otherItem = other.find(it => it.path === item.path) + + return objectsConflict(item, otherItem) + }) + .map(it => it.path) + + if (conflicts.length < 1) { + return null + } + + return conflicts +} + +const storagesConflict = (one: StorageData, other: StorageData): boolean => { + if (!one || !other) { + return false + } + + if (!one.storageSet || other.storageSet) { + return false + } + + return stringsConflict(one.storageId, other.storageId) || objectsConflict(one.storageConfig, other.storageConfig) +} + +const logsConflict = (one: Log, other: Log): ConflictedLogKeys | null => { + if (!one || !other) { + return null + } + + const driver = stringsConflict(one.driver, other.driver) + const options = uniqueKeyValuesConflict(one.options, other.options) + + const conflicts: ConflictedLogKeys = {} + + if (driver) { + conflicts.driver = driver + } + + if (options) { + conflicts.options = options + } + + if (Object.keys(conflicts).length < 1) { + return null + } + + return conflicts +} + +const resoureConfigsConflict = (one: ResourceConfig, other: ResourceConfig): ConflictedResoureConfigKeys | null => { + if (!one || !other) { + return null + } + + const conflicts: ConflictedResoureConfigKeys = { + limits: objectsConflict(one.limits, other.limits), + requests: objectsConflict(one.requests, other.requests), + } + + if (!Object.values(conflicts).find(it => it)) { + // no conflicts + return null + } + + return conflicts +} + +const markersConflict = (one: Marker, other: Marker): ConflictedMarkerKeys | null => { + if (!one || !other) { + return null + } + + const deployment = uniqueKeyValuesConflict(one.deployment, other.deployment) + const ingress = uniqueKeyValuesConflict(one.ingress, other.ingress) + const service = uniqueKeyValuesConflict(one.service, other.service) + + const conflicts: ConflictedMarkerKeys = {} + + if (deployment) { + conflicts.deployment = deployment + } + + if (ingress) { + conflicts.ingress = ingress + } + + if (service) { + conflicts.service = service + } + + if (Object.keys(conflicts).length < 1) { + return null + } + + return conflicts +} + +const collectConflicts = ( + conflicts: ConflictedContainerConfigData, + one: ContainerConfigDataWithId, + other: ContainerConfigDataWithId, +) => { + const checkStringConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as string + const otherValue = other[key] as string + + if (stringsConflict(oneValue, otherValue)) { + conflicts[key] = appendConflict(conflicts[key], one.id, other.id) + } + } + + const checkUniqueKeyValuesConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as UniqueKeyValue[] + const otherValue = other[key] as UniqueKeyValue[] + + const uniqueKeyValueConflicts = uniqueKeyValuesConflict(oneValue, otherValue) + if (uniqueKeyValueConflicts) { + conflicts[key] = appendUniqueItemConflicts(conflicts[key], one.id, other.id, uniqueKeyValueConflicts) + } + } + + const checkBooleanConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as boolean + const otherValue = other[key] as boolean + + if (booleansConflict(oneValue, otherValue)) { + conflicts[key] = appendConflict(conflicts[key], one.id, other.id) + } + } + + const checkNumberConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as number + const otherValue = other[key] as number + + if (numbersConflict(oneValue, otherValue)) { + conflicts[key] = appendConflict(conflicts[key], one.id, other.id) + } + } + + const checkObjectConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as object + const otherValue = other[key] as object + + if (objectsConflict(oneValue, otherValue)) { + conflicts[key] = appendConflict(conflicts[key], one.id, other.id) + } + } + + const checkPortsConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as Port[] + const otherValue = other[key] as Port[] + + const portsConflicts = portsConflict(oneValue, otherValue) + if (portsConflicts) { + conflicts[key] = appendPortConflicts(conflicts[key], one.id, other.id, portsConflicts) + } + } + + const checkPortRangesConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as ContainerPortRange[] + const otherValue = other[key] as ContainerPortRange[] + + const portRangesConflicts = portRangesConflict(oneValue, otherValue) + if (portRangesConflicts) { + conflicts[key] = appendPortRangeConflicts(conflicts[key], one.id, other.id, portRangesConflicts) + } + } + + const checkVolumesConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as Volume[] + const otherValue = other[key] as Volume[] + + const volumeConflicts = volumesConflict(oneValue, otherValue) + if (volumeConflicts) { + conflicts[key] = appendUniqueItemConflicts(conflicts[key], one.id, other.id, volumeConflicts) + } + } + + const checkStorageConflict = () => { + if (storagesConflict(one, other)) { + conflicts.storage = appendConflict(conflicts.storage, one.id, other.id) + } + } + + const checkLogsConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as Log + const otherValue = other[key] as Log + + const logConflicts = logsConflict(oneValue, otherValue) + if (logConflicts) { + conflicts[key] = appendLogConflict(conflicts[key], one.id, other.id, logConflicts) + } + } + + const checkResourceConfigConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as ResourceConfig + const otherValue = other[key] as ResourceConfig + + const resourceConfigConflicts = resoureConfigsConflict(oneValue, otherValue) + if (resourceConfigConflicts) { + conflicts[key] = appendResourceConfigConflict(conflicts[key], one.id, other.id, resourceConfigConflicts) + } + } + + const checkMarkerConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as Marker + const otherValue = other[key] as Marker + + const markerConflicts = markersConflict(oneValue, otherValue) + if (markerConflicts) { + conflicts[key] = appendMarkerConflict(conflicts[key], one.id, other.id, markerConflicts) + } + } + + // common + checkStringConflict('name') + checkUniqueKeyValuesConflict('environment') + // 'secrets' are keys only so duplicates are allowed + checkObjectConflict('routing') + checkStringConflict('expose') + checkNumberConflict('user') + checkStringConflict('workingDirectory') + checkBooleanConflict('tty') + checkObjectConflict('configContainer') + checkPortsConflict('ports') + checkPortRangesConflict('portRanges') + checkVolumesConflict('volumes') + // 'commands' are keys only so duplicates are allowed + // 'args' are keys only so duplicates are allowed + checkObjectConflict('initContainers') // TODO (@m8vago) compare them correctly after the init container rework + checkUniqueKeyValuesConflict('capabilities') + checkStorageConflict() + + // dagent + checkLogsConflict('logConfig') + checkStringConflict('restartPolicy') + checkStringConflict('networkMode') + // 'networks' are keys only so duplicates are allowed + checkUniqueKeyValuesConflict('dockerLabels') + checkStringConflict('expectedState') + + // crane + checkStringConflict('deploymentStrategy') + // 'customHeaders' are keys only so duplicates are allowed + checkBooleanConflict('proxyHeaders') + checkBooleanConflict('useLoadBalancer') + checkUniqueKeyValuesConflict('extraLBAnnotations') + checkObjectConflict('healthCheckConfig') + checkResourceConfigConflict('resourceConfig') + checkMarkerConflict('annotations') + checkMarkerConflict('labels') + checkObjectConflict('metrics') + + if (Object.keys(conflicts).length < 1) { + return null + } + + return conflicts +} + +type ContainerConfigDataProperty = keyof ContainerConfigData +export const checkForConflicts = ( + configs: ContainerConfigDataWithId[], + interestedProperties: ContainerConfigDataProperty[] = [], +): ConflictedContainerConfigData | null => { + configs = configs.map(conf => { + const newConf: ContainerConfigDataWithId = { + ...conf, + } + + Object.keys(conf).forEach(it => { + const prop = it as ContainerConfigDataProperty + if (prop === 'secrets') { + // secrets can not be overriden + return + } + + if (interestedProperties.includes(prop)) { + return + } + + delete newConf[prop] + }) + + return newConf + }) + + const conflicts: ConflictedContainerConfigData = {} + + configs.forEach(one => { + const others = configs.filter(it => it !== one) + + others.forEach(other => collectConflicts(conflicts, one, other)) + }) + + if (Object.keys(configs).length < 1) { + return null + } + + return conflicts +} + +export const getConflictsForConcreteConfig = ( + configs: ContainerConfigDataWithId[], + concreteConfig: ConcreteContainerConfigData, +): ConflictedContainerConfigData | null => + checkForConflicts(configs, Object.keys(concreteConfig) as ContainerConfigDataProperty[]) diff --git a/web/crux/src/domain/container-merge.spec.ts b/web/crux/src/domain/container-merge.spec.ts new file mode 100644 index 0000000000..7ab1249ef9 --- /dev/null +++ b/web/crux/src/domain/container-merge.spec.ts @@ -0,0 +1,528 @@ +import { ConcreteContainerConfigData, ContainerConfigData } from './container' +import { mergeConfigsWithConcreteConfig } from './container-merge' + +describe('container-merge', () => { + const fullConfig: ContainerConfigData = { + name: 'img', + capabilities: [], + deploymentStrategy: 'recreate', + expose: 'expose', + networkMode: 'bridge', + proxyHeaders: false, + restartPolicy: 'no', + tty: false, + useLoadBalancer: false, + annotations: { + deployment: [ + { + id: 'annotations.deployment', + key: 'annotations.deployment', + value: 'annotations.deployment', + }, + ], + ingress: [ + { + id: 'annotations.ingress', + key: 'annotations.ingress', + value: 'annotations.ingress', + }, + ], + service: [ + { + id: 'annotations.service', + key: 'annotations.service', + value: 'annotations.service', + }, + ], + }, + labels: { + deployment: [ + { + id: 'labels.deployment', + key: 'labels.deployment', + value: 'labels.deployment', + }, + ], + ingress: [ + { + id: 'labels.ingress', + key: 'labels.ingress', + value: 'labels.ingress', + }, + ], + service: [ + { + id: 'labels.service', + key: 'labels.service', + value: 'labels.service', + }, + ], + }, + args: [ + { + id: 'arg1', + key: 'arg1', + }, + ], + commands: [ + { + id: 'command1', + key: 'command1', + }, + ], + configContainer: { + image: 'configCont', + keepFiles: false, + path: 'configCont', + volume: 'configCont', + }, + customHeaders: [ + { + id: 'customHead', + key: 'customHead', + }, + ], + dockerLabels: [ + { + id: 'dockerLabel1', + key: 'dockerLabel1', + value: 'dockerLabel1', + }, + ], + environment: [ + { + id: 'env1', + key: 'env1', + value: 'env1', + }, + ], + extraLBAnnotations: [ + { + id: 'lbAnn1', + key: 'lbAnn1', + value: 'lbAnn1', + }, + ], + healthCheckConfig: { + livenessProbe: 'healthCheckConf', + port: 1, + readinessProbe: 'healthCheckConf', + startupProbe: 'healthCheckConf', + }, + storageSet: true, + storageId: 'storageId', + storageConfig: { + bucket: 'storageBucket', + path: 'storagePath', + }, + routing: { + domain: 'domain', + path: 'path', + stripPrefix: true, + uploadLimit: 'uploadLimit', + }, + initContainers: [ + { + id: 'initCont1', + args: [ + { + id: 'initCont1Args', + key: 'initCont1Args', + }, + ], + command: [ + { + id: 'initCont1Command', + key: 'initCont1Command', + }, + ], + environment: [ + { + id: 'initCont1Env', + key: 'initCont1Env', + value: 'initCont1Env', + }, + ], + image: 'initCont1', + name: 'initCont1', + useParentConfig: false, + volumes: [ + { + id: 'initCont1Vol1', + name: 'initCont1Vol1', + path: 'initCont1Vol1', + }, + ], + }, + ], + logConfig: { + driver: 'awslogs', + options: [ + { + id: 'logConfOps', + key: 'logConfOps', + value: 'logConfOps', + }, + ], + }, + networks: [ + { + id: 'network1', + key: 'network1', + }, + ], + portRanges: [ + { + id: 'portRange1', + external: { + from: 1, + to: 2, + }, + internal: { + from: 1, + to: 2, + }, + }, + ], + ports: [ + { + id: 'port1', + internal: 1, + external: 1, + }, + ], + resourceConfig: { + limits: { + cpu: 'limitCpu', + memory: 'limitMemory', + }, + requests: { + cpu: 'requestCpu', + memory: 'requestMemory', + }, + }, + secrets: [ + { + id: 'secret1', + key: 'secret1', + required: false, + }, + ], + user: 1, + volumes: [ + { + id: 'vol1', + name: 'vol1', + path: 'vol1', + class: 'vol1', + size: 'vol1', + type: 'mem', + }, + ], + metrics: null, + expectedState: null, + } + + const fullConcreteConfig: ConcreteContainerConfigData = { + name: 'instance.img', + capabilities: [], + deploymentStrategy: 'recreate', + expose: 'exposeWithTls', + networkMode: 'host', + proxyHeaders: true, + restartPolicy: 'onFailure', + tty: true, + useLoadBalancer: true, + annotations: { + deployment: [ + { + id: 'instance.annotations.deployment', + key: 'instance.annotations.deployment', + value: 'instance.annotations.deployment', + }, + ], + ingress: [ + { + id: 'instance.annotations.ingress', + key: 'instance.annotations.ingress', + value: 'instance.annotations.ingress', + }, + ], + service: [ + { + id: 'instance.annotations.service', + key: 'instance.annotations.service', + value: 'instance.annotations.service', + }, + ], + }, + labels: { + deployment: [ + { + id: 'instance.labels.deployment', + key: 'instance.labels.deployment', + value: 'instance.labels.deployment', + }, + ], + ingress: [ + { + id: 'instance.labels.ingress', + key: 'instance.labels.ingress', + value: 'instance.labels.ingress', + }, + ], + service: [ + { + id: 'instance.labels.service', + key: 'instance.labels.service', + value: 'instance.labels.service', + }, + ], + }, + args: [ + { + id: 'instance.arg1', + key: 'instance.arg1', + }, + ], + commands: [ + { + id: 'instance.command1', + key: 'instance.command1', + }, + ], + configContainer: { + image: 'instance.configCont', + keepFiles: true, + path: 'instance.configCont', + volume: 'instance.configCont', + }, + customHeaders: [ + { + id: 'instance.customHead', + key: 'instance.customHead', + }, + ], + dockerLabels: [ + { + id: 'instance.dockerLabel1', + key: 'instance.dockerLabel1', + value: 'instance.dockerLabel1', + }, + ], + environment: [ + { + id: 'instance.env1', + key: 'instance.env1', + value: 'instance.env1', + }, + ], + extraLBAnnotations: [ + { + id: 'instance.lbAnn1', + key: 'instance.lbAnn1', + value: 'instance.lbAnn1', + }, + ], + healthCheckConfig: { + livenessProbe: 'instance.healthCheckConf', + port: 1, + readinessProbe: 'instance.healthCheckConf', + startupProbe: 'instance.healthCheckConf', + }, + storageSet: true, + storageId: 'instance.storageId', + storageConfig: { + bucket: 'instance.storageBucket', + path: 'instance.storagePath', + }, + routing: { + domain: 'instance.domain', + path: 'instance.path', + stripPrefix: true, + uploadLimit: 'instance.uploadLimit', + }, + initContainers: [ + { + id: 'instance.initCont1', + args: [ + { + id: 'instance.initCont1Args', + key: 'instance.initCont1Args', + }, + ], + command: [ + { + id: 'instance.initCont1Command', + key: 'instance.initCont1Command', + }, + ], + environment: [ + { + id: 'instance.initCont1Env', + key: 'instance.initCont1Env', + value: 'instance.initCont1Env', + }, + ], + image: 'instance.initCont1', + name: 'instance.initCont1', + useParentConfig: true, + volumes: [ + { + id: 'instance.initCont1Vol1', + name: 'instance.initCont1Vol1', + path: 'instance.initCont1Vol1', + }, + ], + }, + ], + logConfig: { + driver: 'gcplogs', + options: [ + { + id: 'instance.logConfOps', + key: 'instance.logConfOps', + value: 'instance.logConfOps', + }, + ], + }, + networks: [ + { + id: 'instance.network1', + key: 'instance.network1', + }, + ], + portRanges: [ + { + id: 'instance.portRange1', + external: { + from: 10, + to: 20, + }, + internal: { + from: 10, + to: 20, + }, + }, + ], + ports: [ + { + id: 'instance.port1', + internal: 10, + external: 10, + }, + ], + resourceConfig: { + limits: { + cpu: 'instance.limitCpu', + memory: 'instance.limitMemory', + }, + requests: { + cpu: 'instance.requestCpu', + memory: 'instance.requestMemory', + }, + }, + secrets: [ + { + id: 'secret1', + key: 'instance.secret1', + required: false, + encrypted: true, + value: 'instance.secret1.publicKey', + publicKey: 'instance.secret1.publicKey', + }, + ], + user: 1, + volumes: [ + { + id: 'instance.vol1', + name: 'instance.vol1', + path: 'instance.vol1', + class: 'instance.vol1', + size: 'instance.vol1', + type: 'rwo', + }, + ], + metrics: null, + expectedState: null, + } + + describe('mergeConfigsWithConcreteConfig', () => { + it('should use the concrete variables when available', () => { + const merged = mergeConfigsWithConcreteConfig([fullConfig], fullConcreteConfig) + + expect(merged).toEqual(fullConcreteConfig) + }) + + it('should use the config variables when the concrete one is not available', () => { + const merged = mergeConfigsWithConcreteConfig([fullConfig], {}) + + const expected: ConcreteContainerConfigData = { + ...fullConfig, + secrets: [ + { + id: 'secret1', + key: 'secret1', + required: false, + encrypted: false, + value: '', + publicKey: null, + }, + ], + } + + expect(merged).toEqual(expected) + }) + + it('should use the instance only when available', () => { + const instance: ConcreteContainerConfigData = { + ports: fullConcreteConfig.ports, + labels: { + deployment: [ + { + id: 'instance.labels.deployment', + key: 'instance.labels.deployment', + value: 'instance.labels.deployment', + }, + ], + }, + annotations: { + service: [ + { + id: 'instance.annotations.service', + key: 'instance.annotations.service', + value: 'instance.annotations.service', + }, + ], + }, + } + + const expected: ConcreteContainerConfigData = { + ...fullConfig, + ports: fullConcreteConfig.ports, + labels: { + ...fullConfig.labels, + deployment: instance.labels.deployment, + }, + annotations: { + ...fullConfig.annotations, + service: instance.annotations.service, + }, + secrets: [ + { + id: 'secret1', + key: 'secret1', + required: false, + encrypted: false, + value: '', + publicKey: null, + }, + ], + } + + const merged = mergeConfigsWithConcreteConfig([fullConfig], instance) + + expect(merged).toEqual(expected) + }) + }) +}) diff --git a/web/crux/src/domain/container-merge.ts b/web/crux/src/domain/container-merge.ts new file mode 100644 index 0000000000..5fe1510dbb --- /dev/null +++ b/web/crux/src/domain/container-merge.ts @@ -0,0 +1,272 @@ +import { + ConcreteContainerConfigData, + ContainerConfigData, + ContainerPortRange, + Marker, + Port, + UniqueKey, + UniqueSecretKey, + UniqueSecretKeyValue, + Volume, +} from './container' +import { rangesOverlap } from './container-conflict' + +const mergeNumber = (strong: number, weak: number): number => { + if (typeof strong === 'number') { + return strong + } + + if (typeof weak === 'number') { + return weak + } + + return null +} + +const mergeBoolean = (strong: boolean, weak: boolean): boolean => { + if (typeof strong === 'boolean') { + return strong + } + + if (typeof weak === 'boolean') { + return weak + } + + return null +} + +type StorageProperties = Pick +const mergeStorage = (strong: StorageProperties, weak: StorageProperties): StorageProperties => { + const set = mergeBoolean(strong.storageSet, weak.storageSet) + if (!set) { + // neither of them are set + + return { + storageSet: false, + storageId: null, + storageConfig: null, + } + } + + if (typeof strong.storageSet === 'boolean') { + // strong is set + + return { + storageSet: true, + storageId: strong.storageId, + storageConfig: strong.storageConfig, + } + } + + return { + storageSet: true, + storageId: weak.storageId, + storageConfig: weak.storageConfig, + } +} + +export const mergeMarkers = (strong: Marker, weak: Marker): Marker => { + if (!strong) { + return weak ?? null + } + + if (!weak) { + return strong + } + + return { + deployment: strong.deployment ?? weak.deployment ?? [], + ingress: strong.ingress ?? weak.ingress ?? [], + service: strong.service ?? weak.service ?? [], + } +} + +const squashSecrets = (one: UniqueSecretKey[], other: UniqueSecretKey[]): UniqueSecretKey[] => { + if (!one) { + return other + } + + if (!other) { + return one + } + + return [...one, ...other.filter(it => !one.includes(it))] +} + +const squashConfigs = (configs: ContainerConfigData[]): ContainerConfigData => + configs.reduce((result, conf) => { + if ('secrets' in conf) { + conf.secrets = squashSecrets(result.secrets, conf.secrets) + } + + return { + ...result, + ...conf, + } + }, {} as ContainerConfigData) + +export const mergeSecrets = (strong: UniqueSecretKeyValue[], weak: UniqueSecretKey[]): UniqueSecretKeyValue[] => { + weak = weak ?? [] + strong = strong ?? [] + + const overriddenIds: Set = new Set(strong?.map(it => it.id)) + + const missing: UniqueSecretKeyValue[] = weak + .filter(it => !overriddenIds.has(it.id)) + .map(it => ({ + ...it, + value: '', + encrypted: false, + publicKey: null, + })) + + return [...missing, ...strong] +} + +// this assumes that the concrete config takes care of any conflict between the other configs +export const mergeConfigsWithConcreteConfig = ( + configs: ContainerConfigData[], + concrete: ConcreteContainerConfigData, +): ConcreteContainerConfigData => { + const squashed = squashConfigs(configs.filter(it => !!it)) + concrete = concrete ?? {} + + return { + // common + name: concrete.name ?? squashed.name, + environment: concrete.environment ?? squashed.environment, + secrets: mergeSecrets(concrete.secrets, squashed.secrets), + user: mergeNumber(concrete.user, squashed.user), + workingDirectory: concrete.workingDirectory ?? squashed.workingDirectory, + tty: mergeBoolean(concrete.tty, squashed.tty), + portRanges: concrete.portRanges ?? squashed.portRanges, + args: concrete.args ?? squashed.args, + commands: concrete.commands ?? squashed.commands, + expose: concrete.expose ?? squashed.expose, + configContainer: concrete.configContainer ?? squashed.configContainer, + routing: concrete.routing ?? squashed.routing, + volumes: concrete.volumes ?? squashed.volumes, + initContainers: concrete.initContainers ?? squashed.initContainers, + capabilities: [], // TODO (@m8vago, @nandor-magyar): capabilities feature is still missing + ports: concrete.ports ?? squashed.ports, + ...mergeStorage(concrete, squashed), + + // crane + customHeaders: concrete.customHeaders ?? squashed.customHeaders, + proxyHeaders: mergeBoolean(concrete.proxyHeaders, squashed.proxyHeaders), + extraLBAnnotations: concrete.extraLBAnnotations ?? squashed.extraLBAnnotations, + healthCheckConfig: concrete.healthCheckConfig ?? squashed.healthCheckConfig, + resourceConfig: concrete.resourceConfig ?? squashed.resourceConfig, + useLoadBalancer: mergeBoolean(concrete.useLoadBalancer, squashed.useLoadBalancer), + deploymentStrategy: concrete.deploymentStrategy ?? squashed.deploymentStrategy, + labels: mergeMarkers(concrete.labels, squashed.labels), + annotations: mergeMarkers(concrete.annotations, squashed.annotations), + metrics: concrete.metrics ?? squashed.metrics, + + // dagent + logConfig: concrete.logConfig ?? squashed.logConfig, + networkMode: concrete.networkMode ?? squashed.networkMode, + restartPolicy: concrete.restartPolicy ?? squashed.restartPolicy, + networks: concrete.networks ?? squashed.networks, + dockerLabels: concrete.dockerLabels ?? squashed.dockerLabels, + expectedState: concrete.expectedState ?? squashed.expectedState, + } +} + +const mergeUniqueKeys = (strong: T[], weak: T[]): T[] => { + if (!strong) { + return weak ?? null + } + + if (!weak) { + return strong + } + + const missing = weak.filter(w => !strong.find(it => it.key === w.key)) + return [...strong, ...missing] +} + +const mergePorts = (strong: Port[], weak: Port[]): Port[] => { + if (!strong) { + return weak ?? null + } + + if (!weak) { + return strong + } + + const missing = weak.filter(w => !strong.find(it => it.internal === w.internal)) + return [...strong, ...missing] +} + +const mergePortRanges = (strong: ContainerPortRange[], weak: ContainerPortRange[]): ContainerPortRange[] => { + if (!strong) { + return weak ?? null + } + + if (!weak) { + return strong + } + + const missing = weak.filter( + w => !strong.find(it => rangesOverlap(w.internal, it.internal) || rangesOverlap(w.external, it.external)), + ) + return [...strong, ...missing] +} + +const mergeVolumes = (strong: Volume[], weak: Volume[]): Volume[] => { + if (!strong) { + return weak ?? null + } + + if (!weak) { + return strong + } + + const missing = weak.filter(w => !strong.find(it => it.path === w.path || it.name === w.path)) + return [...strong, ...missing] +} + +export const mergeInstanceConfigWithDeploymentConfig = ( + deployment: ConcreteContainerConfigData, + instance: ConcreteContainerConfigData, +): ConcreteContainerConfigData => ({ + // common + name: instance.name ?? deployment.name ?? null, + environment: mergeUniqueKeys(instance.environment, deployment.environment), + secrets: mergeUniqueKeys(instance.secrets, deployment.secrets), + user: mergeNumber(instance.user, deployment.user), + workingDirectory: instance.workingDirectory ?? deployment.workingDirectory ?? null, + tty: mergeBoolean(instance.tty, deployment.tty), + ports: mergePorts(instance.ports, deployment.ports), + portRanges: mergePortRanges(instance.portRanges, deployment.portRanges), + args: mergeUniqueKeys(instance.args, deployment.args), + commands: mergeUniqueKeys(instance.commands, deployment.commands), + expose: instance.expose ?? deployment.expose ?? null, + configContainer: instance.configContainer ?? deployment.configContainer ?? null, + routing: instance.routing ?? deployment.routing ?? null, + volumes: mergeVolumes(instance.volumes, deployment.volumes), + initContainers: instance.initContainers ?? deployment.initContainers ?? null, // TODO (@m8vago): merge them correctly after the init container rework + capabilities: [], // TODO (@m8vago, @nandor-magyar): capabilities feature is still missing + ...mergeStorage(instance, deployment), + + // crane + customHeaders: mergeUniqueKeys(deployment.customHeaders, instance.customHeaders), + proxyHeaders: mergeBoolean(instance.proxyHeaders, deployment.proxyHeaders), + extraLBAnnotations: mergeUniqueKeys(instance.extraLBAnnotations, deployment.extraLBAnnotations), + healthCheckConfig: instance.healthCheckConfig ?? deployment.healthCheckConfig ?? null, + resourceConfig: instance.resourceConfig ?? deployment.resourceConfig ?? null, + useLoadBalancer: mergeBoolean(instance.useLoadBalancer, deployment.useLoadBalancer), + deploymentStrategy: instance.deploymentStrategy ?? deployment.deploymentStrategy ?? null, + labels: mergeMarkers(instance.labels, deployment.labels), + annotations: mergeMarkers(instance.annotations, deployment.annotations), + metrics: instance.metrics ?? deployment.metrics ?? null, + + // dagent + logConfig: instance.logConfig ?? deployment.logConfig ?? null, + networkMode: instance.networkMode ?? deployment.networkMode ?? null, + restartPolicy: instance.restartPolicy ?? deployment.restartPolicy ?? null, + networks: mergeUniqueKeys(instance.networks, deployment.networks), + dockerLabels: mergeUniqueKeys(instance.dockerLabels, deployment.dockerLabels), + expectedState: instance.expectedState ?? deployment.expectedState ?? null, +}) diff --git a/web/crux/src/domain/container.ts b/web/crux/src/domain/container.ts index 9e4b01da0c..e5ffe35a68 100644 --- a/web/crux/src/domain/container.ts +++ b/web/crux/src/domain/container.ts @@ -164,14 +164,14 @@ export type ExpectedContainerState = { export type ContainerConfigData = { // common - name: string + name?: string environment?: UniqueKeyValue[] secrets?: UniqueSecretKey[] routing?: Routing - expose: ContainerExposeStrategy + expose?: ContainerExposeStrategy user?: number workingDirectory?: string - tty: boolean + tty?: boolean configContainer?: Container ports?: Port[] portRanges?: ContainerPortRange[] @@ -179,24 +179,24 @@ export type ContainerConfigData = { commands?: UniqueKey[] args?: UniqueKey[] initContainers?: InitContainer[] - capabilities: UniqueKeyValue[] + capabilities?: UniqueKeyValue[] storageSet?: boolean storageId?: string storageConfig?: Storage // dagent logConfig?: Log - restartPolicy: ContainerRestartPolicyType - networkMode: NetworkMode + restartPolicy?: ContainerRestartPolicyType + networkMode?: NetworkMode networks?: UniqueKey[] dockerLabels?: UniqueKeyValue[] expectedState?: ExpectedContainerState // crane - deploymentStrategy: ContainerDeploymentStrategyType + deploymentStrategy?: ContainerDeploymentStrategyType customHeaders?: UniqueKey[] - proxyHeaders: boolean - useLoadBalancer: boolean + proxyHeaders?: boolean + useLoadBalancer?: boolean extraLBAnnotations?: UniqueKeyValue[] healthCheckConfig?: HealthCheck resourceConfig?: ResourceConfig @@ -205,8 +205,12 @@ export type ContainerConfigData = { metrics?: Metrics } -type DagentSpecificConfig = 'logConfig' | 'restartPolicy' | 'networkMode' | 'networks' | 'dockerLabels' -type CraneSpecificConfig = +export type ContainerConfigDataWithId = ContainerConfigData & { + id: string +} + +type DagentSpecificConfigKeys = 'logConfig' | 'restartPolicy' | 'networkMode' | 'networks' | 'dockerLabels' +type CraneSpecificConfigKeys = | 'deploymentStrategy' | 'customHeaders' | 'proxyHeaders' @@ -218,16 +222,14 @@ type CraneSpecificConfig = | 'annotations' | 'metrics' -export type DagentConfigDetails = Pick -export type CraneConfigDetails = Pick -export type CommonConfigDetails = Omit +export type DagentConfigDetails = Pick +export type CraneConfigDetails = Pick +export type CommonConfigDetails = Omit -export type MergedContainerConfigData = Omit & { - secrets: UniqueSecretKeyValue[] +export type ConcreteContainerConfigData = Omit & { + secrets?: UniqueSecretKeyValue[] } -export type InstanceContainerConfigData = Partial - export const CONTAINER_CONFIG_JSON_FIELDS = [ // Common 'environment', @@ -264,3 +266,5 @@ export const CONTAINER_CONFIG_DEFAULT_VALUES = { export const CONTAINER_CONFIG_COMPOSITE_FIELDS = { storage: ['storageSet', 'storageId', 'storageConfig'], } + +export const configIsEmpty = (config: T): boolean => Object.keys(config).length < 1 diff --git a/web/crux/src/domain/deployment.ts b/web/crux/src/domain/deployment.ts index ec6804ce08..5df892bcd3 100644 --- a/web/crux/src/domain/deployment.ts +++ b/web/crux/src/domain/deployment.ts @@ -16,7 +16,7 @@ import { containerStateToJSON, deploymentStatusToJSON, } from 'src/grpc/protobuf/proto/common' -import { ContainerState, MergedContainerConfigData, UniqueKeyValue } from './container' +import { ConcreteContainerConfigData, ContainerState } from './container' export type DeploymentLogLevel = 'info' | 'warn' | 'error' @@ -134,23 +134,43 @@ export type DeploymentNotification = { nodeName: string } +export type DeploymentOptions = { + request: VersionDeployRequest + notification: DeploymentNotification + instanceConfigs: Map + deploymentConfig: ConcreteContainerConfigData + tries: number +} + export default class Deployment { private statusChannel = new Subject() private status: DeploymentStatusEnum = 'preparing' - readonly id: string + private readonly request: VersionDeployRequest + + get id(): string { + return this.options.request.id + } + + get notification(): DeploymentNotification { + return this.options.notification + } - constructor( - private readonly request: VersionDeployRequest, - public notification: DeploymentNotification, - public mergedConfigs: Map, - public sharedEnvironment: UniqueKeyValue[], - public readonly tries: number, - ) { - this.id = request.id + get instanceConfigs(): Map { + return this.options.instanceConfigs } + get deploymentConfig(): ConcreteContainerConfigData { + return this.options.deploymentConfig + } + + get tries(): number { + return this.options.tries + } + + constructor(private readonly options: DeploymentOptions) {} + getStatus() { return this.status } diff --git a/web/crux/src/domain/domain-events.ts b/web/crux/src/domain/domain-events.ts new file mode 100644 index 0000000000..f7779f8fa3 --- /dev/null +++ b/web/crux/src/domain/domain-events.ts @@ -0,0 +1,58 @@ +import { ConfigBundle } from '@prisma/client' +import { ImageDetails } from 'src/app/image/image.mapper' +import { ContainerConfigData } from './container' + +// container +export const CONTAINER_CONFIG_EVENT_UPDATE = 'container-config.update' +export const CONTAINER_CONFIG_ANY = 'container-config.*' +export type ContainerConfigUpdatedEvent = { + id: string + patch: ContainerConfigData +} + +// image +export const IMAGE_EVENT_ADD = 'image.add' +export const IMAGE_EVENT_DELETE = 'image.delete' +export const IMAGE_EVENT_ANY = 'image.*' + +export type ImageEvent = { + versionId: string +} + +export type ImagesAddedEvent = ImageEvent & { + images: ImageDetails[] +} + +export type ImageDeletedEvent = Omit & { + imageId: string + instances: { + id: string + configId: string + deploymentId: string + }[] +} + +// deployment +export type DeploymentEditEvent = { + deploymentId: string +} + +export const DEPLOYMENT_EVENT_INSTACE_CREATE = 'deployment.instance.create' +export const DEPLOYMENT_EVENT_INSTACE_DELETE = 'deployment.instance.delete' + +export type InstanceDetails = { + id: string + configId: string + image: ImageDetails +} + +export type InstancesCreatedEvent = DeploymentEditEvent & { + instances: InstanceDetails[] +} + +export type InstanceDeletedEvent = DeploymentEditEvent & Omit + +export const DEPLOYMENT_EVENT_CONFIG_BUNDLES_UPDATE = 'deployment.config-bundles.update' +export type DeploymentConfigBundlesUpdatedEvent = DeploymentEditEvent & { + bundles: ConfigBundle[] +} diff --git a/web/crux/src/domain/start-deployment.ts b/web/crux/src/domain/start-deployment.ts new file mode 100644 index 0000000000..28e4c07621 --- /dev/null +++ b/web/crux/src/domain/start-deployment.ts @@ -0,0 +1,122 @@ +import { ContainerConfig, DeploymentStatusEnum, VersionTypeEnum } from '@prisma/client' +import { ConcreteContainerConfigData, ContainerConfigData, UniqueSecretKeyValue } from './container' +import { mergeConfigsWithConcreteConfig, mergeInstanceConfigWithDeploymentConfig } from './container-merge' + +export type InvalidSecrets = { + configId: string + invalid: string[] + secrets: UniqueSecretKeyValue[] +} + +export type MissingSecrets = { + configId: string + secretKeys: string[] +} + +export const missingSecretsOf = (configId: string, config: ConcreteContainerConfigData): MissingSecrets | null => { + if (!config?.secrets) { + return null + } + + const requiredAndMissingSecrets = config.secrets.filter(it => it.required && it.encrypted && it.value.length > 0) + if (requiredAndMissingSecrets.length < 1) { + return null + } + + return { + configId, + secretKeys: requiredAndMissingSecrets.map(it => it.key), + } +} + +export const collectInvalidSecrets = ( + configId: string, + config: ConcreteContainerConfigData, + publicKey: string, +): InvalidSecrets => { + if (!config?.secrets) { + return null + } + + const secrets = config.secrets as UniqueSecretKeyValue[] + const invalid = secrets.filter(it => it.publicKey !== publicKey).map(secret => secret.id) + + if (invalid.length < 1) { + return null + } + + return { + configId, + invalid, + secrets: secrets.map(secret => { + if (!invalid.includes(secret.id)) { + return secret + } + + return { + ...secret, + value: '', + encrypted: false, + publicKey, + } + }), + } +} + +type DeployableDeployment = { + version: { + type: VersionTypeEnum + } + status: DeploymentStatusEnum + config: ContainerConfig + configBundles: { + configBundle: { + config: ContainerConfig + } + }[] +} +export const deploymentConfigOf = (deployment: DeployableDeployment): ConcreteContainerConfigData => { + if ( + deployment.version.type !== 'rolling' && + (deployment.status === 'successful' || deployment.status === 'obsolete') + ) { + // this is a redeployment of a successful or an obsolete deployment of an incremental version + // we should not merge and use only the concrete configs + + return deployment.config as any as ConcreteContainerConfigData + } + + const configBundles = deployment.configBundles.map(it => it.configBundle.config as any as ContainerConfigData) + const deploymentConfig = deployment.config as any as ConcreteContainerConfigData + return mergeConfigsWithConcreteConfig(configBundles, deploymentConfig) +} + +type DeployableInstance = { + image: { + config: ContainerConfig + } + config: ContainerConfig +} +export const instanceConfigOf = ( + deployment: DeployableDeployment, + deploymentConfig: ConcreteContainerConfigData, + instance: DeployableInstance, +): ConcreteContainerConfigData => { + if ( + deployment.version.type !== 'rolling' && + (deployment.status === 'successful' || deployment.status === 'obsolete') + ) { + // this is a redeployment of a successful or an obsolete deployment of an incremental version + // we should not merge and use only the concrete configs + + return instance.config as any as ConcreteContainerConfigData + } + + // first we merge the deployment config with the image config to resolve secrets globally + const imageConfig = instance.image.config as any as ContainerConfigData + const mergedDeploymentConfig = mergeConfigsWithConcreteConfig([imageConfig], deploymentConfig) + + // then we merge and override the rest with the instance config + const instanceConfig = instance.config as any as ConcreteContainerConfigData + return mergeInstanceConfigWithDeploymentConfig(mergedDeploymentConfig, instanceConfig) +} diff --git a/web/crux/src/domain/utils.ts b/web/crux/src/domain/utils.ts index 67699c6a94..5224912be7 100644 --- a/web/crux/src/domain/utils.ts +++ b/web/crux/src/domain/utils.ts @@ -91,6 +91,22 @@ export const toPrismaJson = (val: T): T | JsonNull => { return val } +export const toNullableNumber = (val: number): number | null => { + if (typeof val === 'number') { + return val + } + + return null +} + +export const toNullableBoolean = (val: boolean): boolean | null => { + if (typeof val === 'boolean') { + return val + } + + return null +} + export const generateNonce = () => randomBytes(128).toString('hex') export const tapOnce = ( diff --git a/web/crux/src/domain/version-increase.ts b/web/crux/src/domain/version-increase.ts index 5abe740abd..68f10b6492 100644 --- a/web/crux/src/domain/version-increase.ts +++ b/web/crux/src/domain/version-increase.ts @@ -1,22 +1,15 @@ -import { - ContainerConfig, - Deployment, - DeploymentStatusEnum, - Image, - Instance, - InstanceContainerConfig, - Version, -} from '@prisma/client' +import { ContainerConfig, Deployment, DeploymentStatusEnum, Image, Instance, Version } from '@prisma/client' export type ImageWithConfig = Image & { config: ContainerConfig } type InstanceWithConfig = Instance & { - config: InstanceContainerConfig | null + config: ContainerConfig | null } type DeploymentWithInstances = Deployment & { + config: ContainerConfig | null instances: InstanceWithConfig[] } @@ -25,17 +18,18 @@ export type IncreasableVersion = Version & { deployments: DeploymentWithInstances[] } -type CopiedImageWithConfig = Omit & { +type CopiedImageWithConfig = Image & { originalId: string - config: Omit + config: Omit } -type CopiedInstanceWithConfig = Omit & { +type CopiedInstanceWithConfig = Omit & { originalImageId: string - config: Omit + config: Omit } -type CopiedDeploymentWithInstances = Omit & { +type CopiedDeploymentWithInstances = Deployment & { + config: Omit instances: CopiedInstanceWithConfig[] } @@ -44,22 +38,24 @@ export type IncreasedVersion = Omit { - const newInstance: CopiedInstanceWithConfig = { - originalImageId: instance.imageId, - updatedAt: undefined, - config: null, +const copyConfig = (config: ContainerConfig | null): Omit | null => { + if (!config) { + return null } - if (instance.config) { - const config = { - ...instance.config, - } + const newConf = { + ...config, + } + + delete newConf.id - delete config.id - delete config.instanceId + return newConf +} - newInstance.config = config +const copyInstance = (instance: InstanceWithConfig): CopiedInstanceWithConfig => { + const newInstance: CopiedInstanceWithConfig = { + originalImageId: instance.imageId, + config: copyConfig(instance.config), } return newInstance @@ -67,15 +63,14 @@ const copyInstance = (instance: InstanceWithConfig): CopiedInstanceWithConfig => export const copyDeployment = (deployment: DeploymentWithInstances): CopiedDeploymentWithInstances => { const newDeployment: CopiedDeploymentWithInstances = { - note: deployment.note, - prefix: deployment.prefix, + ...deployment, // default status for deployments is preparing status: DeploymentStatusEnum.preparing, - environment: deployment.environment ?? [], - nodeId: deployment.nodeId, - protected: deployment.protected, + config: copyConfig(deployment.config), tries: 0, instances: [], + createdAt: undefined, + createdBy: undefined, updatedAt: undefined, updatedBy: undefined, } @@ -90,23 +85,12 @@ export const copyDeployment = (deployment: DeploymentWithInstances): CopiedDeplo } const copyImage = (image: ImageWithConfig): CopiedImageWithConfig => { - const config = { - ...image.config, - } - - delete config.id - delete config.imageId + const config = copyConfig(image.config) const newImage: CopiedImageWithConfig = { + ...image, 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 diff --git a/web/crux/src/interceptors/prisma-error-interceptor.ts b/web/crux/src/interceptors/prisma-error-interceptor.ts index ff09645abf..ab87e573c7 100644 --- a/web/crux/src/interceptors/prisma-error-interceptor.ts +++ b/web/crux/src/interceptors/prisma-error-interceptor.ts @@ -94,7 +94,6 @@ export default class PrismaErrorInterceptor implements NestInterceptor { DeploymentEvent: 'deploymentEvent', DeploymentToken: 'deploymentToken', Instance: 'instance', - InstanceContainerConfig: 'instanceConfig', UserInvitation: 'invitation', VersionsOnParentVersion: 'versionRelation', UsersOnTeams: 'team', diff --git a/web/crux/src/shared/domain-event.ts b/web/crux/src/shared/domain-event.ts new file mode 100644 index 0000000000..cc36b08a9c --- /dev/null +++ b/web/crux/src/shared/domain-event.ts @@ -0,0 +1,4 @@ +export type DomainEvent = { + type: string + event: T +} diff --git a/web/crux/src/websockets/common.ts b/web/crux/src/websockets/common.ts index 8a2a44f9d1..37f2363e54 100644 --- a/web/crux/src/websockets/common.ts +++ b/web/crux/src/websockets/common.ts @@ -49,6 +49,7 @@ export type WsClientCallbacks = { } export interface WsSubscription { + getCompleter(clientToken: string): Observable getParameter(name: string): string onMessage(client: WsClient, message: WsMessage): Observable sendToAll(message: WsMessage): void diff --git a/web/crux/src/websockets/namespace.ts b/web/crux/src/websockets/namespace.ts index 6a28f2b28b..0c5d45b4dc 100644 --- a/web/crux/src/websockets/namespace.ts +++ b/web/crux/src/websockets/namespace.ts @@ -38,6 +38,11 @@ export default class WsNamespace implements WsSubscription { this.logger.verbose('Closed') } + getCompleter(clientToken: string): Observable { + const resources = this.clients.get(clientToken) + return resources.completer as Observable + } + getParameter(name: string): string | null { return this.params[name] ?? null } From 839cc1febc1d4c988fcc3ec4d4e595e55db0dc4c Mon Sep 17 00:00:00 2001 From: Mate Vago Date: Mon, 25 Nov 2024 15:02:11 +0100 Subject: [PATCH 02/32] fix: secret handling --- golang/internal/grpc/grpc.go | 83 +- golang/internal/mapper/grpc.go | 60 +- golang/internal/mapper/grpc_test.go | 10 +- golang/pkg/crane/crane.go | 1 + golang/pkg/crane/crux/deploy.go | 4 + golang/pkg/crane/k8s/delete_facade.go | 20 +- golang/pkg/crane/k8s/deploy_facade.go | 14 + golang/pkg/dagent/dagent.go | 1 + golang/pkg/dagent/utils/docker.go | 57 +- golang/pkg/dagent/utils/environment.go | 85 - golang/pkg/dagent/utils/prefix_file.go | 112 ++ ...nvironment_test.go => prefix_file_test.go} | 14 +- protobuf/go/agent/agent.pb.go | 1665 ++++++++--------- protobuf/go/common/common.pb.go | 471 ++--- protobuf/proto/agent.proto | 47 +- protobuf/proto/common.proto | 18 +- web/crux-ui/e2e/utils/config-bundle.ts | 8 +- web/crux-ui/e2e/utils/projects.ts | 2 +- .../deployment/deployment-copy.spec.ts | 2 +- .../deployment-mutability-versioned.spec.ts | 14 +- .../deployment-mutability-versionless.spec.ts | 10 +- .../e2e/with-login/resource-copy.spec.ts | 10 +- web/crux-ui/i18n.json | 1 + web/crux-ui/locales/en/common.json | 3 +- web/crux-ui/locales/en/container.json | 9 + web/crux-ui/locales/en/deployments.json | 3 +- web/crux-ui/locales/en/images.json | 2 - ...icon.svg => concrete_container_config.svg} | 0 .../concrete_container_config_turquoise.svg | 4 + ...e_config_icon.svg => container_config.svg} | 0 .../public/container_config_turquoise.svg | 4 + .../composer/converted-container.tsx | 8 +- .../config-bundles/config-bundle-card.tsx | 26 +- .../config-bundle-page-menu.tsx | 65 - ...e-card.tsx => edit-config-bundle-card.tsx} | 61 +- .../use-config-bundle-details-state.tsx | 161 -- .../common-config-section.tsx | 507 ++--- .../config-section-label.tsx | 41 + .../config-to-filters.ts | 12 +- .../container-config-filters.tsx} | 56 +- .../container-config-json-editor.tsx} | 15 +- .../crane-config-section.tsx | 102 +- .../dagent-config-section.tsx | 89 +- .../edit-container-config-card.tsx | 116 ++ .../edit-container-config-heading.tsx} | 8 +- .../extendable-item-list.tsx | 8 +- .../use-container-config-socket.ts | 38 + .../use-container-config-state.ts | 68 + .../use-patch-container-config.ts | 54 + .../src/components/dashboard/onboarding.tsx | 2 +- .../deployment-container-status-list.tsx | 21 +- .../deployment-details-section.tsx | 81 +- .../deployments/deployment-view-list.tsx | 16 +- .../deployments/deployment-view-tile.tsx | 35 +- .../deployments/edit-deployment-card.tsx | 76 +- .../deployments/edit-deployment-instances.tsx | 2 +- .../instances/edit-instance-card.tsx | 65 - .../instances/use-instance-state.ts | 203 +- .../deployments/use-deployment-state.tsx | 261 ++- .../components/nodes/node-containers-list.tsx | 10 +- .../versions/images/add-images-card.tsx | 4 +- .../images/config/config-section-label.tsx | 33 - .../versions/images/edit-image-card.tsx | 88 - .../versions/images/use-image-editor-state.ts | 110 -- .../projects/versions/use-version-state.ts | 6 +- .../versions/version-images-section.tsx | 2 +- .../projects/versions/version-view-list.tsx | 17 +- .../projects/versions/version-view-tile.tsx | 46 +- .../src/components/shared/key-only-input.tsx | 4 +- .../src/components/shared/key-value-input.tsx | 20 +- .../components/shared/secret-key-input.tsx | 2 +- .../shared/secret-key-value-input.tsx | 1 + web/crux-ui/src/const.ts | 3 +- web/crux-ui/src/elements/dyo-modal.tsx | 4 +- web/crux-ui/src/elements/dyo-multi-select.tsx | 84 +- web/crux-ui/src/hooks/use-deploy.ts | 19 +- web/crux-ui/src/hooks/use-throttleing.ts | 15 +- .../src/hooks/use-websocket-translation.ts | 1 + web/crux-ui/src/models/compose.ts | 16 +- web/crux-ui/src/models/config-bundle.ts | 16 +- web/crux-ui/src/models/container-config.ts | 38 +- web/crux-ui/src/models/container-conflict.ts | 676 +++++++ web/crux-ui/src/models/container-errors.ts | 194 ++ web/crux-ui/src/models/container-merge.ts | 240 +++ web/crux-ui/src/models/container.ts | 429 +++-- web/crux-ui/src/models/deployment.ts | 30 +- web/crux-ui/src/models/image.ts | 84 +- web/crux-ui/src/models/index.ts | 9 +- web/crux-ui/src/models/instance.ts | 9 +- web/crux-ui/src/models/registry.ts | 8 + .../src/pages/[teamSlug]/config-bundles.tsx | 11 +- .../config-bundles/[configBundleId].tsx | 118 +- .../container-configurations/[configId].tsx | 525 ++++++ .../[teamSlug]/deployments/[deploymentId].tsx | 2 +- .../[deploymentId]/instances/[instanceId].tsx | 312 --- .../versions/[versionId]/images/[imageId].tsx | 347 ---- web/crux-ui/src/routes.ts | 53 +- web/crux-ui/src/utils.ts | 2 +- web/crux-ui/src/validations/common.ts | 2 +- web/crux-ui/src/validations/config-bundle.ts | 10 +- web/crux-ui/src/validations/container.ts | 94 +- web/crux-ui/src/validations/deployment.ts | 7 +- web/crux-ui/src/validations/instance.ts | 10 +- web/crux-ui/src/websockets/common.ts | 2 +- .../websockets/websocket-client-endpoint.ts | 6 +- .../src/websockets/websocket-client-route.ts | 6 +- .../migration.sql | 72 +- web/crux/prisma/schema.prisma | 18 +- web/crux/proto/agent.proto | 47 +- web/crux/proto/common.proto | 18 +- .../agent.connection-strategy.provider.ts | 11 +- web/crux/src/app/agent/agent.module.ts | 8 +- web/crux/src/app/agent/agent.service.ts | 10 - .../agent.connection.legacy.strategy.ts | 98 - .../app/config.bundle/config.bundle.dto.ts | 3 + .../config.bundle.http.controller.ts | 23 +- .../app/config.bundle/config.bundle.mapper.ts | 22 +- .../app/config.bundle/config.bundle.module.ts | 12 +- .../config.bundle/config.bundle.service.ts | 15 +- .../container-config.http.service.ts | 84 + .../app/container/container-config.message.ts | 7 + .../app/container/container-config.service.ts | 189 +- .../container/container-config.ws.gateway.ts | 22 +- web/crux/src/app/container/container.dto.ts | 85 +- .../src/app/container/container.mapper.ts | 183 +- .../src/app/container/container.module.ts | 22 +- .../container-config.team-access.guard.ts | 22 + .../app/dashboard/dashboard.mapper.spec.ts | 24 +- .../src/app/dashboard/dashboard.mapper.ts | 6 +- .../src/app/dashboard/dashboard.service.ts | 22 +- web/crux/src/app/deploy/deploy.dto.ts | 69 +- .../src/app/deploy/deploy.http.controller.ts | 33 +- web/crux/src/app/deploy/deploy.mapper.spec.ts | 9 +- web/crux/src/app/deploy/deploy.mapper.ts | 120 +- web/crux/src/app/deploy/deploy.message.ts | 20 +- web/crux/src/app/deploy/deploy.module.ts | 4 +- web/crux/src/app/deploy/deploy.service.ts | 286 +-- web/crux/src/app/deploy/deploy.ws.gateway.ts | 19 +- .../interceptors/deploy.delete.interceptor.ts | 4 +- .../interceptors/deploy.patch.interceptor.ts | 4 +- .../interceptors/deploy.start.interceptor.ts | 12 +- web/crux/src/app/image/image.dto.ts | 9 +- .../src/app/image/image.http.controller.ts | 14 +- web/crux/src/app/image/image.mapper.ts | 37 +- web/crux/src/app/image/image.module.ts | 4 +- web/crux/src/app/image/image.service.ts | 15 +- web/crux/src/app/node/node.service.ts | 12 +- .../app/node/pipes/node.get-script.pipe.ts | 2 +- web/crux/src/app/package/package.service.ts | 61 +- web/crux/src/app/pipeline/pipeline.service.ts | 2 - web/crux/src/app/project/project.mapper.ts | 5 +- web/crux/src/app/project/project.module.ts | 4 +- web/crux/src/app/version/version.dto.ts | 6 +- web/crux/src/app/version/version.mapper.ts | 31 +- web/crux/src/app/version/version.message.ts | 8 +- web/crux/src/app/version/version.module.ts | 12 +- web/crux/src/app/version/version.service.ts | 12 +- .../src/app/version/version.ws.gateway.ts | 7 +- web/crux/src/domain/agent-callback.ts | 1 + web/crux/src/domain/agent.ts | 13 +- web/crux/src/domain/container-conflict.ts | 73 +- web/crux/src/domain/container-merge.ts | 98 +- web/crux/src/domain/container.ts | 11 + web/crux/src/domain/deployment.spec.ts | 8 +- web/crux/src/domain/deployment.ts | 56 +- web/crux/src/domain/domain-events.ts | 2 +- web/crux/src/domain/image.ts | 9 + web/crux/src/domain/start-deployment.ts | 49 +- web/crux/src/domain/validation.ts | 515 +++-- web/crux/src/domain/version.ts | 19 +- web/crux/src/grpc/protobuf/proto/agent.ts | 155 +- web/crux/src/grpc/protobuf/proto/common.ts | 54 +- web/crux/src/shared/const.ts | 1 + 173 files changed, 6505 insertions(+), 5059 deletions(-) delete mode 100644 golang/pkg/dagent/utils/environment.go create mode 100644 golang/pkg/dagent/utils/prefix_file.go rename golang/pkg/dagent/utils/{environment_test.go => prefix_file_test.go} (85%) rename web/crux-ui/public/{instance_config_icon.svg => concrete_container_config.svg} (100%) create mode 100644 web/crux-ui/public/concrete_container_config_turquoise.svg rename web/crux-ui/public/{image_config_icon.svg => container_config.svg} (100%) create mode 100644 web/crux-ui/public/container_config_turquoise.svg delete mode 100644 web/crux-ui/src/components/config-bundles/config-bundle-page-menu.tsx rename web/crux-ui/src/components/config-bundles/{add-config-bundle-card.tsx => edit-config-bundle-card.tsx} (58%) delete mode 100644 web/crux-ui/src/components/config-bundles/use-config-bundle-details-state.tsx rename web/crux-ui/src/components/{projects/versions/images/config => container-configs}/common-config-section.tsx (70%) create mode 100644 web/crux-ui/src/components/container-configs/config-section-label.tsx rename web/crux-ui/src/components/{projects/versions/images/config => container-configs}/config-to-filters.ts (71%) rename web/crux-ui/src/components/{projects/versions/images/image-config-filters.tsx => container-configs/container-config-filters.tsx} (71%) rename web/crux-ui/src/components/{projects/versions/images/edit-image-json.tsx => container-configs/container-config-json-editor.tsx} (83%) rename web/crux-ui/src/components/{projects/versions/images/config => container-configs}/crane-config-section.tsx (90%) rename web/crux-ui/src/components/{projects/versions/images/config => container-configs}/dagent-config-section.tsx (83%) create mode 100644 web/crux-ui/src/components/container-configs/edit-container-config-card.tsx rename web/crux-ui/src/components/{projects/versions/images/edit-image-heading.tsx => container-configs/edit-container-config-heading.tsx} (82%) rename web/crux-ui/src/components/{projects/versions/images/config => container-configs}/extendable-item-list.tsx (97%) create mode 100644 web/crux-ui/src/components/container-configs/use-container-config-socket.ts create mode 100644 web/crux-ui/src/components/container-configs/use-container-config-state.ts create mode 100644 web/crux-ui/src/components/container-configs/use-patch-container-config.ts delete mode 100644 web/crux-ui/src/components/deployments/instances/edit-instance-card.tsx delete mode 100644 web/crux-ui/src/components/projects/versions/images/config/config-section-label.tsx delete mode 100644 web/crux-ui/src/components/projects/versions/images/edit-image-card.tsx delete mode 100644 web/crux-ui/src/components/projects/versions/images/use-image-editor-state.ts create mode 100644 web/crux-ui/src/models/container-conflict.ts create mode 100644 web/crux-ui/src/models/container-errors.ts create mode 100644 web/crux-ui/src/models/container-merge.ts create mode 100644 web/crux-ui/src/pages/[teamSlug]/container-configurations/[configId].tsx delete mode 100644 web/crux-ui/src/pages/[teamSlug]/deployments/[deploymentId]/instances/[instanceId].tsx delete mode 100644 web/crux-ui/src/pages/[teamSlug]/projects/[projectId]/versions/[versionId]/images/[imageId].tsx delete mode 100644 web/crux/src/app/agent/connection-strategies/agent.connection.legacy.strategy.ts create mode 100644 web/crux/src/app/container/container-config.http.service.ts create mode 100644 web/crux/src/app/container/guards/container-config.team-access.guard.ts diff --git a/golang/internal/grpc/grpc.go b/golang/internal/grpc/grpc.go index 89ff2dd33c..1f4bc25753 100644 --- a/golang/internal/grpc/grpc.go +++ b/golang/internal/grpc/grpc.go @@ -78,6 +78,7 @@ type ClientLoop struct { type ( DeployFunc func(context.Context, *dogger.DeploymentLogger, *v1.DeployImageRequest, *v1.VersionData) error + DeploySharedSecretsFunc func(context.Context, string, map[string]string) error WatchContainerStatusFunc func(context.Context, string, bool) (*ContainerStatusStream, error) DeleteFunc func(context.Context, string, string) error SecretListFunc func(context.Context, string, string) ([]string, error) @@ -94,6 +95,7 @@ type ( type WorkerFunctions struct { Deploy DeployFunc + DeploySharedSecrets DeploySharedSecretsFunc WatchContainerStatus WatchContainerStatusFunc Delete DeleteFunc SecretList SecretListFunc @@ -186,7 +188,13 @@ func fetchCertificatesFromURL(ctx context.Context, addr string) (*x509.CertPool, func (cl *ClientLoop) grpcProcessCommand(command *agent.AgentCommand) { switch { case command.GetDeploy() != nil: - go executeVersionDeployRequest(cl.Ctx, command.GetDeploy(), cl.WorkerFuncs.Deploy, cl.AppConfig) + go executeDeployRequest( + cl.Ctx, + command.GetDeploy(), + cl.WorkerFuncs.Deploy, + cl.WorkerFuncs.DeploySharedSecrets, + cl.AppConfig, + ) case command.GetContainerState() != nil: go executeWatchContainerStatus(cl.Ctx, command.GetContainerState(), cl.WorkerFuncs.WatchContainerStatus) case command.GetContainerDelete() != nil: @@ -455,9 +463,9 @@ func (cl *ClientLoop) handleGrpcTokenError(err error, token *config.ValidJWT) { } } -func executeVersionDeployRequest( - ctx context.Context, req *agent.VersionDeployRequest, - deploy DeployFunc, appConfig *config.CommonConfiguration, +func executeDeployRequest( + ctx context.Context, req *agent.DeployRequest, + deploy DeployFunc, deploySecrets DeploySharedSecretsFunc, appConfig *config.CommonConfiguration, ) { if deploy == nil { log.Error().Msg("Deploy function not implemented") @@ -486,10 +494,27 @@ func executeVersionDeployRequest( return } - failed := false - var deployStatus common.DeploymentStatus + deployStatus := common.DeploymentStatus_FAILED + defer func() { + dog.WriteDeploymentStatus(deployStatus) + + err = statusStream.CloseSend() + if err != nil { + log.Error().Stack().Err(err).Str("deployment", req.Id).Msg("Status close error") + } + }() + + if len(req.Secrets) > 0 { + dog.WriteInfo("Deploying secrets") + err = deploySecrets(ctx, req.Prefix, req.Secrets) + if err != nil { + dog.WriteError(err.Error()) + return + } + } + for i := range req.Requests { - imageReq := mapper.MapDeployImage(req.Requests[i], appConfig) + imageReq := mapper.MapDeployImage(req.Prefix, req.Requests[i], appConfig) dog.SetRequestID(imageReq.RequestID) var versionData *v1.VersionData @@ -498,24 +523,12 @@ func executeVersionDeployRequest( } if err = deploy(ctx, dog, imageReq, versionData); err != nil { - failed = true dog.WriteError(err.Error()) + return } } - if failed { - deployStatus = common.DeploymentStatus_FAILED - } else { - deployStatus = common.DeploymentStatus_SUCCESSFUL - } - - dog.WriteDeploymentStatus(deployStatus) - - err = statusStream.CloseSend() - if err != nil { - log.Error().Stack().Err(err).Str("deployment", req.Id).Msg("Status close error") - return - } + deployStatus = common.DeploymentStatus_SUCCESSFUL } func streamContainerStatus( @@ -636,13 +649,10 @@ func executeDeleteMultipleContainers( req *common.DeleteContainersRequest, deleteFn DeleteContainersFunc, ) *AgentGrpcError { - var prefix, name string - if req.GetContainer() != nil { - prefix = req.GetContainer().Prefix - name = req.GetContainer().Name - } else { - prefix = req.GetPrefix() - name = "" + prefix, name, err := mapper.MapContainerOrPrefixToPrefixName(req.Target) + if err != nil { + log.Error().Err(err).Msg("Failed to delete multiple containers") + return agentError(ctx, err) } ctx = metadata.AppendToOutgoingContext(ctx, "dyo-container-prefix", prefix, "dyo-container-name", name) @@ -653,7 +663,7 @@ func executeDeleteMultipleContainers( log.Info().Msg("Deleting multiple containers") - err := deleteFn(ctx, req) + err = deleteFn(ctx, req) if err != nil { log.Error().Stack().Err(err).Msg("Failed to delete multiple containers") return agentError(ctx, err) @@ -741,8 +751,15 @@ func executeSecretList( listFunc SecretListFunc, appConfig *config.CommonConfiguration, ) *AgentGrpcError { - prefix := command.Container.Prefix - name := command.Container.Name + var prefix string + name := "" + + if command.Target.GetContainer() != nil { + prefix = command.Target.GetContainer().Prefix + name = command.Target.GetContainer().Name + } else { + prefix = command.Target.GetPrefix() + } ctx = metadata.AppendToOutgoingContext(ctx, "dyo-container-prefix", prefix, "dyo-container-name", name) @@ -765,10 +782,8 @@ func executeSecretList( } resp := &common.ListSecretsResponse{ - Prefix: prefix, - Name: name, + Target: command.Target, PublicKey: publicKey, - HasKeys: keys != nil, Keys: keys, } diff --git a/golang/internal/mapper/grpc.go b/golang/internal/mapper/grpc.go index c30b2aeb77..0d05f0683c 100644 --- a/golang/internal/mapper/grpc.go +++ b/golang/internal/mapper/grpc.go @@ -1,6 +1,7 @@ package mapper import ( + "errors" "fmt" "strings" "time" @@ -25,33 +26,18 @@ import ( corev1 "k8s.io/api/core/v1" ) -func mapInstanceConfig(in *agent.InstanceConfig) v1.InstanceConfig { - instanceConfig := v1.InstanceConfig{ - ContainerPreName: in.Prefix, - Name: in.Prefix, - SharedEnvironment: map[string]string{}, - } - - if in.RepositoryPrefix != nil { - instanceConfig.RepositoryPreName = *in.RepositoryPrefix - } - - if in.MountPath != nil { - instanceConfig.MountPath = *in.MountPath - } +var ErrNoTargetContainerOrPrefix = errors.New("no target container or prefix") - if in.Environment != nil { - instanceConfig.Environment = in.Environment - } - - return instanceConfig -} - -func MapDeployImage(req *agent.DeployRequest, appConfig *config.CommonConfiguration) *v1.DeployImageRequest { +func MapDeployImage(prefix string, req *agent.DeployWorkloadRequest, appConfig *config.CommonConfiguration) *v1.DeployImageRequest { res := &v1.DeployImageRequest{ - RequestID: req.Id, - InstanceConfig: mapInstanceConfig(req.InstanceConfig), - ContainerConfig: mapContainerConfig(req), + RequestID: req.Id, + InstanceConfig: v1.InstanceConfig{ + UseSharedEnvs: false, + Environment: map[string]string{}, + SharedEnvironment: map[string]string{}, + ContainerPreName: prefix, + }, + ContainerConfig: mapContainerConfig(prefix, req), ImageName: req.ImageName, Tag: req.Tag, Registry: req.Registry, @@ -68,22 +54,18 @@ func MapDeployImage(req *agent.DeployRequest, appConfig *config.CommonConfigurat v1.SetDeploymentDefaults(res, appConfig) - if req.RuntimeConfig != nil { - res.RuntimeConfig = v1.Base64JSONBytes(*req.RuntimeConfig) - } - if req.Registry != nil { res.Registry = req.Registry } return res } -func mapContainerConfig(in *agent.DeployRequest) v1.ContainerConfig { +func mapContainerConfig(prefix string, in *agent.DeployWorkloadRequest) v1.ContainerConfig { cc := in.Common containerConfig := v1.ContainerConfig{ Container: cc.Name, - ContainerPreName: in.InstanceConfig.Prefix, + ContainerPreName: prefix, Ports: MapPorts(cc.Ports), PortRanges: mapPortRanges(cc.PortRanges), Volumes: mapVolumes(cc.Volumes), @@ -692,3 +674,19 @@ func MapDockerContainerEventToContainerState(event string) common.ContainerState return common.ContainerState_CONTAINER_STATE_UNSPECIFIED } } + +func MapContainerOrPrefixToPrefixName(target *common.ContainerOrPrefix) (string, string, error) { + if target == nil { + return "", "", ErrNoTargetContainerOrPrefix + } + + if target.GetContainer() != nil { + return target.GetContainer().GetPrefix(), target.GetContainer().Name, nil + } + + if target.GetPrefix() == "" { + return "", "", ErrNoTargetContainerOrPrefix + } + + return target.GetPrefix(), "", nil +} diff --git a/golang/internal/mapper/grpc_test.go b/golang/internal/mapper/grpc_test.go index b4b28a4ae0..1189e5af3d 100644 --- a/golang/internal/mapper/grpc_test.go +++ b/golang/internal/mapper/grpc_test.go @@ -57,7 +57,7 @@ func TestMapDeployImageRequestRestartPolicies(t *testing.T) { } } -func testExpectedCommon(req *agent.DeployRequest) *v1.DeployImageRequest { +func testExpectedCommon(req *agent.DeployWorkloadRequest) *v1.DeployImageRequest { return &v1.DeployImageRequest{ RequestID: "testID", RegistryAuth: &image.RegistryAuth{ @@ -210,7 +210,7 @@ func TestMapDockerContainerEventToContainerState(t *testing.T) { assert.Equal(t, common.ContainerState_EXITED, MapDockerContainerEventToContainerState("die")) } -func testDeployRequest() *agent.DeployRequest { +func testDeployRequest() *agent.DeployWorkloadRequest { registry := "https://my-registry.com" runtimeCfg := "key1=val1,key2=val2" var uid int64 = 777 @@ -219,7 +219,7 @@ func testDeployRequest() *agent.DeployRequest { repoPrefix := "repo-prefix" strategy := common.ExposeStrategy_EXPOSE_WITH_TLS b := true - return &agent.DeployRequest{ + return &agent.DeployWorkloadRequest{ Id: "testID", ContainerName: "test-container", ImageName: "test-image", @@ -411,7 +411,7 @@ func testAppConfig() *config.CommonConfiguration { } } -func testDeployRequestWithLogDriver(driverType common.DriverType) *agent.DeployRequest { +func testDeployRequestWithLogDriver(driverType common.DriverType) *agent.DeployWorkloadRequest { request := testDeployRequest() if driverType == common.DriverType_NODE_DEFAULT { request.Dagent.LogConfig = nil @@ -421,7 +421,7 @@ func testDeployRequestWithLogDriver(driverType common.DriverType) *agent.DeployR return request } -func testExpectedCommonWithLogConfigType(req *agent.DeployRequest, logConfigType string) *v1.DeployImageRequest { +func testExpectedCommonWithLogConfigType(req *agent.DeployWorkloadRequest, logConfigType string) *v1.DeployImageRequest { expected := testExpectedCommon(req) if req.Dagent.LogConfig == nil { expected.ContainerConfig.LogConfig = nil diff --git a/golang/pkg/crane/crane.go b/golang/pkg/crane/crane.go index 3343fd7223..6cc77ca2b5 100644 --- a/golang/pkg/crane/crane.go +++ b/golang/pkg/crane/crane.go @@ -39,6 +39,7 @@ func Serve(cfg *config.Configuration, secretStore commonConfig.SecretStore) { grpcContext := grpc.WithGRPCConfig(context.Background(), cfg) grpc.Init(grpcContext, &cfg.CommonConfiguration, secretStore, &grpc.WorkerFunctions{ Deploy: k8s.Deploy, + DeploySharedSecrets: k8s.DeploySharedSecrets, WatchContainerStatus: crux.WatchDeploymentsByPrefix, Delete: k8s.Delete, ContainerCommand: crux.DeploymentCommand, diff --git a/golang/pkg/crane/crux/deploy.go b/golang/pkg/crane/crux/deploy.go index ec67fba3f3..e61703eb13 100644 --- a/golang/pkg/crane/crux/deploy.go +++ b/golang/pkg/crane/crux/deploy.go @@ -54,6 +54,10 @@ func GetSecretsList(ctx context.Context, prefix, name string) ([]string, error) cfg := grpc.GetConfigFromContext(ctx).(*config.Configuration) secretHandler := k8s.NewSecret(ctx, k8s.NewClient(cfg)) + if name == "" { + name = prefix + "-shared" + } + return secretHandler.ListSecrets(prefix, name) } diff --git a/golang/pkg/crane/k8s/delete_facade.go b/golang/pkg/crane/k8s/delete_facade.go index 97e4e64953..dec20fb386 100644 --- a/golang/pkg/crane/k8s/delete_facade.go +++ b/golang/pkg/crane/k8s/delete_facade.go @@ -2,11 +2,11 @@ package k8s import ( "context" - "fmt" "github.com/rs/zerolog/log" "github.com/dyrector-io/dyrectorio/golang/internal/grpc" + "github.com/dyrector-io/dyrectorio/golang/internal/mapper" "github.com/dyrector-io/dyrectorio/golang/pkg/crane/config" "github.com/dyrector-io/dyrectorio/protobuf/go/common" @@ -64,14 +64,18 @@ func (d *DeleteFacade) DeleteIngresses() error { // hard-delete if called with prefix name only without container name func DeleteMultiple(c context.Context, request *common.DeleteContainersRequest) error { cfg := grpc.GetConfigFromContext(c).(*config.Configuration) - if ns := request.GetContainer().GetPrefix(); ns != "" { - if deploymentName := request.GetContainer().GetName(); deploymentName != "" { - return Delete(c, ns, deploymentName) - } - del := NewDeleteFacade(c, ns, "", cfg) - return del.DeleteNamespace(ns) + + prefix, name, err := mapper.MapContainerOrPrefixToPrefixName(request.Target) + if err != nil { + return err } - return fmt.Errorf("invalid DeleteContainers request") + + if name != "" { + return Delete(c, prefix, name) + } + + del := NewDeleteFacade(c, prefix, "", cfg) + return del.DeleteNamespace(prefix) } // soft-delete: deployment,services,configmaps, ingresses diff --git a/golang/pkg/crane/k8s/deploy_facade.go b/golang/pkg/crane/k8s/deploy_facade.go index 5a922417d1..31d20edf40 100644 --- a/golang/pkg/crane/k8s/deploy_facade.go +++ b/golang/pkg/crane/k8s/deploy_facade.go @@ -262,6 +262,20 @@ func (d *DeployFacade) Clear() error { return nil } +func DeploySharedSecrets(c context.Context, prefix string, secrets map[string]string) error { + cfg := grpc.GetConfigFromContext(c).(*config.Configuration) + + k8sClient := NewClient(cfg) + secret := NewSecret(c, k8sClient) + + err := secret.applySecrets(prefix, prefix+"-shared", secrets) + if err != nil { + return fmt.Errorf("could not write secrets, aborting: %w", err) + } + + return nil +} + func Deploy(c context.Context, dog *dogger.DeploymentLogger, deployImageRequest *v1.DeployImageRequest, _ *v1.VersionData, ) error { diff --git a/golang/pkg/dagent/dagent.go b/golang/pkg/dagent/dagent.go index 5205deec31..09d7901ccd 100644 --- a/golang/pkg/dagent/dagent.go +++ b/golang/pkg/dagent/dagent.go @@ -36,6 +36,7 @@ func Serve(cfg *config.Configuration) { grpcContext := grpc.WithGRPCConfig(context.Background(), cfg) grpc.Init(grpcContext, &cfg.CommonConfiguration, cfg, &grpc.WorkerFunctions{ Deploy: utils.DeployImage, + DeploySharedSecrets: utils.DeploySharedSecrets, WatchContainerStatus: utils.ContainerStateStream, Delete: utils.DeleteContainerByPrefixAndName, SecretList: utils.SecretList, diff --git a/golang/pkg/dagent/utils/docker.go b/golang/pkg/dagent/utils/docker.go index 43dfe773b3..a43220ef0e 100644 --- a/golang/pkg/dagent/utils/docker.go +++ b/golang/pkg/dagent/utils/docker.go @@ -317,6 +317,21 @@ func waitForContainer( return <-errorChannel } +func DeploySharedSecrets(ctx context.Context, + prefix string, + secrets map[string]string, +) error { + cfg := grpc.GetConfigFromContext(ctx).(*config.Configuration) + + pf := NewSecretsFile(cfg.InternalMountPath, prefix) + err := pf.WriteVariables(secrets) + if err != nil { + return fmt.Errorf("could not write secrets, aborting: %w", err) + } + + return nil +} + //nolint:funlen,gocyclo // TODO(@nandor-magyar): refactor this function into smaller parts func DeployImage(ctx context.Context, dog *dogger.DeploymentLogger, @@ -348,11 +363,10 @@ func DeployImage(ctx context.Context, log.Debug().Str("name", deployImageRequest.ImageName).Str("full", expandedImageName).Msg("Image name parsed") logDeployInfo(dog, deployImageRequest, expandedImageName, containerName) + pf := NewSharedEnvPrefixFile(cfg.InternalMountPath, prefix) if len(deployImageRequest.InstanceConfig.SharedEnvironment) > 0 { - err = WriteSharedEnvironmentVariables( - cfg.InternalMountPath, - prefix, - deployImageRequest.InstanceConfig.SharedEnvironment) + + err = pf.WriteVariables(deployImageRequest.InstanceConfig.SharedEnvironment) if err != nil { dog.WriteError("could not write shared environment variables, aborting...", err.Error()) return err @@ -362,8 +376,7 @@ func DeployImage(ctx context.Context, var envMap map[string]string if deployImageRequest.InstanceConfig.UseSharedEnvs { - envMap, err = ReadSharedEnvironmentVariables(cfg.InternalMountPath, - prefix) + envMap, err = pf.ReadVariables() if err != nil { dog.WriteError("could not load shared environment variables, while useSharedEnvs is on, aborting...", err.Error()) return err @@ -540,6 +553,8 @@ func getContainerName(deployImageRequest *v1.DeployImageRequest) string { func getContainerPrefix(deployImageRequest *v1.DeployImageRequest) string { containerPrefix := "" + // TODO (@nandor-magyar): the line below is probably wrong, fix it if you dare + // soon to be removed though, as merging is done by crux if deployImageRequest.ContainerConfig.Container != "" { if deployImageRequest.InstanceConfig.MountPath != "" { containerPrefix = deployImageRequest.InstanceConfig.MountPath @@ -709,7 +724,19 @@ func setImageLabels(expandedImageName string, return labels, nil } -func SecretList(ctx context.Context, prefix, name string) ([]string, error) { +func SecretList(ctx context.Context, prefix string, name string) ([]string, error) { + if name == "" { + cfg := grpc.GetConfigFromContext(ctx).(*config.Configuration) + + pf := NewSecretsFile(cfg.InternalMountPath, prefix) + secrets, err := pf.ReadVariables() + if err != nil { + return []string{}, fmt.Errorf("could not read secrets, aborting: %w", err) + } + + return maps.Keys(secrets), nil + } + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) if err != nil { return nil, err @@ -717,7 +744,7 @@ func SecretList(ctx context.Context, prefix, name string) ([]string, error) { containers, err := cli.ContainerList(ctx, container.ListOptions{ All: true, - Filters: filters.NewArgs(filters.KeyValuePair{Key: "name", Value: fmt.Sprintf("^/?%s-%s$", prefix, name)}), + Filters: filters.NewArgs(filters.KeyValuePair{Key: "name", Value: fmt.Sprintf("^/?%s$", util.JoinV("-", prefix, name))}), }) if err != nil { return nil, err @@ -771,13 +798,15 @@ func ContainerCommand(ctx context.Context, command *common.ContainerCommandReque } func DeleteContainers(ctx context.Context, request *common.DeleteContainersRequest) error { - var err error - if request.GetContainer() != nil { - err = DeleteContainerByPrefixAndName(ctx, request.GetContainer().Prefix, request.GetContainer().Name) - } else if request.GetPrefix() != "" { - err = dockerHelper.DeleteContainersByLabel(ctx, label.GetPrefixLabelFilter(request.GetPrefix())) + prefix, name, err := mapper.MapContainerOrPrefixToPrefixName(request.Target) + if err != nil { + return err + } + + if name != "" { + err = DeleteContainerByPrefixAndName(ctx, prefix, name) } else { - log.Error().Msg("Unknown DeleteContainers request") + err = dockerHelper.DeleteContainersByLabel(ctx, label.GetPrefixLabelFilter(prefix)) } return err diff --git a/golang/pkg/dagent/utils/environment.go b/golang/pkg/dagent/utils/environment.go deleted file mode 100644 index 02d97a4f35..0000000000 --- a/golang/pkg/dagent/utils/environment.go +++ /dev/null @@ -1,85 +0,0 @@ -package utils - -import ( - "fmt" - "io/fs" - "os" - "path/filepath" - - "github.com/joho/godotenv" -) - -const dirPerm = 0o700 - -type SharedVariableParamError struct { - variable string -} - -func (e SharedVariableParamError) Error() string { - return fmt.Sprintf("variable %s was empty and it is necessary for shared environment variables", e.variable) -} - -func NewErrSharedVariableParamEmpty(param string) SharedVariableParamError { - return SharedVariableParamError{variable: param} -} - -func WriteSharedEnvironmentVariables(dataRoot, prefix string, in map[string]string) error { - var err error - err = validatePath(dataRoot, prefix) - if err != nil { - return err - } - - out, err := godotenv.Marshal(in) - if err != nil { - return err - } - sharedEnvDirPath := getSharedEnvDir(dataRoot, prefix) - sharedEnvFilePath := getSharedEnvPath(dataRoot, prefix) - - err = os.MkdirAll(sharedEnvDirPath, dirPerm) - if err != nil { - return err - } - - err = os.WriteFile(sharedEnvFilePath, []byte(out), fs.ModePerm) - if err != nil { - return err - } - - return nil -} - -func validatePath(dataRoot, prefix string) error { - if dataRoot == "" { - return NewErrSharedVariableParamEmpty("dataRoot") - } else if prefix == "" { - return NewErrSharedVariableParamEmpty("prefix") - } - - return nil -} - -func getSharedEnvDir(dataRoot, prefix string) string { - return filepath.Join(dataRoot, prefix) -} - -func getSharedEnvPath(dataRoot, prefix string) string { - return filepath.Join(getSharedEnvDir(dataRoot, prefix), ".shared-env") -} - -func ReadSharedEnvironmentVariables(dataRoot, prefix string) (map[string]string, error) { - var err error - err = validatePath(dataRoot, prefix) - if err != nil { - return nil, err - } - sharedEnvPath := getSharedEnvPath(dataRoot, prefix) - - sharedEnvsFile, err := os.ReadFile(sharedEnvPath) // #nosec G304 -- shared-envs are generated from prefix+name and those are RFC1039 - if err != nil { - return nil, err - } - - return godotenv.UnmarshalBytes(sharedEnvsFile) -} diff --git a/golang/pkg/dagent/utils/prefix_file.go b/golang/pkg/dagent/utils/prefix_file.go new file mode 100644 index 0000000000..f23aa11ef0 --- /dev/null +++ b/golang/pkg/dagent/utils/prefix_file.go @@ -0,0 +1,112 @@ +package utils + +import ( + "errors" + "fmt" + "io/fs" + "os" + "path/filepath" + + "github.com/joho/godotenv" +) + +const dirPerm = 0o700 + +type PrefixFileParamError struct { + variable string +} + +func (e PrefixFileParamError) Error() string { + return fmt.Sprintf("variable %s was empty and it is necessary for shared environment variables", e.variable) +} + +func NewErrPrefixFileParamEmpty(param string) PrefixFileParamError { + return PrefixFileParamError{variable: param} +} + +type prefixFile struct { + DataRoot string + Prefix string + FileName string +} + +func NewSharedEnvPrefixFile(dataRoot, prefix string) prefixFile { + return prefixFile{ + DataRoot: dataRoot, + Prefix: prefix, + FileName: ".shared-env", + } +} + +func NewSecretsFile(dataRoot, prefix string) prefixFile { + return prefixFile{ + DataRoot: dataRoot, + Prefix: prefix, + FileName: ".shared-secrets", + } +} + +func (pf *prefixFile) validatePath() error { + if pf.DataRoot == "" { + return NewErrPrefixFileParamEmpty("dataRoot") + } else if pf.Prefix == "" { + return NewErrPrefixFileParamEmpty("prefix") + } + + return nil +} + +func (pf *prefixFile) getDirectory() string { + return filepath.Join(pf.DataRoot, pf.Prefix) +} + +func (pf *prefixFile) getFilePath() string { + return filepath.Join(pf.getDirectory(), pf.FileName) +} + +func (pf *prefixFile) ReadVariables() (map[string]string, error) { + err := pf.validatePath() + if err != nil { + return nil, err + } + + filePath := pf.getFilePath() + + file, err := os.ReadFile(filePath) // #nosec G304 -- shared-envs are generated from prefix+name and those are RFC1039 + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return map[string]string{}, nil + } + + return nil, err + } + + return godotenv.UnmarshalBytes(file) +} + +func (pf *prefixFile) WriteVariables(in map[string]string) error { + var err error + err = pf.validatePath() + if err != nil { + return err + } + + out, err := godotenv.Marshal(in) + if err != nil { + return err + } + dirPath := pf.getDirectory() + filePath := pf.getFilePath() + + err = os.MkdirAll(dirPath, dirPerm) + if err != nil { + return err + } + + err = os.WriteFile(filePath, []byte(out), fs.ModePerm) + if err != nil { + return err + } + + return nil +} diff --git a/golang/pkg/dagent/utils/environment_test.go b/golang/pkg/dagent/utils/prefix_file_test.go similarity index 85% rename from golang/pkg/dagent/utils/environment_test.go rename to golang/pkg/dagent/utils/prefix_file_test.go index a4d12bbbcb..8d656ed38f 100644 --- a/golang/pkg/dagent/utils/environment_test.go +++ b/golang/pkg/dagent/utils/prefix_file_test.go @@ -14,7 +14,7 @@ import ( "github.com/stretchr/testify/assert" ) -func TestWriteSharedEnvironmentVariables(t *testing.T) { +func TestWriteVariables(t *testing.T) { tests := []struct { name string dataRoot string @@ -37,13 +37,15 @@ func TestWriteSharedEnvironmentVariables(t *testing.T) { prefix: "prefix", instanceName: "instance", inputVariables: map[string]string{"key1": "value1", "key2": "value2"}, - expectedError: utils.NewErrSharedVariableParamEmpty("dataRoot"), + expectedError: utils.NewErrPrefixFileParamEmpty("dataRoot"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := utils.WriteSharedEnvironmentVariables(tt.dataRoot, tt.prefix, tt.inputVariables) + pf := utils.NewSharedEnvPrefixFile(tt.dataRoot, tt.prefix) + + err := pf.WriteVariables(tt.inputVariables) assert.Equal(t, tt.expectedError, err) if err == nil { @@ -60,7 +62,7 @@ func TestWriteSharedEnvironmentVariables(t *testing.T) { } } -func TestReadSharedEnvironmentVariables(t *testing.T) { +func TestReadVariables(t *testing.T) { tests := []struct { name string dataRoot string @@ -81,6 +83,8 @@ func TestReadSharedEnvironmentVariables(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + pf := utils.NewSharedEnvPrefixFile(tt.dataRoot, tt.prefix) + sharedEnvPath := filepath.Join(tt.dataRoot, tt.prefix, ".shared-env") err := os.WriteFile(sharedEnvPath, []byte(tt.fileContent), fs.ModePerm) assert.NoError(t, err) @@ -90,7 +94,7 @@ func TestReadSharedEnvironmentVariables(t *testing.T) { assert.NoError(t, removeErr) }() - result, err := utils.ReadSharedEnvironmentVariables(tt.dataRoot, tt.prefix) + result, err := pf.ReadVariables() assert.Equal(t, tt.expectedError, err) assert.Equal(t, tt.expectedResult, result) }) diff --git a/protobuf/go/agent/agent.pb.go b/protobuf/go/agent/agent.pb.go index fb3337b1fc..819dd03c05 100644 --- a/protobuf/go/agent/agent.pb.go +++ b/protobuf/go/agent/agent.pb.go @@ -215,7 +215,7 @@ func (m *AgentCommand) GetCommand() isAgentCommand_Command { return nil } -func (x *AgentCommand) GetDeploy() *VersionDeployRequest { +func (x *AgentCommand) GetDeploy() *DeployRequest { if x, ok := x.GetCommand().(*AgentCommand_Deploy); ok { return x.Deploy } @@ -304,7 +304,7 @@ type isAgentCommand_Command interface { } type AgentCommand_Deploy struct { - Deploy *VersionDeployRequest `protobuf:"bytes,1,opt,name=deploy,proto3,oneof"` + Deploy *DeployRequest `protobuf:"bytes,1,opt,name=deploy,proto3,oneof"` } type AgentCommand_ContainerState struct { @@ -588,19 +588,21 @@ func (x *DeployResponse) GetStarted() bool { return false } -type VersionDeployRequest struct { +type DeployRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - VersionName string `protobuf:"bytes,2,opt,name=versionName,proto3" json:"versionName,omitempty"` - ReleaseNotes string `protobuf:"bytes,3,opt,name=releaseNotes,proto3" json:"releaseNotes,omitempty"` - Requests []*DeployRequest `protobuf:"bytes,4,rep,name=requests,proto3" json:"requests,omitempty"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + VersionName string `protobuf:"bytes,2,opt,name=versionName,proto3" json:"versionName,omitempty"` + ReleaseNotes string `protobuf:"bytes,3,opt,name=releaseNotes,proto3" json:"releaseNotes,omitempty"` + Prefix string `protobuf:"bytes,4,opt,name=prefix,proto3" json:"prefix,omitempty"` + Secrets map[string]string `protobuf:"bytes,5,rep,name=secrets,proto3" json:"secrets,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + Requests []*DeployWorkloadRequest `protobuf:"bytes,6,rep,name=requests,proto3" json:"requests,omitempty"` } -func (x *VersionDeployRequest) Reset() { - *x = VersionDeployRequest{} +func (x *DeployRequest) Reset() { + *x = DeployRequest{} if protoimpl.UnsafeEnabled { mi := &file_protobuf_proto_agent_proto_msgTypes[5] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -608,13 +610,13 @@ func (x *VersionDeployRequest) Reset() { } } -func (x *VersionDeployRequest) String() string { +func (x *DeployRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*VersionDeployRequest) ProtoMessage() {} +func (*DeployRequest) ProtoMessage() {} -func (x *VersionDeployRequest) ProtoReflect() protoreflect.Message { +func (x *DeployRequest) ProtoReflect() protoreflect.Message { mi := &file_protobuf_proto_agent_proto_msgTypes[5] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -626,33 +628,47 @@ func (x *VersionDeployRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use VersionDeployRequest.ProtoReflect.Descriptor instead. -func (*VersionDeployRequest) Descriptor() ([]byte, []int) { +// Deprecated: Use DeployRequest.ProtoReflect.Descriptor instead. +func (*DeployRequest) Descriptor() ([]byte, []int) { return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{5} } -func (x *VersionDeployRequest) GetId() string { +func (x *DeployRequest) GetId() string { if x != nil { return x.Id } return "" } -func (x *VersionDeployRequest) GetVersionName() string { +func (x *DeployRequest) GetVersionName() string { if x != nil { return x.VersionName } return "" } -func (x *VersionDeployRequest) GetReleaseNotes() string { +func (x *DeployRequest) GetReleaseNotes() string { if x != nil { return x.ReleaseNotes } return "" } -func (x *VersionDeployRequest) GetRequests() []*DeployRequest { +func (x *DeployRequest) GetPrefix() string { + if x != nil { + return x.Prefix + } + return "" +} + +func (x *DeployRequest) GetSecrets() map[string]string { + if x != nil { + return x.Secrets + } + return nil +} + +func (x *DeployRequest) GetRequests() []*DeployWorkloadRequest { if x != nil { return x.Requests } @@ -665,7 +681,7 @@ type ListSecretsRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Container *common.ContainerIdentifier `protobuf:"bytes,1,opt,name=container,proto3" json:"container,omitempty"` + Target *common.ContainerOrPrefix `protobuf:"bytes,1,opt,name=target,proto3" json:"target,omitempty"` } func (x *ListSecretsRequest) Reset() { @@ -700,88 +716,13 @@ func (*ListSecretsRequest) Descriptor() ([]byte, []int) { return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{6} } -func (x *ListSecretsRequest) GetContainer() *common.ContainerIdentifier { +func (x *ListSecretsRequest) GetTarget() *common.ContainerOrPrefix { if x != nil { - return x.Container + return x.Target } return nil } -// * -// Deploys a single container -type InstanceConfig struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - // prefix mapped into host folder structure, - // used as namespace id - Prefix string `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"` - MountPath *string `protobuf:"bytes,2,opt,name=mountPath,proto3,oneof" json:"mountPath,omitempty"` // mount path of instance (docker only) - Environment map[string]string `protobuf:"bytes,3,rep,name=environment,proto3" json:"environment,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` // environment variable map - RepositoryPrefix *string `protobuf:"bytes,4,opt,name=repositoryPrefix,proto3,oneof" json:"repositoryPrefix,omitempty"` // registry repo prefix -} - -func (x *InstanceConfig) Reset() { - *x = InstanceConfig{} - if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[7] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *InstanceConfig) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*InstanceConfig) ProtoMessage() {} - -func (x *InstanceConfig) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[7] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use InstanceConfig.ProtoReflect.Descriptor instead. -func (*InstanceConfig) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{7} -} - -func (x *InstanceConfig) GetPrefix() string { - if x != nil { - return x.Prefix - } - return "" -} - -func (x *InstanceConfig) GetMountPath() string { - if x != nil && x.MountPath != nil { - return *x.MountPath - } - return "" -} - -func (x *InstanceConfig) GetEnvironment() map[string]string { - if x != nil { - return x.Environment - } - return nil -} - -func (x *InstanceConfig) GetRepositoryPrefix() string { - if x != nil && x.RepositoryPrefix != nil { - return *x.RepositoryPrefix - } - return "" -} - type RegistryAuth struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -796,7 +737,7 @@ type RegistryAuth struct { func (x *RegistryAuth) Reset() { *x = RegistryAuth{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[8] + mi := &file_protobuf_proto_agent_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -809,7 +750,7 @@ func (x *RegistryAuth) String() string { func (*RegistryAuth) ProtoMessage() {} func (x *RegistryAuth) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[8] + mi := &file_protobuf_proto_agent_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -822,7 +763,7 @@ func (x *RegistryAuth) ProtoReflect() protoreflect.Message { // Deprecated: Use RegistryAuth.ProtoReflect.Descriptor instead. func (*RegistryAuth) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{8} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{7} } func (x *RegistryAuth) GetName() string { @@ -865,7 +806,7 @@ type Port struct { func (x *Port) Reset() { *x = Port{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[9] + mi := &file_protobuf_proto_agent_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -878,7 +819,7 @@ func (x *Port) String() string { func (*Port) ProtoMessage() {} func (x *Port) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[9] + mi := &file_protobuf_proto_agent_proto_msgTypes[8] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -891,7 +832,7 @@ func (x *Port) ProtoReflect() protoreflect.Message { // Deprecated: Use Port.ProtoReflect.Descriptor instead. func (*Port) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{9} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{8} } func (x *Port) GetInternal() int32 { @@ -920,7 +861,7 @@ type PortRange struct { func (x *PortRange) Reset() { *x = PortRange{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[10] + mi := &file_protobuf_proto_agent_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -933,7 +874,7 @@ func (x *PortRange) String() string { func (*PortRange) ProtoMessage() {} func (x *PortRange) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[10] + mi := &file_protobuf_proto_agent_proto_msgTypes[9] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -946,7 +887,7 @@ func (x *PortRange) ProtoReflect() protoreflect.Message { // Deprecated: Use PortRange.ProtoReflect.Descriptor instead. func (*PortRange) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{10} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{9} } func (x *PortRange) GetFrom() int32 { @@ -975,7 +916,7 @@ type PortRangeBinding struct { func (x *PortRangeBinding) Reset() { *x = PortRangeBinding{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[11] + mi := &file_protobuf_proto_agent_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -988,7 +929,7 @@ func (x *PortRangeBinding) String() string { func (*PortRangeBinding) ProtoMessage() {} func (x *PortRangeBinding) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[11] + mi := &file_protobuf_proto_agent_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1001,7 +942,7 @@ func (x *PortRangeBinding) ProtoReflect() protoreflect.Message { // Deprecated: Use PortRangeBinding.ProtoReflect.Descriptor instead. func (*PortRangeBinding) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{11} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{10} } func (x *PortRangeBinding) GetInternal() *PortRange { @@ -1033,7 +974,7 @@ type Volume struct { func (x *Volume) Reset() { *x = Volume{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[12] + mi := &file_protobuf_proto_agent_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1046,7 +987,7 @@ func (x *Volume) String() string { func (*Volume) ProtoMessage() {} func (x *Volume) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[12] + mi := &file_protobuf_proto_agent_proto_msgTypes[11] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1059,7 +1000,7 @@ func (x *Volume) ProtoReflect() protoreflect.Message { // Deprecated: Use Volume.ProtoReflect.Descriptor instead. func (*Volume) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{12} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{11} } func (x *Volume) GetName() string { @@ -1109,7 +1050,7 @@ type VolumeLink struct { func (x *VolumeLink) Reset() { *x = VolumeLink{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[13] + mi := &file_protobuf_proto_agent_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1122,7 +1063,7 @@ func (x *VolumeLink) String() string { func (*VolumeLink) ProtoMessage() {} func (x *VolumeLink) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[13] + mi := &file_protobuf_proto_agent_proto_msgTypes[12] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1135,7 +1076,7 @@ func (x *VolumeLink) ProtoReflect() protoreflect.Message { // Deprecated: Use VolumeLink.ProtoReflect.Descriptor instead. func (*VolumeLink) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{13} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{12} } func (x *VolumeLink) GetName() string { @@ -1169,7 +1110,7 @@ type InitContainer struct { func (x *InitContainer) Reset() { *x = InitContainer{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[14] + mi := &file_protobuf_proto_agent_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1182,7 +1123,7 @@ func (x *InitContainer) String() string { func (*InitContainer) ProtoMessage() {} func (x *InitContainer) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[14] + mi := &file_protobuf_proto_agent_proto_msgTypes[13] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1195,7 +1136,7 @@ func (x *InitContainer) ProtoReflect() protoreflect.Message { // Deprecated: Use InitContainer.ProtoReflect.Descriptor instead. func (*InitContainer) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{14} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{13} } func (x *InitContainer) GetName() string { @@ -1260,7 +1201,7 @@ type ImportContainer struct { func (x *ImportContainer) Reset() { *x = ImportContainer{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[15] + mi := &file_protobuf_proto_agent_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1273,7 +1214,7 @@ func (x *ImportContainer) String() string { func (*ImportContainer) ProtoMessage() {} func (x *ImportContainer) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[15] + mi := &file_protobuf_proto_agent_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1286,7 +1227,7 @@ func (x *ImportContainer) ProtoReflect() protoreflect.Message { // Deprecated: Use ImportContainer.ProtoReflect.Descriptor instead. func (*ImportContainer) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{15} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{14} } func (x *ImportContainer) GetVolume() string { @@ -1322,7 +1263,7 @@ type LogConfig struct { func (x *LogConfig) Reset() { *x = LogConfig{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[16] + mi := &file_protobuf_proto_agent_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1335,7 +1276,7 @@ func (x *LogConfig) String() string { func (*LogConfig) ProtoMessage() {} func (x *LogConfig) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[16] + mi := &file_protobuf_proto_agent_proto_msgTypes[15] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1348,7 +1289,7 @@ func (x *LogConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use LogConfig.ProtoReflect.Descriptor instead. func (*LogConfig) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{16} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{15} } func (x *LogConfig) GetDriver() common.DriverType { @@ -1378,7 +1319,7 @@ type Marker struct { func (x *Marker) Reset() { *x = Marker{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[17] + mi := &file_protobuf_proto_agent_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1391,7 +1332,7 @@ func (x *Marker) String() string { func (*Marker) ProtoMessage() {} func (x *Marker) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[17] + mi := &file_protobuf_proto_agent_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1404,7 +1345,7 @@ func (x *Marker) ProtoReflect() protoreflect.Message { // Deprecated: Use Marker.ProtoReflect.Descriptor instead. func (*Marker) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{17} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{16} } func (x *Marker) GetDeployment() map[string]string { @@ -1440,7 +1381,7 @@ type Metrics struct { func (x *Metrics) Reset() { *x = Metrics{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[18] + mi := &file_protobuf_proto_agent_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1453,7 +1394,7 @@ func (x *Metrics) String() string { func (*Metrics) ProtoMessage() {} func (x *Metrics) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[18] + mi := &file_protobuf_proto_agent_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1466,7 +1407,7 @@ func (x *Metrics) ProtoReflect() protoreflect.Message { // Deprecated: Use Metrics.ProtoReflect.Descriptor instead. func (*Metrics) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{18} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{17} } func (x *Metrics) GetPort() int32 { @@ -1496,7 +1437,7 @@ type ExpectedState struct { func (x *ExpectedState) Reset() { *x = ExpectedState{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[19] + mi := &file_protobuf_proto_agent_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1509,7 +1450,7 @@ func (x *ExpectedState) String() string { func (*ExpectedState) ProtoMessage() {} func (x *ExpectedState) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[19] + mi := &file_protobuf_proto_agent_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1522,7 +1463,7 @@ func (x *ExpectedState) ProtoReflect() protoreflect.Message { // Deprecated: Use ExpectedState.ProtoReflect.Descriptor instead. func (*ExpectedState) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{19} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{18} } func (x *ExpectedState) GetState() common.ContainerState { @@ -1562,7 +1503,7 @@ type DagentContainerConfig struct { func (x *DagentContainerConfig) Reset() { *x = DagentContainerConfig{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[20] + mi := &file_protobuf_proto_agent_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1575,7 +1516,7 @@ func (x *DagentContainerConfig) String() string { func (*DagentContainerConfig) ProtoMessage() {} func (x *DagentContainerConfig) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[20] + mi := &file_protobuf_proto_agent_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1588,7 +1529,7 @@ func (x *DagentContainerConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use DagentContainerConfig.ProtoReflect.Descriptor instead. func (*DagentContainerConfig) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{20} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{19} } func (x *DagentContainerConfig) GetLogConfig() *LogConfig { @@ -1653,7 +1594,7 @@ type CraneContainerConfig struct { func (x *CraneContainerConfig) Reset() { *x = CraneContainerConfig{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[21] + mi := &file_protobuf_proto_agent_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1666,7 +1607,7 @@ func (x *CraneContainerConfig) String() string { func (*CraneContainerConfig) ProtoMessage() {} func (x *CraneContainerConfig) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[21] + mi := &file_protobuf_proto_agent_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1679,7 +1620,7 @@ func (x *CraneContainerConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use CraneContainerConfig.ProtoReflect.Descriptor instead. func (*CraneContainerConfig) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{21} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{20} } func (x *CraneContainerConfig) GetDeploymentStrategy() common.DeploymentStrategy { @@ -1778,7 +1719,7 @@ type CommonContainerConfig struct { func (x *CommonContainerConfig) Reset() { *x = CommonContainerConfig{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[22] + mi := &file_protobuf_proto_agent_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1791,7 +1732,7 @@ func (x *CommonContainerConfig) String() string { func (*CommonContainerConfig) ProtoMessage() {} func (x *CommonContainerConfig) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[22] + mi := &file_protobuf_proto_agent_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1804,7 +1745,7 @@ func (x *CommonContainerConfig) ProtoReflect() protoreflect.Message { // Deprecated: Use CommonContainerConfig.ProtoReflect.Descriptor instead. func (*CommonContainerConfig) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{22} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{21} } func (x *CommonContainerConfig) GetName() string { @@ -1919,44 +1860,40 @@ func (x *CommonContainerConfig) GetInitContainers() []*InitContainer { return nil } -type DeployRequest struct { +type DeployWorkloadRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` ContainerName string `protobuf:"bytes,2,opt,name=containerName,proto3" json:"containerName,omitempty"` - // InstanceConfig is set for multiple containers - InstanceConfig *InstanceConfig `protobuf:"bytes,3,opt,name=instanceConfig,proto3" json:"instanceConfig,omitempty"` // ContainerConfigs - Common *CommonContainerConfig `protobuf:"bytes,4,opt,name=common,proto3,oneof" json:"common,omitempty"` - Dagent *DagentContainerConfig `protobuf:"bytes,5,opt,name=dagent,proto3,oneof" json:"dagent,omitempty"` - Crane *CraneContainerConfig `protobuf:"bytes,6,opt,name=crane,proto3,oneof" json:"crane,omitempty"` - // Runtime info and requirements of a container - RuntimeConfig *string `protobuf:"bytes,7,opt,name=runtimeConfig,proto3,oneof" json:"runtimeConfig,omitempty"` - Registry *string `protobuf:"bytes,8,opt,name=registry,proto3,oneof" json:"registry,omitempty"` - ImageName string `protobuf:"bytes,9,opt,name=imageName,proto3" json:"imageName,omitempty"` - Tag string `protobuf:"bytes,10,opt,name=tag,proto3" json:"tag,omitempty"` - RegistryAuth *RegistryAuth `protobuf:"bytes,11,opt,name=registryAuth,proto3,oneof" json:"registryAuth,omitempty"` + Common *CommonContainerConfig `protobuf:"bytes,3,opt,name=common,proto3,oneof" json:"common,omitempty"` + Dagent *DagentContainerConfig `protobuf:"bytes,4,opt,name=dagent,proto3,oneof" json:"dagent,omitempty"` + Crane *CraneContainerConfig `protobuf:"bytes,5,opt,name=crane,proto3,oneof" json:"crane,omitempty"` + Registry *string `protobuf:"bytes,6,opt,name=registry,proto3,oneof" json:"registry,omitempty"` + ImageName string `protobuf:"bytes,7,opt,name=imageName,proto3" json:"imageName,omitempty"` + Tag string `protobuf:"bytes,8,opt,name=tag,proto3" json:"tag,omitempty"` + RegistryAuth *RegistryAuth `protobuf:"bytes,9,opt,name=registryAuth,proto3,oneof" json:"registryAuth,omitempty"` } -func (x *DeployRequest) Reset() { - *x = DeployRequest{} +func (x *DeployWorkloadRequest) Reset() { + *x = DeployWorkloadRequest{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[23] + mi := &file_protobuf_proto_agent_proto_msgTypes[22] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } } -func (x *DeployRequest) String() string { +func (x *DeployWorkloadRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*DeployRequest) ProtoMessage() {} +func (*DeployWorkloadRequest) ProtoMessage() {} -func (x *DeployRequest) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[23] +func (x *DeployWorkloadRequest) ProtoReflect() protoreflect.Message { + mi := &file_protobuf_proto_agent_proto_msgTypes[22] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1967,82 +1904,68 @@ func (x *DeployRequest) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use DeployRequest.ProtoReflect.Descriptor instead. -func (*DeployRequest) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{23} +// Deprecated: Use DeployWorkloadRequest.ProtoReflect.Descriptor instead. +func (*DeployWorkloadRequest) Descriptor() ([]byte, []int) { + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{22} } -func (x *DeployRequest) GetId() string { +func (x *DeployWorkloadRequest) GetId() string { if x != nil { return x.Id } return "" } -func (x *DeployRequest) GetContainerName() string { +func (x *DeployWorkloadRequest) GetContainerName() string { if x != nil { return x.ContainerName } return "" } -func (x *DeployRequest) GetInstanceConfig() *InstanceConfig { - if x != nil { - return x.InstanceConfig - } - return nil -} - -func (x *DeployRequest) GetCommon() *CommonContainerConfig { +func (x *DeployWorkloadRequest) GetCommon() *CommonContainerConfig { if x != nil { return x.Common } return nil } -func (x *DeployRequest) GetDagent() *DagentContainerConfig { +func (x *DeployWorkloadRequest) GetDagent() *DagentContainerConfig { if x != nil { return x.Dagent } return nil } -func (x *DeployRequest) GetCrane() *CraneContainerConfig { +func (x *DeployWorkloadRequest) GetCrane() *CraneContainerConfig { if x != nil { return x.Crane } return nil } -func (x *DeployRequest) GetRuntimeConfig() string { - if x != nil && x.RuntimeConfig != nil { - return *x.RuntimeConfig - } - return "" -} - -func (x *DeployRequest) GetRegistry() string { +func (x *DeployWorkloadRequest) GetRegistry() string { if x != nil && x.Registry != nil { return *x.Registry } return "" } -func (x *DeployRequest) GetImageName() string { +func (x *DeployWorkloadRequest) GetImageName() string { if x != nil { return x.ImageName } return "" } -func (x *DeployRequest) GetTag() string { +func (x *DeployWorkloadRequest) GetTag() string { if x != nil { return x.Tag } return "" } -func (x *DeployRequest) GetRegistryAuth() *RegistryAuth { +func (x *DeployWorkloadRequest) GetRegistryAuth() *RegistryAuth { if x != nil { return x.RegistryAuth } @@ -2061,7 +1984,7 @@ type ContainerStateRequest struct { func (x *ContainerStateRequest) Reset() { *x = ContainerStateRequest{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[24] + mi := &file_protobuf_proto_agent_proto_msgTypes[23] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2074,7 +1997,7 @@ func (x *ContainerStateRequest) String() string { func (*ContainerStateRequest) ProtoMessage() {} func (x *ContainerStateRequest) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[24] + mi := &file_protobuf_proto_agent_proto_msgTypes[23] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2087,7 +2010,7 @@ func (x *ContainerStateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ContainerStateRequest.ProtoReflect.Descriptor instead. func (*ContainerStateRequest) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{24} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{23} } func (x *ContainerStateRequest) GetPrefix() string { @@ -2116,7 +2039,7 @@ type ContainerDeleteRequest struct { func (x *ContainerDeleteRequest) Reset() { *x = ContainerDeleteRequest{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[25] + mi := &file_protobuf_proto_agent_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2129,7 +2052,7 @@ func (x *ContainerDeleteRequest) String() string { func (*ContainerDeleteRequest) ProtoMessage() {} func (x *ContainerDeleteRequest) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[25] + mi := &file_protobuf_proto_agent_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2142,7 +2065,7 @@ func (x *ContainerDeleteRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ContainerDeleteRequest.ProtoReflect.Descriptor instead. func (*ContainerDeleteRequest) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{25} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{24} } func (x *ContainerDeleteRequest) GetPrefix() string { @@ -2171,7 +2094,7 @@ type DeployRequestLegacy struct { func (x *DeployRequestLegacy) Reset() { *x = DeployRequestLegacy{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[26] + mi := &file_protobuf_proto_agent_proto_msgTypes[25] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2184,7 +2107,7 @@ func (x *DeployRequestLegacy) String() string { func (*DeployRequestLegacy) ProtoMessage() {} func (x *DeployRequestLegacy) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[26] + mi := &file_protobuf_proto_agent_proto_msgTypes[25] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2197,7 +2120,7 @@ func (x *DeployRequestLegacy) ProtoReflect() protoreflect.Message { // Deprecated: Use DeployRequestLegacy.ProtoReflect.Descriptor instead. func (*DeployRequestLegacy) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{26} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{25} } func (x *DeployRequestLegacy) GetRequestId() string { @@ -2228,7 +2151,7 @@ type AgentUpdateRequest struct { func (x *AgentUpdateRequest) Reset() { *x = AgentUpdateRequest{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[27] + mi := &file_protobuf_proto_agent_proto_msgTypes[26] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2241,7 +2164,7 @@ func (x *AgentUpdateRequest) String() string { func (*AgentUpdateRequest) ProtoMessage() {} func (x *AgentUpdateRequest) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[27] + mi := &file_protobuf_proto_agent_proto_msgTypes[26] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2254,7 +2177,7 @@ func (x *AgentUpdateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AgentUpdateRequest.ProtoReflect.Descriptor instead. func (*AgentUpdateRequest) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{27} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{26} } func (x *AgentUpdateRequest) GetTag() string { @@ -2289,7 +2212,7 @@ type ReplaceTokenRequest struct { func (x *ReplaceTokenRequest) Reset() { *x = ReplaceTokenRequest{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[28] + mi := &file_protobuf_proto_agent_proto_msgTypes[27] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2302,7 +2225,7 @@ func (x *ReplaceTokenRequest) String() string { func (*ReplaceTokenRequest) ProtoMessage() {} func (x *ReplaceTokenRequest) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[28] + mi := &file_protobuf_proto_agent_proto_msgTypes[27] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2315,7 +2238,7 @@ func (x *ReplaceTokenRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ReplaceTokenRequest.ProtoReflect.Descriptor instead. func (*ReplaceTokenRequest) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{28} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{27} } func (x *ReplaceTokenRequest) GetToken() string { @@ -2336,7 +2259,7 @@ type AgentAbortUpdate struct { func (x *AgentAbortUpdate) Reset() { *x = AgentAbortUpdate{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[29] + mi := &file_protobuf_proto_agent_proto_msgTypes[28] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2349,7 +2272,7 @@ func (x *AgentAbortUpdate) String() string { func (*AgentAbortUpdate) ProtoMessage() {} func (x *AgentAbortUpdate) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[29] + mi := &file_protobuf_proto_agent_proto_msgTypes[28] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2362,7 +2285,7 @@ func (x *AgentAbortUpdate) ProtoReflect() protoreflect.Message { // Deprecated: Use AgentAbortUpdate.ProtoReflect.Descriptor instead. func (*AgentAbortUpdate) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{29} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{28} } func (x *AgentAbortUpdate) GetError() string { @@ -2386,7 +2309,7 @@ type ContainerLogRequest struct { func (x *ContainerLogRequest) Reset() { *x = ContainerLogRequest{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[30] + mi := &file_protobuf_proto_agent_proto_msgTypes[29] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2399,7 +2322,7 @@ func (x *ContainerLogRequest) String() string { func (*ContainerLogRequest) ProtoMessage() {} func (x *ContainerLogRequest) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[30] + mi := &file_protobuf_proto_agent_proto_msgTypes[29] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2412,7 +2335,7 @@ func (x *ContainerLogRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ContainerLogRequest.ProtoReflect.Descriptor instead. func (*ContainerLogRequest) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{30} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{29} } func (x *ContainerLogRequest) GetContainer() *common.ContainerIdentifier { @@ -2448,7 +2371,7 @@ type ContainerInspectRequest struct { func (x *ContainerInspectRequest) Reset() { *x = ContainerInspectRequest{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[31] + mi := &file_protobuf_proto_agent_proto_msgTypes[30] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2461,7 +2384,7 @@ func (x *ContainerInspectRequest) String() string { func (*ContainerInspectRequest) ProtoMessage() {} func (x *ContainerInspectRequest) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[31] + mi := &file_protobuf_proto_agent_proto_msgTypes[30] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2474,7 +2397,7 @@ func (x *ContainerInspectRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ContainerInspectRequest.ProtoReflect.Descriptor instead. func (*ContainerInspectRequest) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{31} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{30} } func (x *ContainerInspectRequest) GetContainer() *common.ContainerIdentifier { @@ -2495,7 +2418,7 @@ type CloseConnectionRequest struct { func (x *CloseConnectionRequest) Reset() { *x = CloseConnectionRequest{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_agent_proto_msgTypes[32] + mi := &file_protobuf_proto_agent_proto_msgTypes[31] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -2508,7 +2431,7 @@ func (x *CloseConnectionRequest) String() string { func (*CloseConnectionRequest) ProtoMessage() {} func (x *CloseConnectionRequest) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_agent_proto_msgTypes[32] + mi := &file_protobuf_proto_agent_proto_msgTypes[31] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -2521,7 +2444,7 @@ func (x *CloseConnectionRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use CloseConnectionRequest.ProtoReflect.Descriptor instead. func (*CloseConnectionRequest) Descriptor() ([]byte, []int) { - return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{32} + return file_protobuf_proto_agent_proto_rawDescGZIP(), []int{31} } func (x *CloseConnectionRequest) GetReason() CloseReason { @@ -2547,515 +2470,497 @@ var file_protobuf_proto_agent_proto_rawDesc = []byte{ 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x88, 0x01, 0x01, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4e, - 0x61, 0x6d, 0x65, 0x22, 0xc0, 0x06, 0x0a, 0x0c, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6d, - 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x35, 0x0a, 0x06, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x56, 0x65, 0x72, - 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x48, 0x00, 0x52, 0x06, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x12, 0x46, 0x0a, 0x0e, 0x63, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6e, 0x74, - 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x48, 0x00, 0x52, 0x0e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x12, 0x49, 0x0a, 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, - 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0f, 0x63, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x40, - 0x0a, 0x0c, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x18, 0x04, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x70, - 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, - 0x48, 0x00, 0x52, 0x0c, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, - 0x12, 0x3d, 0x0a, 0x0b, 0x6c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4c, 0x69, - 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x48, 0x00, 0x52, 0x0b, 0x6c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x12, - 0x33, 0x0a, 0x06, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x19, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x55, 0x70, 0x64, - 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x06, 0x75, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, 0x05, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x18, 0x07, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6c, 0x6f, 0x73, - 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x48, 0x00, 0x52, 0x05, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x10, 0x63, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, - 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x4d, 0x0a, 0x10, 0x64, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x09, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, - 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x10, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x40, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, + 0x61, 0x6d, 0x65, 0x22, 0xb9, 0x06, 0x0a, 0x0c, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6d, + 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x2e, 0x0a, 0x06, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x70, + 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x06, 0x64, 0x65, + 0x70, 0x6c, 0x6f, 0x79, 0x12, 0x46, 0x0a, 0x0e, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0e, 0x63, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x49, 0x0a, 0x0f, + 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x40, 0x0a, 0x0c, 0x64, 0x65, 0x70, 0x6c, 0x6f, + 0x79, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x48, 0x00, 0x52, 0x0c, 0x64, 0x65, 0x70, + 0x6c, 0x6f, 0x79, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x12, 0x3d, 0x0a, 0x0b, 0x6c, 0x69, 0x73, + 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0b, 0x6c, 0x69, 0x73, + 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x12, 0x33, 0x0a, 0x06, 0x75, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x06, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x35, 0x0a, + 0x05, 0x63, 0x6c, 0x6f, 0x73, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x05, 0x63, + 0x6c, 0x6f, 0x73, 0x65, 0x12, 0x4d, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, + 0x00, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6d, 0x6d, + 0x61, 0x6e, 0x64, 0x12, 0x4d, 0x0a, 0x10, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, + 0x52, 0x10, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x73, 0x12, 0x40, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, + 0x6f, 0x67, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x4c, 0x6f, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x54, + 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0c, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, + 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x4c, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, + 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1e, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x48, 0x00, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, + 0x70, 0x65, 0x63, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x22, + 0x3a, 0x0a, 0x0a, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x16, 0x0a, + 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, + 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x90, 0x02, 0x0a, 0x11, + 0x41, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x45, 0x72, 0x72, 0x6f, + 0x72, 0x12, 0x35, 0x0a, 0x0b, 0x6c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, + 0x67, 0x65, 0x6e, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x48, 0x00, 0x52, 0x0b, 0x6c, 0x69, 0x73, + 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x12, 0x3f, 0x0a, 0x10, 0x64, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, + 0x45, 0x72, 0x72, 0x6f, 0x72, 0x48, 0x00, 0x52, 0x10, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, + 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x37, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, - 0x72, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x0c, 0x63, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x12, 0x40, 0x0a, 0x0c, 0x72, - 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, - 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, - 0x0c, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x4c, 0x0a, - 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, - 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x61, - 0x69, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x63, - 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x22, 0x3a, 0x0a, 0x0a, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x45, - 0x72, 0x72, 0x6f, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x14, 0x0a, 0x05, - 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, - 0x6f, 0x72, 0x22, 0x90, 0x02, 0x0a, 0x11, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6d, 0x6d, - 0x61, 0x6e, 0x64, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x35, 0x0a, 0x0b, 0x6c, 0x69, 0x73, 0x74, - 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, - 0x48, 0x00, 0x52, 0x0b, 0x6c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x12, - 0x3f, 0x0a, 0x10, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, - 0x65, 0x72, 0x73, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x48, 0x00, 0x52, 0x10, - 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, - 0x12, 0x37, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, - 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, - 0x67, 0x65, 0x6e, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x48, 0x00, 0x52, 0x0c, 0x63, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x12, 0x3f, 0x0a, 0x10, 0x63, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x18, 0x0c, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, - 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x48, 0x00, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x63, 0x6f, - 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x22, 0x2a, 0x0a, 0x0e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, - 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, - 0x64, 0x22, 0x9e, 0x01, 0x0a, 0x14, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x70, - 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x20, 0x0a, 0x0b, 0x76, 0x65, - 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0b, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x22, 0x0a, 0x0c, - 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x4e, 0x6f, 0x74, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x4e, 0x6f, 0x74, 0x65, 0x73, - 0x12, 0x30, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x73, 0x18, 0x04, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, - 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x73, 0x22, 0x4f, 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, - 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, - 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, - 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x22, 0xa9, 0x02, 0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x21, - 0x0a, 0x09, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x48, 0x00, 0x52, 0x09, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x88, 0x01, - 0x01, 0x12, 0x48, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, - 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x49, - 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x45, 0x6e, - 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, - 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x2f, 0x0a, 0x10, 0x72, - 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, - 0x04, 0x20, 0x01, 0x28, 0x09, 0x48, 0x01, 0x52, 0x10, 0x72, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, - 0x6f, 0x72, 0x79, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x88, 0x01, 0x01, 0x1a, 0x3e, 0x0a, 0x10, - 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x0c, 0x0a, 0x0a, - 0x5f, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x61, 0x74, 0x68, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x72, - 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x6f, 0x72, 0x79, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x22, - 0x64, 0x0a, 0x0c, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x41, 0x75, 0x74, 0x68, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, - 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, - 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x50, 0x0a, 0x04, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1a, 0x0a, - 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x64, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x12, 0x1f, 0x0a, 0x08, 0x65, 0x78, 0x74, - 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x65, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x08, 0x65, - 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x88, 0x01, 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x65, - 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x22, 0x2f, 0x0a, 0x09, 0x50, 0x6f, 0x72, 0x74, 0x52, - 0x61, 0x6e, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x64, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x65, - 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x74, 0x6f, 0x22, 0x6e, 0x0a, 0x10, 0x50, 0x6f, 0x72, 0x74, - 0x52, 0x61, 0x6e, 0x67, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x2c, 0x0a, 0x08, - 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, - 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, - 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x12, 0x2c, 0x0a, 0x08, 0x65, 0x78, - 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, - 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x08, - 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x22, 0xad, 0x01, 0x0a, 0x06, 0x56, 0x6f, 0x6c, - 0x75, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, - 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x17, 0x0a, 0x04, 0x73, - 0x69, 0x7a, 0x65, 0x18, 0x66, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x04, 0x73, 0x69, 0x7a, - 0x65, 0x88, 0x01, 0x01, 0x12, 0x2b, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x67, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x56, 0x6f, 0x6c, 0x75, - 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65, 0x48, 0x01, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x88, 0x01, - 0x01, 0x12, 0x19, 0x0a, 0x05, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x18, 0x68, 0x20, 0x01, 0x28, 0x09, - 0x48, 0x02, 0x52, 0x05, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x88, 0x01, 0x01, 0x42, 0x07, 0x0a, 0x05, - 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x08, - 0x0a, 0x06, 0x5f, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x22, 0x34, 0x0a, 0x0a, 0x56, 0x6f, 0x6c, 0x75, - 0x6d, 0x65, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x64, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, - 0x74, 0x68, 0x18, 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0xe4, - 0x02, 0x0a, 0x0d, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, - 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x65, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x2d, 0x0a, 0x0f, 0x75, 0x73, - 0x65, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x66, 0x20, - 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0f, 0x75, 0x73, 0x65, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x88, 0x01, 0x01, 0x12, 0x2c, 0x0a, 0x07, 0x76, 0x6f, 0x6c, - 0x75, 0x6d, 0x65, 0x73, 0x18, 0xe8, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x4c, 0x69, 0x6e, 0x6b, 0x52, 0x07, - 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x12, 0x19, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, - 0x6e, 0x64, 0x18, 0xe9, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, - 0x6e, 0x64, 0x12, 0x13, 0x0a, 0x04, 0x61, 0x72, 0x67, 0x73, 0x18, 0xea, 0x07, 0x20, 0x03, 0x28, - 0x09, 0x52, 0x04, 0x61, 0x72, 0x67, 0x73, 0x12, 0x48, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, - 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0xeb, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, - 0x74, 0x1a, 0x3e, 0x0a, 0x10, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x75, 0x73, 0x65, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xcf, 0x01, 0x0a, 0x0f, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, - 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x76, 0x6f, 0x6c, - 0x75, 0x6d, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, - 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x65, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x4a, 0x0a, 0x0b, 0x65, - 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0xe8, 0x07, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x27, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, - 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, - 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, - 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x1a, 0x3e, 0x0a, 0x10, 0x45, 0x6e, 0x76, 0x69, 0x72, - 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xad, 0x01, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2a, 0x0a, 0x06, 0x64, 0x72, 0x69, 0x76, 0x65, 0x72, 0x18, - 0x64, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, - 0x72, 0x69, 0x76, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x52, 0x06, 0x64, 0x72, 0x69, 0x76, 0x65, - 0x72, 0x12, 0x38, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xe8, 0x07, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4c, 0x6f, 0x67, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x3a, 0x0a, 0x0c, 0x4f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xed, 0x02, 0x0a, 0x06, 0x4d, 0x61, 0x72, 0x6b, - 0x65, 0x72, 0x12, 0x3e, 0x0a, 0x0a, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, - 0x18, 0xe8, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, - 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, - 0x6e, 0x74, 0x12, 0x35, 0x0a, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x18, 0xe9, 0x07, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x61, 0x72, - 0x6b, 0x65, 0x72, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x35, 0x0a, 0x07, 0x69, 0x6e, 0x67, - 0x72, 0x65, 0x73, 0x73, 0x18, 0xea, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72, 0x2e, 0x49, 0x6e, 0x67, 0x72, 0x65, - 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x69, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, - 0x1a, 0x3d, 0x0a, 0x0f, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, + 0x11, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x45, 0x72, 0x72, + 0x6f, 0x72, 0x48, 0x00, 0x52, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, + 0x6f, 0x67, 0x12, 0x3f, 0x0a, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, + 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x48, + 0x00, 0x52, 0x10, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, + 0x65, 0x63, 0x74, 0x42, 0x09, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x22, 0x2a, + 0x0a, 0x0e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x18, 0x0a, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x07, 0x73, 0x74, 0x61, 0x72, 0x74, 0x65, 0x64, 0x22, 0xb0, 0x02, 0x0a, 0x0d, 0x44, + 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x20, 0x0a, 0x0b, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0b, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x22, + 0x0a, 0x0c, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x4e, 0x6f, 0x74, 0x65, 0x73, 0x18, 0x03, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x4e, 0x6f, 0x74, + 0x65, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x3b, 0x0a, 0x07, 0x73, 0x65, + 0x63, 0x72, 0x65, 0x74, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, + 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x12, 0x38, 0x0a, 0x08, 0x72, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x57, 0x6f, 0x72, 0x6b, 0x6c, 0x6f, 0x61, 0x64, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x08, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x73, 0x1a, 0x3a, 0x0a, 0x0c, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x47, 0x0a, + 0x12, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4f, 0x72, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x52, 0x06, + 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x22, 0x64, 0x0a, 0x0c, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, + 0x72, 0x79, 0x41, 0x75, 0x74, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, + 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x12, 0x0a, 0x04, + 0x75, 0x73, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, + 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x50, 0x0a, 0x04, + 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x18, 0x64, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x12, 0x1f, 0x0a, 0x08, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x65, 0x20, 0x01, + 0x28, 0x05, 0x48, 0x00, 0x52, 0x08, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x88, 0x01, + 0x01, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x22, 0x2f, + 0x0a, 0x09, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, + 0x72, 0x6f, 0x6d, 0x18, 0x64, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x12, + 0x0e, 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x65, 0x20, 0x01, 0x28, 0x05, 0x52, 0x02, 0x74, 0x6f, 0x22, + 0x6e, 0x0a, 0x10, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x42, 0x69, 0x6e, 0x64, + 0x69, 0x6e, 0x67, 0x12, 0x2c, 0x0a, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, + 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, + 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x12, 0x2c, 0x0a, 0x08, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x18, 0x65, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, + 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x08, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x22, + 0xad, 0x01, 0x0a, 0x06, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, + 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, + 0x74, 0x68, 0x12, 0x17, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x66, 0x20, 0x01, 0x28, 0x09, + 0x48, 0x00, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x88, 0x01, 0x01, 0x12, 0x2b, 0x0a, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x18, 0x67, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65, 0x48, 0x01, 0x52, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x88, 0x01, 0x01, 0x12, 0x19, 0x0a, 0x05, 0x63, 0x6c, 0x61, 0x73, + 0x73, 0x18, 0x68, 0x20, 0x01, 0x28, 0x09, 0x48, 0x02, 0x52, 0x05, 0x63, 0x6c, 0x61, 0x73, 0x73, + 0x88, 0x01, 0x01, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x42, 0x07, 0x0a, 0x05, + 0x5f, 0x74, 0x79, 0x70, 0x65, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x63, 0x6c, 0x61, 0x73, 0x73, 0x22, + 0x34, 0x0a, 0x0a, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x4c, 0x69, 0x6e, 0x6b, 0x12, 0x12, 0x0a, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0xe4, 0x02, 0x0a, 0x0d, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x69, + 0x6d, 0x61, 0x67, 0x65, 0x18, 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, + 0x65, 0x12, 0x2d, 0x0a, 0x0f, 0x75, 0x73, 0x65, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x18, 0x66, 0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x0f, 0x75, 0x73, + 0x65, 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x88, 0x01, 0x01, + 0x12, 0x2c, 0x0a, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x18, 0xe8, 0x07, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x11, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, + 0x65, 0x4c, 0x69, 0x6e, 0x6b, 0x52, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x12, 0x19, + 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0xe9, 0x07, 0x20, 0x03, 0x28, 0x09, + 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x12, 0x13, 0x0a, 0x04, 0x61, 0x72, 0x67, + 0x73, 0x18, 0xea, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x61, 0x72, 0x67, 0x73, 0x12, 0x48, + 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0xeb, 0x07, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x25, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x49, 0x6e, 0x69, + 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x2e, 0x45, 0x6e, 0x76, 0x69, 0x72, + 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x65, 0x6e, 0x76, + 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x1a, 0x3e, 0x0a, 0x10, 0x45, 0x6e, 0x76, 0x69, + 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, + 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x75, 0x73, 0x65, + 0x50, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0xcf, 0x01, 0x0a, + 0x0f, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x12, 0x16, 0x0a, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x06, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, + 0x61, 0x6e, 0x64, 0x18, 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, + 0x6e, 0x64, 0x12, 0x4a, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, + 0x74, 0x18, 0xe8, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x2e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x1a, 0x3e, + 0x0a, 0x10, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xad, + 0x01, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2a, 0x0a, 0x06, + 0x64, 0x72, 0x69, 0x76, 0x65, 0x72, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x63, + 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x72, 0x69, 0x76, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, + 0x52, 0x06, 0x64, 0x72, 0x69, 0x76, 0x65, 0x72, 0x12, 0x38, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x18, 0xe8, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x4c, 0x6f, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x4f, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x73, 0x1a, 0x3a, 0x0a, 0x0c, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0xed, + 0x02, 0x0a, 0x06, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72, 0x12, 0x3e, 0x0a, 0x0a, 0x64, 0x65, 0x70, + 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0xe8, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1d, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72, 0x2e, 0x44, 0x65, + 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0a, 0x64, + 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x35, 0x0a, 0x07, 0x73, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x18, 0xe9, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72, 0x2e, 0x53, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x12, 0x35, 0x0a, 0x07, 0x69, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x18, 0xea, 0x07, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x61, 0x72, 0x6b, 0x65, + 0x72, 0x2e, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, + 0x69, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x1a, 0x3d, 0x0a, 0x0f, 0x44, 0x65, 0x70, 0x6c, 0x6f, + 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x3a, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x1a, 0x3a, 0x0a, 0x0c, 0x49, 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x31, + 0x0a, 0x07, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x12, 0x0a, + 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, + 0x68, 0x22, 0x96, 0x01, 0x0a, 0x0d, 0x45, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x74, + 0x61, 0x74, 0x65, 0x12, 0x2c, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x64, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x1d, 0x0a, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x65, 0x20, 0x01, + 0x28, 0x05, 0x48, 0x00, 0x52, 0x07, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x88, 0x01, 0x01, + 0x12, 0x1f, 0x0a, 0x08, 0x65, 0x78, 0x69, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x66, 0x20, 0x01, + 0x28, 0x05, 0x48, 0x01, 0x52, 0x08, 0x65, 0x78, 0x69, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x88, 0x01, + 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x42, 0x0b, 0x0a, + 0x09, 0x5f, 0x65, 0x78, 0x69, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x22, 0xe8, 0x03, 0x0a, 0x15, 0x44, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x12, 0x33, 0x0a, 0x09, 0x6c, 0x6f, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, + 0x4c, 0x6f, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x09, 0x6c, 0x6f, 0x67, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x88, 0x01, 0x01, 0x12, 0x40, 0x0a, 0x0d, 0x72, 0x65, 0x73, + 0x74, 0x61, 0x72, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x15, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, + 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x48, 0x01, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x74, 0x61, + 0x72, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x88, 0x01, 0x01, 0x12, 0x3a, 0x0a, 0x0b, 0x6e, + 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x64, 0x65, 0x18, 0x66, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x13, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, + 0x6b, 0x4d, 0x6f, 0x64, 0x65, 0x48, 0x02, 0x52, 0x0b, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, + 0x4d, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, 0x12, 0x3f, 0x0a, 0x0d, 0x65, 0x78, 0x70, 0x65, 0x63, + 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x18, 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x48, 0x03, 0x52, 0x0d, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x88, 0x01, 0x01, 0x12, 0x1b, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, + 0x6f, 0x72, 0x6b, 0x73, 0x18, 0xe8, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, + 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x41, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, + 0xe9, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x44, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, + 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x6c, 0x6f, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x72, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x50, 0x6f, 0x6c, + 0x69, 0x63, 0x79, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, + 0x6f, 0x64, 0x65, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x22, 0xc3, 0x06, 0x0a, 0x14, 0x43, 0x72, 0x61, 0x6e, 0x65, 0x43, + 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4f, + 0x0a, 0x12, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x61, + 0x74, 0x65, 0x67, 0x79, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, + 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x48, 0x00, 0x52, 0x12, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x88, 0x01, 0x01, 0x12, + 0x4c, 0x0a, 0x11, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x43, 0x6f, + 0x6e, 0x66, 0x69, 0x67, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x48, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x01, 0x52, 0x11, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, + 0x68, 0x65, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x88, 0x01, 0x01, 0x12, 0x43, 0x0a, + 0x0e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, + 0x66, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, + 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x02, 0x52, + 0x0e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x88, + 0x01, 0x01, 0x12, 0x27, 0x0a, 0x0c, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x48, 0x65, 0x61, 0x64, 0x65, + 0x72, 0x73, 0x18, 0x67, 0x20, 0x01, 0x28, 0x08, 0x48, 0x03, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x78, + 0x79, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x0f, 0x75, + 0x73, 0x65, 0x4c, 0x6f, 0x61, 0x64, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x18, 0x68, + 0x20, 0x01, 0x28, 0x08, 0x48, 0x04, 0x52, 0x0f, 0x75, 0x73, 0x65, 0x4c, 0x6f, 0x61, 0x64, 0x42, + 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x34, 0x0a, 0x0b, 0x61, 0x6e, + 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x69, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x0d, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72, 0x48, 0x05, + 0x52, 0x0b, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x88, 0x01, 0x01, + 0x12, 0x2a, 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0x6a, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x0d, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72, 0x48, + 0x06, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x07, + 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x18, 0x6b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x48, 0x07, 0x52, + 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x88, 0x01, 0x01, 0x12, 0x25, 0x0a, 0x0d, 0x63, + 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0xe8, 0x07, 0x20, + 0x03, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x48, 0x65, 0x61, 0x64, 0x65, + 0x72, 0x73, 0x12, 0x64, 0x0a, 0x12, 0x65, 0x78, 0x74, 0x72, 0x61, 0x4c, 0x42, 0x41, 0x6e, 0x6e, + 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xe9, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x33, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x72, 0x61, 0x6e, 0x65, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x45, 0x78, 0x74, + 0x72, 0x61, 0x4c, 0x42, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x12, 0x65, 0x78, 0x74, 0x72, 0x61, 0x4c, 0x42, 0x41, 0x6e, 0x6e, + 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x45, 0x0a, 0x17, 0x45, 0x78, 0x74, 0x72, + 0x61, 0x4c, 0x42, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, + 0x15, 0x0a, 0x13, 0x5f, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, + 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x42, 0x14, 0x0a, 0x12, 0x5f, 0x68, 0x65, 0x61, 0x6c, 0x74, + 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x11, 0x0a, 0x0f, + 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, + 0x0f, 0x0a, 0x0d, 0x5f, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, + 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x75, 0x73, 0x65, 0x4c, 0x6f, 0x61, 0x64, 0x42, 0x61, 0x6c, 0x61, + 0x6e, 0x63, 0x65, 0x72, 0x42, 0x0e, 0x0a, 0x0c, 0x5f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x42, + 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x22, 0xf2, 0x07, 0x0a, 0x15, + 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x65, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x33, 0x0a, 0x06, 0x65, 0x78, 0x70, + 0x6f, 0x73, 0x65, 0x18, 0x66, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, + 0x79, 0x48, 0x00, 0x52, 0x06, 0x65, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x88, 0x01, 0x01, 0x12, 0x2e, + 0x0a, 0x07, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x18, 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x0f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, + 0x48, 0x01, 0x52, 0x07, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x88, 0x01, 0x01, 0x12, 0x46, + 0x0a, 0x0f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x18, 0x68, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x48, 0x02, 0x52, 0x0f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, + 0x6e, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x45, 0x0a, 0x0f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, + 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x69, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x16, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x48, 0x03, 0x52, 0x0f, 0x69, 0x6d, 0x70, 0x6f, 0x72, + 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x17, 0x0a, + 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x6a, 0x20, 0x01, 0x28, 0x03, 0x48, 0x04, 0x52, 0x04, 0x75, + 0x73, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x15, 0x0a, 0x03, 0x54, 0x54, 0x59, 0x18, 0x6b, 0x20, + 0x01, 0x28, 0x08, 0x48, 0x05, 0x52, 0x03, 0x54, 0x54, 0x59, 0x88, 0x01, 0x01, 0x12, 0x2f, 0x0a, + 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, + 0x79, 0x18, 0x6c, 0x20, 0x01, 0x28, 0x09, 0x48, 0x06, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x69, + 0x6e, 0x67, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x88, 0x01, 0x01, 0x12, 0x22, + 0x0a, 0x05, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x18, 0xe8, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x05, 0x70, 0x6f, 0x72, + 0x74, 0x73, 0x12, 0x38, 0x0a, 0x0a, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, + 0x18, 0xe9, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, + 0x50, 0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, + 0x52, 0x0a, 0x70, 0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x07, + 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x18, 0xea, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x07, 0x76, + 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, + 0x64, 0x73, 0x18, 0xeb, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x61, + 0x6e, 0x64, 0x73, 0x12, 0x13, 0x0a, 0x04, 0x61, 0x72, 0x67, 0x73, 0x18, 0xec, 0x07, 0x20, 0x03, + 0x28, 0x09, 0x52, 0x04, 0x61, 0x72, 0x67, 0x73, 0x12, 0x50, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, + 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0xed, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x45, 0x6e, 0x76, + 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x65, + 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x44, 0x0a, 0x07, 0x73, 0x65, + 0x63, 0x72, 0x65, 0x74, 0x73, 0x18, 0xee, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, + 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x07, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, + 0x12, 0x3d, 0x0a, 0x0e, 0x69, 0x6e, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x73, 0x18, 0xef, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x67, 0x65, 0x6e, + 0x74, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, + 0x0e, 0x69, 0x6e, 0x69, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x1a, + 0x3e, 0x0a, 0x10, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, - 0x3a, 0x0a, 0x0c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, + 0x3a, 0x0a, 0x0c, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x3a, 0x0a, 0x0c, 0x49, - 0x6e, 0x67, 0x72, 0x65, 0x73, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x31, 0x0a, 0x07, 0x4d, 0x65, 0x74, 0x72, 0x69, - 0x63, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, - 0x52, 0x04, 0x70, 0x6f, 0x72, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x22, 0x96, 0x01, 0x0a, 0x0d, 0x45, - 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x2c, 0x0a, 0x05, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, - 0x61, 0x74, 0x65, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1d, 0x0a, 0x07, 0x74, 0x69, - 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x18, 0x65, 0x20, 0x01, 0x28, 0x05, 0x48, 0x00, 0x52, 0x07, 0x74, - 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x08, 0x65, 0x78, 0x69, - 0x74, 0x43, 0x6f, 0x64, 0x65, 0x18, 0x66, 0x20, 0x01, 0x28, 0x05, 0x48, 0x01, 0x52, 0x08, 0x65, - 0x78, 0x69, 0x74, 0x43, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x74, - 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x65, 0x78, 0x69, 0x74, 0x43, - 0x6f, 0x64, 0x65, 0x22, 0xe8, 0x03, 0x0a, 0x15, 0x44, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, - 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x33, 0x0a, - 0x09, 0x6c, 0x6f, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x10, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4c, 0x6f, 0x67, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x48, 0x00, 0x52, 0x09, 0x6c, 0x6f, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x88, - 0x01, 0x01, 0x12, 0x40, 0x0a, 0x0d, 0x72, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x50, 0x6f, 0x6c, - 0x69, 0x63, 0x79, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, - 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x48, 0x01, 0x52, 0x0d, 0x72, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, - 0x79, 0x88, 0x01, 0x01, 0x12, 0x3a, 0x0a, 0x0b, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, - 0x6f, 0x64, 0x65, 0x18, 0x66, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x13, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, - 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x64, 0x65, 0x48, 0x02, - 0x52, 0x0b, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, - 0x12, 0x3f, 0x0a, 0x0d, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x18, 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x45, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x48, 0x03, 0x52, - 0x0d, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x88, 0x01, - 0x01, 0x12, 0x1b, 0x0a, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x18, 0xe8, 0x07, - 0x20, 0x03, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x12, 0x41, - 0x0a, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x18, 0xe9, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, - 0x28, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, - 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x4c, 0x61, - 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, 0x6c, - 0x73, 0x1a, 0x39, 0x0a, 0x0b, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x0c, 0x0a, 0x0a, - 0x5f, 0x6c, 0x6f, 0x67, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x72, - 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x42, 0x0e, 0x0a, 0x0c, - 0x5f, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x64, 0x65, 0x42, 0x10, 0x0a, 0x0e, - 0x5f, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, 0x65, 0x64, 0x53, 0x74, 0x61, 0x74, 0x65, 0x22, 0xc3, - 0x06, 0x0a, 0x14, 0x43, 0x72, 0x61, 0x6e, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, - 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x4f, 0x0a, 0x12, 0x64, 0x65, 0x70, 0x6c, 0x6f, - 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x18, 0x64, 0x20, - 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x70, - 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x48, - 0x00, 0x52, 0x12, 0x64, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, - 0x61, 0x74, 0x65, 0x67, 0x79, 0x88, 0x01, 0x01, 0x12, 0x4c, 0x0a, 0x11, 0x68, 0x65, 0x61, 0x6c, - 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x65, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x48, 0x65, 0x61, - 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x01, - 0x52, 0x11, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x88, 0x01, 0x01, 0x12, 0x43, 0x0a, 0x0e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x66, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, - 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x02, 0x52, 0x0e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x88, 0x01, 0x01, 0x12, 0x27, 0x0a, 0x0c, 0x70, - 0x72, 0x6f, 0x78, 0x79, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0x67, 0x20, 0x01, 0x28, - 0x08, 0x48, 0x03, 0x52, 0x0c, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, - 0x73, 0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x0f, 0x75, 0x73, 0x65, 0x4c, 0x6f, 0x61, 0x64, 0x42, - 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x18, 0x68, 0x20, 0x01, 0x28, 0x08, 0x48, 0x04, 0x52, - 0x0f, 0x75, 0x73, 0x65, 0x4c, 0x6f, 0x61, 0x64, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, - 0x88, 0x01, 0x01, 0x12, 0x34, 0x0a, 0x0b, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x73, 0x18, 0x69, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72, 0x48, 0x05, 0x52, 0x0b, 0x61, 0x6e, 0x6e, 0x6f, 0x74, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x88, 0x01, 0x01, 0x12, 0x2a, 0x0a, 0x06, 0x6c, 0x61, 0x62, - 0x65, 0x6c, 0x73, 0x18, 0x6a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x4d, 0x61, 0x72, 0x6b, 0x65, 0x72, 0x48, 0x06, 0x52, 0x06, 0x6c, 0x61, 0x62, 0x65, - 0x6c, 0x73, 0x88, 0x01, 0x01, 0x12, 0x2d, 0x0a, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, - 0x18, 0x6b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x4d, - 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x48, 0x07, 0x52, 0x07, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, - 0x73, 0x88, 0x01, 0x01, 0x12, 0x25, 0x0a, 0x0d, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x48, 0x65, - 0x61, 0x64, 0x65, 0x72, 0x73, 0x18, 0xe8, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x75, - 0x73, 0x74, 0x6f, 0x6d, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x12, 0x64, 0x0a, 0x12, 0x65, - 0x78, 0x74, 0x72, 0x61, 0x4c, 0x42, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x18, 0xe9, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x43, 0x72, 0x61, 0x6e, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x45, 0x78, 0x74, 0x72, 0x61, 0x4c, 0x42, 0x41, 0x6e, 0x6e, - 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x12, 0x65, - 0x78, 0x74, 0x72, 0x61, 0x4c, 0x42, 0x41, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x1a, 0x45, 0x0a, 0x17, 0x45, 0x78, 0x74, 0x72, 0x61, 0x4c, 0x42, 0x41, 0x6e, 0x6e, 0x6f, - 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x64, 0x65, 0x70, - 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x42, - 0x14, 0x0a, 0x12, 0x5f, 0x68, 0x65, 0x61, 0x6c, 0x74, 0x68, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x11, 0x0a, 0x0f, 0x5f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x70, 0x72, 0x6f, - 0x78, 0x79, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x73, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x75, 0x73, - 0x65, 0x4c, 0x6f, 0x61, 0x64, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x72, 0x42, 0x0e, 0x0a, - 0x0c, 0x5f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x09, 0x0a, - 0x07, 0x5f, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x73, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6d, 0x65, 0x74, - 0x72, 0x69, 0x63, 0x73, 0x22, 0xf2, 0x07, 0x0a, 0x15, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x43, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x12, - 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x12, 0x33, 0x0a, 0x06, 0x65, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x18, 0x66, 0x20, 0x01, - 0x28, 0x0e, 0x32, 0x16, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x78, 0x70, 0x6f, - 0x73, 0x65, 0x53, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x48, 0x00, 0x52, 0x06, 0x65, 0x78, - 0x70, 0x6f, 0x73, 0x65, 0x88, 0x01, 0x01, 0x12, 0x2e, 0x0a, 0x07, 0x72, 0x6f, 0x75, 0x74, 0x69, - 0x6e, 0x67, 0x18, 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, - 0x6e, 0x2e, 0x52, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x48, 0x01, 0x52, 0x07, 0x72, 0x6f, 0x75, - 0x74, 0x69, 0x6e, 0x67, 0x88, 0x01, 0x01, 0x12, 0x46, 0x0a, 0x0f, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x68, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x17, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x48, 0x02, 0x52, 0x0f, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, - 0x45, 0x0a, 0x0f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, - 0x65, 0x72, 0x18, 0x69, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x49, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, - 0x48, 0x03, 0x52, 0x0f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, 0x17, 0x0a, 0x04, 0x75, 0x73, 0x65, 0x72, 0x18, 0x6a, - 0x20, 0x01, 0x28, 0x03, 0x48, 0x04, 0x52, 0x04, 0x75, 0x73, 0x65, 0x72, 0x88, 0x01, 0x01, 0x12, - 0x15, 0x0a, 0x03, 0x54, 0x54, 0x59, 0x18, 0x6b, 0x20, 0x01, 0x28, 0x08, 0x48, 0x05, 0x52, 0x03, - 0x54, 0x54, 0x59, 0x88, 0x01, 0x01, 0x12, 0x2f, 0x0a, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, - 0x67, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x6c, 0x20, 0x01, 0x28, 0x09, - 0x48, 0x06, 0x52, 0x10, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x44, 0x69, 0x72, 0x65, 0x63, - 0x74, 0x6f, 0x72, 0x79, 0x88, 0x01, 0x01, 0x12, 0x22, 0x0a, 0x05, 0x70, 0x6f, 0x72, 0x74, 0x73, - 0x18, 0xe8, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0b, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x50, 0x6f, 0x72, 0x74, 0x52, 0x05, 0x70, 0x6f, 0x72, 0x74, 0x73, 0x12, 0x38, 0x0a, 0x0a, 0x70, - 0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0xe9, 0x07, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x17, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x50, 0x6f, 0x72, 0x74, 0x52, 0x61, 0x6e, - 0x67, 0x65, 0x42, 0x69, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x52, 0x0a, 0x70, 0x6f, 0x72, 0x74, 0x52, - 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12, 0x28, 0x0a, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, - 0x18, 0xea, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x52, 0x07, 0x76, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x73, 0x12, - 0x1b, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x73, 0x18, 0xeb, 0x07, 0x20, 0x03, - 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x73, 0x12, 0x13, 0x0a, 0x04, - 0x61, 0x72, 0x67, 0x73, 0x18, 0xec, 0x07, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x61, 0x72, 0x67, - 0x73, 0x12, 0x50, 0x0a, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, - 0x18, 0xed, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, - 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x45, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, - 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x0b, 0x65, 0x6e, 0x76, 0x69, 0x72, 0x6f, 0x6e, 0x6d, - 0x65, 0x6e, 0x74, 0x12, 0x44, 0x0a, 0x07, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x18, 0xee, - 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x2e, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x52, 0x07, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x12, 0x3d, 0x0a, 0x0e, 0x69, 0x6e, 0x69, - 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x18, 0xef, 0x07, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x49, 0x6e, 0x69, 0x74, 0x43, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x52, 0x0e, 0x69, 0x6e, 0x69, 0x74, 0x43, 0x6f, - 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x1a, 0x3e, 0x0a, 0x10, 0x45, 0x6e, 0x76, 0x69, - 0x72, 0x6f, 0x6e, 0x6d, 0x65, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x3a, 0x0a, 0x0c, 0x53, 0x65, 0x63, 0x72, - 0x65, 0x74, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x3a, 0x02, 0x38, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x65, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x42, - 0x0a, 0x0a, 0x08, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x69, 0x6e, 0x67, 0x42, 0x12, 0x0a, 0x10, 0x5f, - 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x42, - 0x12, 0x0a, 0x10, 0x5f, 0x69, 0x6d, 0x70, 0x6f, 0x72, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x42, 0x06, 0x0a, 0x04, - 0x5f, 0x54, 0x54, 0x59, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, - 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x22, 0xbc, 0x04, 0x0a, 0x0d, 0x44, 0x65, - 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x24, 0x0a, 0x0d, 0x63, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, - 0x65, 0x12, 0x3d, 0x0a, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x61, 0x67, 0x65, 0x6e, - 0x74, 0x2e, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x52, 0x0e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x12, 0x39, 0x0a, 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, + 0x65, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x72, 0x6f, 0x75, 0x74, 0x69, + 0x6e, 0x67, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x42, 0x12, 0x0a, 0x10, 0x5f, 0x69, 0x6d, 0x70, 0x6f, 0x72, + 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x75, + 0x73, 0x65, 0x72, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x54, 0x54, 0x59, 0x42, 0x13, 0x0a, 0x11, 0x5f, + 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, + 0x22, 0xc8, 0x03, 0x0a, 0x15, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x57, 0x6f, 0x72, 0x6b, 0x6c, + 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x24, 0x0a, 0x0d, 0x63, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x39, 0x0a, 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x39, 0x0a, 0x06, 0x64, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x67, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x01, 0x52, 0x06, 0x64, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x36, 0x0a, 0x05, 0x63, 0x72, 0x61, 0x6e, 0x65, 0x18, - 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x72, + 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x72, 0x61, 0x6e, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x48, 0x02, 0x52, 0x05, 0x63, 0x72, 0x61, 0x6e, 0x65, 0x88, 0x01, 0x01, 0x12, 0x29, - 0x0a, 0x0d, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, - 0x07, 0x20, 0x01, 0x28, 0x09, 0x48, 0x03, 0x52, 0x0d, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x08, 0x72, 0x65, 0x67, - 0x69, 0x73, 0x74, 0x72, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x48, 0x04, 0x52, 0x08, 0x72, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x88, 0x01, 0x01, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x6d, - 0x61, 0x67, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, - 0x6d, 0x61, 0x67, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, - 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x3c, 0x0a, 0x0c, 0x72, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x41, 0x75, 0x74, 0x68, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x13, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, - 0x79, 0x41, 0x75, 0x74, 0x68, 0x48, 0x05, 0x52, 0x0c, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, - 0x79, 0x41, 0x75, 0x74, 0x68, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x63, 0x6f, 0x6d, - 0x6d, 0x6f, 0x6e, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x64, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x42, 0x08, - 0x0a, 0x06, 0x5f, 0x63, 0x72, 0x61, 0x6e, 0x65, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x72, 0x75, 0x6e, - 0x74, 0x69, 0x6d, 0x65, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x72, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x72, 0x65, 0x67, 0x69, - 0x73, 0x74, 0x72, 0x79, 0x41, 0x75, 0x74, 0x68, 0x22, 0x6a, 0x0a, 0x15, 0x43, 0x6f, 0x6e, 0x74, - 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x1b, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x48, 0x00, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x88, 0x01, 0x01, 0x12, 0x1d, - 0x0a, 0x07, 0x6f, 0x6e, 0x65, 0x53, 0x68, 0x6f, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x48, - 0x01, 0x52, 0x07, 0x6f, 0x6e, 0x65, 0x53, 0x68, 0x6f, 0x74, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, - 0x07, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6f, 0x6e, 0x65, - 0x53, 0x68, 0x6f, 0x74, 0x22, 0x44, 0x0a, 0x16, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, - 0x72, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, - 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, - 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x47, 0x0a, 0x13, 0x44, 0x65, - 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x65, 0x67, 0x61, 0x63, - 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, - 0x12, 0x0a, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6a, - 0x73, 0x6f, 0x6e, 0x22, 0x64, 0x0a, 0x12, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x26, 0x0a, 0x0e, 0x74, - 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x0e, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, - 0x6e, 0x64, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x2b, 0x0a, 0x13, 0x52, 0x65, 0x70, - 0x6c, 0x61, 0x63, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x28, 0x0a, 0x10, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, - 0x62, 0x6f, 0x72, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, - 0x22, 0x82, 0x01, 0x0a, 0x13, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, - 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, - 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, - 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, 0x6e, - 0x67, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, - 0x04, 0x74, 0x61, 0x69, 0x6c, 0x22, 0x54, 0x0a, 0x17, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, - 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x12, 0x39, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, - 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x44, 0x0a, 0x16, 0x43, - 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6c, - 0x6f, 0x73, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, - 0x6e, 0x2a, 0x69, 0x0a, 0x0b, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, - 0x12, 0x1c, 0x0a, 0x18, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, - 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x09, - 0x0a, 0x05, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x45, 0x4c, - 0x46, 0x5f, 0x44, 0x45, 0x53, 0x54, 0x52, 0x55, 0x43, 0x54, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, - 0x53, 0x48, 0x55, 0x54, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x12, 0x10, 0x0a, 0x0c, 0x52, 0x45, - 0x56, 0x4f, 0x4b, 0x45, 0x5f, 0x54, 0x4f, 0x4b, 0x45, 0x4e, 0x10, 0x04, 0x32, 0x9c, 0x05, 0x0a, - 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x07, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x12, 0x10, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x49, - 0x6e, 0x66, 0x6f, 0x1a, 0x13, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, - 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x30, 0x01, 0x12, 0x37, 0x0a, 0x0c, 0x43, 0x6f, - 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, 0x2e, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x45, - 0x72, 0x72, 0x6f, 0x72, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x12, 0x35, 0x0a, 0x0b, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x55, 0x70, 0x64, 0x61, - 0x74, 0x65, 0x12, 0x17, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, - 0x41, 0x62, 0x6f, 0x72, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x2d, 0x0a, 0x0d, 0x54, 0x6f, - 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x64, 0x12, 0x0d, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, - 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x44, 0x0a, 0x10, 0x44, 0x65, 0x70, - 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1f, 0x2e, - 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, - 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x0d, - 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x28, 0x01, 0x12, - 0x44, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, - 0x65, 0x12, 0x21, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, - 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x73, - 0x73, 0x61, 0x67, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x28, 0x01, 0x12, 0x42, 0x0a, 0x12, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, - 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x1b, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, - 0x67, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, - 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x28, 0x01, 0x12, 0x38, 0x0a, 0x0a, 0x53, 0x65, 0x63, - 0x72, 0x65, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1b, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, - 0x70, 0x74, 0x79, 0x12, 0x30, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, - 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x0c, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, - 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x12, 0x20, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x52, + 0x69, 0x67, 0x48, 0x02, 0x52, 0x05, 0x63, 0x72, 0x61, 0x6e, 0x65, 0x88, 0x01, 0x01, 0x12, 0x1f, + 0x0a, 0x08, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, + 0x48, 0x03, 0x52, 0x08, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x88, 0x01, 0x01, 0x12, + 0x1c, 0x0a, 0x09, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, + 0x03, 0x74, 0x61, 0x67, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, + 0x3c, 0x0a, 0x0c, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x41, 0x75, 0x74, 0x68, 0x18, + 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x41, 0x75, 0x74, 0x68, 0x48, 0x04, 0x52, 0x0c, 0x72, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x41, 0x75, 0x74, 0x68, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, + 0x07, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x64, 0x61, 0x67, + 0x65, 0x6e, 0x74, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x63, 0x72, 0x61, 0x6e, 0x65, 0x42, 0x0b, 0x0a, + 0x09, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x72, + 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x41, 0x75, 0x74, 0x68, 0x22, 0x6a, 0x0a, 0x15, 0x43, + 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x88, 0x01, + 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x6f, 0x6e, 0x65, 0x53, 0x68, 0x6f, 0x74, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x08, 0x48, 0x01, 0x52, 0x07, 0x6f, 0x6e, 0x65, 0x53, 0x68, 0x6f, 0x74, 0x88, 0x01, 0x01, + 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x42, 0x0a, 0x0a, 0x08, 0x5f, + 0x6f, 0x6e, 0x65, 0x53, 0x68, 0x6f, 0x74, 0x22, 0x44, 0x0a, 0x16, 0x43, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x47, 0x0a, + 0x13, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x65, + 0x67, 0x61, 0x63, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, + 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x22, 0x64, 0x0a, 0x12, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, + 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x26, + 0x0a, 0x0e, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, + 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x2b, 0x0a, 0x13, + 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x28, 0x0a, 0x10, 0x41, 0x67, 0x65, + 0x6e, 0x74, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, + 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x22, 0x82, 0x01, 0x0a, 0x13, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x09, 0x63, + 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x09, 0x63, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, + 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x74, 0x72, 0x65, 0x61, + 0x6d, 0x69, 0x6e, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x0d, 0x52, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x22, 0x54, 0x0a, 0x17, 0x43, 0x6f, 0x6e, 0x74, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, + 0x69, 0x65, 0x72, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x44, + 0x0a, 0x16, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, + 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, + 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x06, 0x72, 0x65, + 0x61, 0x73, 0x6f, 0x6e, 0x2a, 0x69, 0x0a, 0x0b, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x61, + 0x73, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x18, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x5f, 0x52, 0x45, 0x41, + 0x53, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, + 0x00, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, + 0x53, 0x45, 0x4c, 0x46, 0x5f, 0x44, 0x45, 0x53, 0x54, 0x52, 0x55, 0x43, 0x54, 0x10, 0x02, 0x12, + 0x0c, 0x0a, 0x08, 0x53, 0x48, 0x55, 0x54, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x12, 0x10, 0x0a, + 0x0c, 0x52, 0x45, 0x56, 0x4f, 0x4b, 0x45, 0x5f, 0x54, 0x4f, 0x4b, 0x45, 0x4e, 0x10, 0x04, 0x32, + 0x9c, 0x05, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x07, 0x43, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x12, 0x10, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, + 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x1a, 0x13, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, + 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x30, 0x01, 0x12, 0x37, 0x0a, + 0x0c, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, 0x2e, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x61, + 0x6e, 0x64, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x35, 0x0a, 0x0b, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x55, + 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, + 0x65, 0x6e, 0x74, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x1a, 0x0d, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x2d, 0x0a, + 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x64, 0x12, 0x0d, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x44, 0x0a, 0x10, + 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x12, 0x1f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x28, 0x01, 0x12, 0x44, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, + 0x74, 0x61, 0x74, 0x65, 0x12, 0x21, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x28, 0x01, 0x12, 0x42, 0x0a, 0x12, 0x43, 0x6f, 0x6e, 0x74, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x1b, + 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x4c, 0x6f, 0x67, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x28, 0x01, 0x12, 0x38, 0x0a, 0x0a, + 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1b, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x43, 0x0a, 0x10, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x12, 0x20, 0x2e, 0x63, 0x6f, 0x6d, - 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, - 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x0d, 0x2e, 0x63, - 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x35, 0x5a, 0x33, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x79, 0x72, 0x65, 0x63, 0x74, - 0x6f, 0x72, 0x2d, 0x69, 0x6f, 0x2f, 0x64, 0x79, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x69, 0x6f, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x67, 0x6f, 0x2f, 0x61, 0x67, 0x65, - 0x6e, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x30, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x0c, 0x43, 0x6f, 0x6e, 0x74, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x12, 0x20, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x4c, 0x69, + 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x43, 0x0a, 0x10, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x12, 0x20, 0x2e, + 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, + 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x35, + 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x79, 0x72, + 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2d, 0x69, 0x6f, 0x2f, 0x64, 0x79, 0x72, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x69, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x67, 0x6f, 0x2f, + 0x61, 0x67, 0x65, 0x6e, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -3071,7 +2976,7 @@ func file_protobuf_proto_agent_proto_rawDescGZIP() []byte { } var file_protobuf_proto_agent_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_protobuf_proto_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 44) +var file_protobuf_proto_agent_proto_msgTypes = make([]protoimpl.MessageInfo, 43) var file_protobuf_proto_agent_proto_goTypes = []interface{}{ (CloseReason)(0), // 0: agent.CloseReason (*AgentInfo)(nil), // 1: agent.AgentInfo @@ -3079,59 +2984,59 @@ var file_protobuf_proto_agent_proto_goTypes = []interface{}{ (*AgentError)(nil), // 3: agent.AgentError (*AgentCommandError)(nil), // 4: agent.AgentCommandError (*DeployResponse)(nil), // 5: agent.DeployResponse - (*VersionDeployRequest)(nil), // 6: agent.VersionDeployRequest + (*DeployRequest)(nil), // 6: agent.DeployRequest (*ListSecretsRequest)(nil), // 7: agent.ListSecretsRequest - (*InstanceConfig)(nil), // 8: agent.InstanceConfig - (*RegistryAuth)(nil), // 9: agent.RegistryAuth - (*Port)(nil), // 10: agent.Port - (*PortRange)(nil), // 11: agent.PortRange - (*PortRangeBinding)(nil), // 12: agent.PortRangeBinding - (*Volume)(nil), // 13: agent.Volume - (*VolumeLink)(nil), // 14: agent.VolumeLink - (*InitContainer)(nil), // 15: agent.InitContainer - (*ImportContainer)(nil), // 16: agent.ImportContainer - (*LogConfig)(nil), // 17: agent.LogConfig - (*Marker)(nil), // 18: agent.Marker - (*Metrics)(nil), // 19: agent.Metrics - (*ExpectedState)(nil), // 20: agent.ExpectedState - (*DagentContainerConfig)(nil), // 21: agent.DagentContainerConfig - (*CraneContainerConfig)(nil), // 22: agent.CraneContainerConfig - (*CommonContainerConfig)(nil), // 23: agent.CommonContainerConfig - (*DeployRequest)(nil), // 24: agent.DeployRequest - (*ContainerStateRequest)(nil), // 25: agent.ContainerStateRequest - (*ContainerDeleteRequest)(nil), // 26: agent.ContainerDeleteRequest - (*DeployRequestLegacy)(nil), // 27: agent.DeployRequestLegacy - (*AgentUpdateRequest)(nil), // 28: agent.AgentUpdateRequest - (*ReplaceTokenRequest)(nil), // 29: agent.ReplaceTokenRequest - (*AgentAbortUpdate)(nil), // 30: agent.AgentAbortUpdate - (*ContainerLogRequest)(nil), // 31: agent.ContainerLogRequest - (*ContainerInspectRequest)(nil), // 32: agent.ContainerInspectRequest - (*CloseConnectionRequest)(nil), // 33: agent.CloseConnectionRequest - nil, // 34: agent.InstanceConfig.EnvironmentEntry - nil, // 35: agent.InitContainer.EnvironmentEntry - nil, // 36: agent.ImportContainer.EnvironmentEntry - nil, // 37: agent.LogConfig.OptionsEntry - nil, // 38: agent.Marker.DeploymentEntry - nil, // 39: agent.Marker.ServiceEntry - nil, // 40: agent.Marker.IngressEntry - nil, // 41: agent.DagentContainerConfig.LabelsEntry - nil, // 42: agent.CraneContainerConfig.ExtraLBAnnotationsEntry - nil, // 43: agent.CommonContainerConfig.EnvironmentEntry - nil, // 44: agent.CommonContainerConfig.SecretsEntry - (*common.ContainerCommandRequest)(nil), // 45: common.ContainerCommandRequest - (*common.DeleteContainersRequest)(nil), // 46: common.DeleteContainersRequest - (*common.ContainerIdentifier)(nil), // 47: common.ContainerIdentifier - (common.VolumeType)(0), // 48: common.VolumeType - (common.DriverType)(0), // 49: common.DriverType - (common.ContainerState)(0), // 50: common.ContainerState - (common.RestartPolicy)(0), // 51: common.RestartPolicy - (common.NetworkMode)(0), // 52: common.NetworkMode - (common.DeploymentStrategy)(0), // 53: common.DeploymentStrategy - (*common.HealthCheckConfig)(nil), // 54: common.HealthCheckConfig - (*common.ResourceConfig)(nil), // 55: common.ResourceConfig - (common.ExposeStrategy)(0), // 56: common.ExposeStrategy - (*common.Routing)(nil), // 57: common.Routing - (*common.ConfigContainer)(nil), // 58: common.ConfigContainer + (*RegistryAuth)(nil), // 8: agent.RegistryAuth + (*Port)(nil), // 9: agent.Port + (*PortRange)(nil), // 10: agent.PortRange + (*PortRangeBinding)(nil), // 11: agent.PortRangeBinding + (*Volume)(nil), // 12: agent.Volume + (*VolumeLink)(nil), // 13: agent.VolumeLink + (*InitContainer)(nil), // 14: agent.InitContainer + (*ImportContainer)(nil), // 15: agent.ImportContainer + (*LogConfig)(nil), // 16: agent.LogConfig + (*Marker)(nil), // 17: agent.Marker + (*Metrics)(nil), // 18: agent.Metrics + (*ExpectedState)(nil), // 19: agent.ExpectedState + (*DagentContainerConfig)(nil), // 20: agent.DagentContainerConfig + (*CraneContainerConfig)(nil), // 21: agent.CraneContainerConfig + (*CommonContainerConfig)(nil), // 22: agent.CommonContainerConfig + (*DeployWorkloadRequest)(nil), // 23: agent.DeployWorkloadRequest + (*ContainerStateRequest)(nil), // 24: agent.ContainerStateRequest + (*ContainerDeleteRequest)(nil), // 25: agent.ContainerDeleteRequest + (*DeployRequestLegacy)(nil), // 26: agent.DeployRequestLegacy + (*AgentUpdateRequest)(nil), // 27: agent.AgentUpdateRequest + (*ReplaceTokenRequest)(nil), // 28: agent.ReplaceTokenRequest + (*AgentAbortUpdate)(nil), // 29: agent.AgentAbortUpdate + (*ContainerLogRequest)(nil), // 30: agent.ContainerLogRequest + (*ContainerInspectRequest)(nil), // 31: agent.ContainerInspectRequest + (*CloseConnectionRequest)(nil), // 32: agent.CloseConnectionRequest + nil, // 33: agent.DeployRequest.SecretsEntry + nil, // 34: agent.InitContainer.EnvironmentEntry + nil, // 35: agent.ImportContainer.EnvironmentEntry + nil, // 36: agent.LogConfig.OptionsEntry + nil, // 37: agent.Marker.DeploymentEntry + nil, // 38: agent.Marker.ServiceEntry + nil, // 39: agent.Marker.IngressEntry + nil, // 40: agent.DagentContainerConfig.LabelsEntry + nil, // 41: agent.CraneContainerConfig.ExtraLBAnnotationsEntry + nil, // 42: agent.CommonContainerConfig.EnvironmentEntry + nil, // 43: agent.CommonContainerConfig.SecretsEntry + (*common.ContainerCommandRequest)(nil), // 44: common.ContainerCommandRequest + (*common.DeleteContainersRequest)(nil), // 45: common.DeleteContainersRequest + (*common.ContainerOrPrefix)(nil), // 46: common.ContainerOrPrefix + (common.VolumeType)(0), // 47: common.VolumeType + (common.DriverType)(0), // 48: common.DriverType + (common.ContainerState)(0), // 49: common.ContainerState + (common.RestartPolicy)(0), // 50: common.RestartPolicy + (common.NetworkMode)(0), // 51: common.NetworkMode + (common.DeploymentStrategy)(0), // 52: common.DeploymentStrategy + (*common.HealthCheckConfig)(nil), // 53: common.HealthCheckConfig + (*common.ResourceConfig)(nil), // 54: common.ResourceConfig + (common.ExposeStrategy)(0), // 55: common.ExposeStrategy + (*common.Routing)(nil), // 56: common.Routing + (*common.ConfigContainer)(nil), // 57: common.ConfigContainer + (*common.ContainerIdentifier)(nil), // 58: common.ContainerIdentifier (*common.Empty)(nil), // 59: common.Empty (*common.DeploymentStatusMessage)(nil), // 60: common.DeploymentStatusMessage (*common.ContainerStateListMessage)(nil), // 61: common.ContainerStateListMessage @@ -3141,94 +3046,93 @@ var file_protobuf_proto_agent_proto_goTypes = []interface{}{ (*common.ContainerInspectResponse)(nil), // 65: common.ContainerInspectResponse } var file_protobuf_proto_agent_proto_depIdxs = []int32{ - 6, // 0: agent.AgentCommand.deploy:type_name -> agent.VersionDeployRequest - 25, // 1: agent.AgentCommand.containerState:type_name -> agent.ContainerStateRequest - 26, // 2: agent.AgentCommand.containerDelete:type_name -> agent.ContainerDeleteRequest - 27, // 3: agent.AgentCommand.deployLegacy:type_name -> agent.DeployRequestLegacy + 6, // 0: agent.AgentCommand.deploy:type_name -> agent.DeployRequest + 24, // 1: agent.AgentCommand.containerState:type_name -> agent.ContainerStateRequest + 25, // 2: agent.AgentCommand.containerDelete:type_name -> agent.ContainerDeleteRequest + 26, // 3: agent.AgentCommand.deployLegacy:type_name -> agent.DeployRequestLegacy 7, // 4: agent.AgentCommand.listSecrets:type_name -> agent.ListSecretsRequest - 28, // 5: agent.AgentCommand.update:type_name -> agent.AgentUpdateRequest - 33, // 6: agent.AgentCommand.close:type_name -> agent.CloseConnectionRequest - 45, // 7: agent.AgentCommand.containerCommand:type_name -> common.ContainerCommandRequest - 46, // 8: agent.AgentCommand.deleteContainers:type_name -> common.DeleteContainersRequest - 31, // 9: agent.AgentCommand.containerLog:type_name -> agent.ContainerLogRequest - 29, // 10: agent.AgentCommand.replaceToken:type_name -> agent.ReplaceTokenRequest - 32, // 11: agent.AgentCommand.containerInspect:type_name -> agent.ContainerInspectRequest + 27, // 5: agent.AgentCommand.update:type_name -> agent.AgentUpdateRequest + 32, // 6: agent.AgentCommand.close:type_name -> agent.CloseConnectionRequest + 44, // 7: agent.AgentCommand.containerCommand:type_name -> common.ContainerCommandRequest + 45, // 8: agent.AgentCommand.deleteContainers:type_name -> common.DeleteContainersRequest + 30, // 9: agent.AgentCommand.containerLog:type_name -> agent.ContainerLogRequest + 28, // 10: agent.AgentCommand.replaceToken:type_name -> agent.ReplaceTokenRequest + 31, // 11: agent.AgentCommand.containerInspect:type_name -> agent.ContainerInspectRequest 3, // 12: agent.AgentCommandError.listSecrets:type_name -> agent.AgentError 3, // 13: agent.AgentCommandError.deleteContainers:type_name -> agent.AgentError 3, // 14: agent.AgentCommandError.containerLog:type_name -> agent.AgentError 3, // 15: agent.AgentCommandError.containerInspect:type_name -> agent.AgentError - 24, // 16: agent.VersionDeployRequest.requests:type_name -> agent.DeployRequest - 47, // 17: agent.ListSecretsRequest.container:type_name -> common.ContainerIdentifier - 34, // 18: agent.InstanceConfig.environment:type_name -> agent.InstanceConfig.EnvironmentEntry - 11, // 19: agent.PortRangeBinding.internal:type_name -> agent.PortRange - 11, // 20: agent.PortRangeBinding.external:type_name -> agent.PortRange - 48, // 21: agent.Volume.type:type_name -> common.VolumeType - 14, // 22: agent.InitContainer.volumes:type_name -> agent.VolumeLink - 35, // 23: agent.InitContainer.environment:type_name -> agent.InitContainer.EnvironmentEntry - 36, // 24: agent.ImportContainer.environment:type_name -> agent.ImportContainer.EnvironmentEntry - 49, // 25: agent.LogConfig.driver:type_name -> common.DriverType - 37, // 26: agent.LogConfig.options:type_name -> agent.LogConfig.OptionsEntry - 38, // 27: agent.Marker.deployment:type_name -> agent.Marker.DeploymentEntry - 39, // 28: agent.Marker.service:type_name -> agent.Marker.ServiceEntry - 40, // 29: agent.Marker.ingress:type_name -> agent.Marker.IngressEntry - 50, // 30: agent.ExpectedState.state:type_name -> common.ContainerState - 17, // 31: agent.DagentContainerConfig.logConfig:type_name -> agent.LogConfig - 51, // 32: agent.DagentContainerConfig.restartPolicy:type_name -> common.RestartPolicy - 52, // 33: agent.DagentContainerConfig.networkMode:type_name -> common.NetworkMode - 20, // 34: agent.DagentContainerConfig.expectedState:type_name -> agent.ExpectedState - 41, // 35: agent.DagentContainerConfig.labels:type_name -> agent.DagentContainerConfig.LabelsEntry - 53, // 36: agent.CraneContainerConfig.deploymentStrategy:type_name -> common.DeploymentStrategy - 54, // 37: agent.CraneContainerConfig.healthCheckConfig:type_name -> common.HealthCheckConfig - 55, // 38: agent.CraneContainerConfig.resourceConfig:type_name -> common.ResourceConfig - 18, // 39: agent.CraneContainerConfig.annotations:type_name -> agent.Marker - 18, // 40: agent.CraneContainerConfig.labels:type_name -> agent.Marker - 19, // 41: agent.CraneContainerConfig.metrics:type_name -> agent.Metrics - 42, // 42: agent.CraneContainerConfig.extraLBAnnotations:type_name -> agent.CraneContainerConfig.ExtraLBAnnotationsEntry - 56, // 43: agent.CommonContainerConfig.expose:type_name -> common.ExposeStrategy - 57, // 44: agent.CommonContainerConfig.routing:type_name -> common.Routing - 58, // 45: agent.CommonContainerConfig.configContainer:type_name -> common.ConfigContainer - 16, // 46: agent.CommonContainerConfig.importContainer:type_name -> agent.ImportContainer - 10, // 47: agent.CommonContainerConfig.ports:type_name -> agent.Port - 12, // 48: agent.CommonContainerConfig.portRanges:type_name -> agent.PortRangeBinding - 13, // 49: agent.CommonContainerConfig.volumes:type_name -> agent.Volume - 43, // 50: agent.CommonContainerConfig.environment:type_name -> agent.CommonContainerConfig.EnvironmentEntry - 44, // 51: agent.CommonContainerConfig.secrets:type_name -> agent.CommonContainerConfig.SecretsEntry - 15, // 52: agent.CommonContainerConfig.initContainers:type_name -> agent.InitContainer - 8, // 53: agent.DeployRequest.instanceConfig:type_name -> agent.InstanceConfig - 23, // 54: agent.DeployRequest.common:type_name -> agent.CommonContainerConfig - 21, // 55: agent.DeployRequest.dagent:type_name -> agent.DagentContainerConfig - 22, // 56: agent.DeployRequest.crane:type_name -> agent.CraneContainerConfig - 9, // 57: agent.DeployRequest.registryAuth:type_name -> agent.RegistryAuth - 47, // 58: agent.ContainerLogRequest.container:type_name -> common.ContainerIdentifier - 47, // 59: agent.ContainerInspectRequest.container:type_name -> common.ContainerIdentifier - 0, // 60: agent.CloseConnectionRequest.reason:type_name -> agent.CloseReason - 1, // 61: agent.Agent.Connect:input_type -> agent.AgentInfo - 4, // 62: agent.Agent.CommandError:input_type -> agent.AgentCommandError - 30, // 63: agent.Agent.AbortUpdate:input_type -> agent.AgentAbortUpdate - 59, // 64: agent.Agent.TokenReplaced:input_type -> common.Empty - 60, // 65: agent.Agent.DeploymentStatus:input_type -> common.DeploymentStatusMessage - 61, // 66: agent.Agent.ContainerState:input_type -> common.ContainerStateListMessage - 62, // 67: agent.Agent.ContainerLogStream:input_type -> common.ContainerLogMessage - 63, // 68: agent.Agent.SecretList:input_type -> common.ListSecretsResponse - 59, // 69: agent.Agent.DeleteContainers:input_type -> common.Empty - 64, // 70: agent.Agent.ContainerLog:input_type -> common.ContainerLogListResponse - 65, // 71: agent.Agent.ContainerInspect:input_type -> common.ContainerInspectResponse - 2, // 72: agent.Agent.Connect:output_type -> agent.AgentCommand - 59, // 73: agent.Agent.CommandError:output_type -> common.Empty - 59, // 74: agent.Agent.AbortUpdate:output_type -> common.Empty - 59, // 75: agent.Agent.TokenReplaced:output_type -> common.Empty - 59, // 76: agent.Agent.DeploymentStatus:output_type -> common.Empty - 59, // 77: agent.Agent.ContainerState:output_type -> common.Empty - 59, // 78: agent.Agent.ContainerLogStream:output_type -> common.Empty - 59, // 79: agent.Agent.SecretList:output_type -> common.Empty - 59, // 80: agent.Agent.DeleteContainers:output_type -> common.Empty - 59, // 81: agent.Agent.ContainerLog:output_type -> common.Empty - 59, // 82: agent.Agent.ContainerInspect:output_type -> common.Empty - 72, // [72:83] is the sub-list for method output_type - 61, // [61:72] is the sub-list for method input_type - 61, // [61:61] is the sub-list for extension type_name - 61, // [61:61] is the sub-list for extension extendee - 0, // [0:61] is the sub-list for field type_name + 33, // 16: agent.DeployRequest.secrets:type_name -> agent.DeployRequest.SecretsEntry + 23, // 17: agent.DeployRequest.requests:type_name -> agent.DeployWorkloadRequest + 46, // 18: agent.ListSecretsRequest.target:type_name -> common.ContainerOrPrefix + 10, // 19: agent.PortRangeBinding.internal:type_name -> agent.PortRange + 10, // 20: agent.PortRangeBinding.external:type_name -> agent.PortRange + 47, // 21: agent.Volume.type:type_name -> common.VolumeType + 13, // 22: agent.InitContainer.volumes:type_name -> agent.VolumeLink + 34, // 23: agent.InitContainer.environment:type_name -> agent.InitContainer.EnvironmentEntry + 35, // 24: agent.ImportContainer.environment:type_name -> agent.ImportContainer.EnvironmentEntry + 48, // 25: agent.LogConfig.driver:type_name -> common.DriverType + 36, // 26: agent.LogConfig.options:type_name -> agent.LogConfig.OptionsEntry + 37, // 27: agent.Marker.deployment:type_name -> agent.Marker.DeploymentEntry + 38, // 28: agent.Marker.service:type_name -> agent.Marker.ServiceEntry + 39, // 29: agent.Marker.ingress:type_name -> agent.Marker.IngressEntry + 49, // 30: agent.ExpectedState.state:type_name -> common.ContainerState + 16, // 31: agent.DagentContainerConfig.logConfig:type_name -> agent.LogConfig + 50, // 32: agent.DagentContainerConfig.restartPolicy:type_name -> common.RestartPolicy + 51, // 33: agent.DagentContainerConfig.networkMode:type_name -> common.NetworkMode + 19, // 34: agent.DagentContainerConfig.expectedState:type_name -> agent.ExpectedState + 40, // 35: agent.DagentContainerConfig.labels:type_name -> agent.DagentContainerConfig.LabelsEntry + 52, // 36: agent.CraneContainerConfig.deploymentStrategy:type_name -> common.DeploymentStrategy + 53, // 37: agent.CraneContainerConfig.healthCheckConfig:type_name -> common.HealthCheckConfig + 54, // 38: agent.CraneContainerConfig.resourceConfig:type_name -> common.ResourceConfig + 17, // 39: agent.CraneContainerConfig.annotations:type_name -> agent.Marker + 17, // 40: agent.CraneContainerConfig.labels:type_name -> agent.Marker + 18, // 41: agent.CraneContainerConfig.metrics:type_name -> agent.Metrics + 41, // 42: agent.CraneContainerConfig.extraLBAnnotations:type_name -> agent.CraneContainerConfig.ExtraLBAnnotationsEntry + 55, // 43: agent.CommonContainerConfig.expose:type_name -> common.ExposeStrategy + 56, // 44: agent.CommonContainerConfig.routing:type_name -> common.Routing + 57, // 45: agent.CommonContainerConfig.configContainer:type_name -> common.ConfigContainer + 15, // 46: agent.CommonContainerConfig.importContainer:type_name -> agent.ImportContainer + 9, // 47: agent.CommonContainerConfig.ports:type_name -> agent.Port + 11, // 48: agent.CommonContainerConfig.portRanges:type_name -> agent.PortRangeBinding + 12, // 49: agent.CommonContainerConfig.volumes:type_name -> agent.Volume + 42, // 50: agent.CommonContainerConfig.environment:type_name -> agent.CommonContainerConfig.EnvironmentEntry + 43, // 51: agent.CommonContainerConfig.secrets:type_name -> agent.CommonContainerConfig.SecretsEntry + 14, // 52: agent.CommonContainerConfig.initContainers:type_name -> agent.InitContainer + 22, // 53: agent.DeployWorkloadRequest.common:type_name -> agent.CommonContainerConfig + 20, // 54: agent.DeployWorkloadRequest.dagent:type_name -> agent.DagentContainerConfig + 21, // 55: agent.DeployWorkloadRequest.crane:type_name -> agent.CraneContainerConfig + 8, // 56: agent.DeployWorkloadRequest.registryAuth:type_name -> agent.RegistryAuth + 58, // 57: agent.ContainerLogRequest.container:type_name -> common.ContainerIdentifier + 58, // 58: agent.ContainerInspectRequest.container:type_name -> common.ContainerIdentifier + 0, // 59: agent.CloseConnectionRequest.reason:type_name -> agent.CloseReason + 1, // 60: agent.Agent.Connect:input_type -> agent.AgentInfo + 4, // 61: agent.Agent.CommandError:input_type -> agent.AgentCommandError + 29, // 62: agent.Agent.AbortUpdate:input_type -> agent.AgentAbortUpdate + 59, // 63: agent.Agent.TokenReplaced:input_type -> common.Empty + 60, // 64: agent.Agent.DeploymentStatus:input_type -> common.DeploymentStatusMessage + 61, // 65: agent.Agent.ContainerState:input_type -> common.ContainerStateListMessage + 62, // 66: agent.Agent.ContainerLogStream:input_type -> common.ContainerLogMessage + 63, // 67: agent.Agent.SecretList:input_type -> common.ListSecretsResponse + 59, // 68: agent.Agent.DeleteContainers:input_type -> common.Empty + 64, // 69: agent.Agent.ContainerLog:input_type -> common.ContainerLogListResponse + 65, // 70: agent.Agent.ContainerInspect:input_type -> common.ContainerInspectResponse + 2, // 71: agent.Agent.Connect:output_type -> agent.AgentCommand + 59, // 72: agent.Agent.CommandError:output_type -> common.Empty + 59, // 73: agent.Agent.AbortUpdate:output_type -> common.Empty + 59, // 74: agent.Agent.TokenReplaced:output_type -> common.Empty + 59, // 75: agent.Agent.DeploymentStatus:output_type -> common.Empty + 59, // 76: agent.Agent.ContainerState:output_type -> common.Empty + 59, // 77: agent.Agent.ContainerLogStream:output_type -> common.Empty + 59, // 78: agent.Agent.SecretList:output_type -> common.Empty + 59, // 79: agent.Agent.DeleteContainers:output_type -> common.Empty + 59, // 80: agent.Agent.ContainerLog:output_type -> common.Empty + 59, // 81: agent.Agent.ContainerInspect:output_type -> common.Empty + 71, // [71:82] is the sub-list for method output_type + 60, // [60:71] is the sub-list for method input_type + 60, // [60:60] is the sub-list for extension type_name + 60, // [60:60] is the sub-list for extension extendee + 0, // [0:60] is the sub-list for field type_name } func init() { file_protobuf_proto_agent_proto_init() } @@ -3298,7 +3202,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*VersionDeployRequest); i { + switch v := v.(*DeployRequest); i { case 0: return &v.state case 1: @@ -3322,7 +3226,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*InstanceConfig); i { + switch v := v.(*RegistryAuth); i { case 0: return &v.state case 1: @@ -3334,7 +3238,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*RegistryAuth); i { + switch v := v.(*Port); i { case 0: return &v.state case 1: @@ -3346,7 +3250,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Port); i { + switch v := v.(*PortRange); i { case 0: return &v.state case 1: @@ -3358,7 +3262,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PortRange); i { + switch v := v.(*PortRangeBinding); i { case 0: return &v.state case 1: @@ -3370,7 +3274,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PortRangeBinding); i { + switch v := v.(*Volume); i { case 0: return &v.state case 1: @@ -3382,7 +3286,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Volume); i { + switch v := v.(*VolumeLink); i { case 0: return &v.state case 1: @@ -3394,7 +3298,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*VolumeLink); i { + switch v := v.(*InitContainer); i { case 0: return &v.state case 1: @@ -3406,7 +3310,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*InitContainer); i { + switch v := v.(*ImportContainer); i { case 0: return &v.state case 1: @@ -3418,7 +3322,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ImportContainer); i { + switch v := v.(*LogConfig); i { case 0: return &v.state case 1: @@ -3430,7 +3334,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LogConfig); i { + switch v := v.(*Marker); i { case 0: return &v.state case 1: @@ -3442,7 +3346,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Marker); i { + switch v := v.(*Metrics); i { case 0: return &v.state case 1: @@ -3454,7 +3358,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*Metrics); i { + switch v := v.(*ExpectedState); i { case 0: return &v.state case 1: @@ -3466,7 +3370,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ExpectedState); i { + switch v := v.(*DagentContainerConfig); i { case 0: return &v.state case 1: @@ -3478,7 +3382,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DagentContainerConfig); i { + switch v := v.(*CraneContainerConfig); i { case 0: return &v.state case 1: @@ -3490,7 +3394,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CraneContainerConfig); i { + switch v := v.(*CommonContainerConfig); i { case 0: return &v.state case 1: @@ -3502,7 +3406,7 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*CommonContainerConfig); i { + switch v := v.(*DeployWorkloadRequest); i { case 0: return &v.state case 1: @@ -3514,18 +3418,6 @@ func file_protobuf_proto_agent_proto_init() { } } file_protobuf_proto_agent_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*DeployRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_protobuf_proto_agent_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ContainerStateRequest); i { case 0: return &v.state @@ -3537,7 +3429,7 @@ func file_protobuf_proto_agent_proto_init() { return nil } } - file_protobuf_proto_agent_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { + file_protobuf_proto_agent_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ContainerDeleteRequest); i { case 0: return &v.state @@ -3549,7 +3441,7 @@ func file_protobuf_proto_agent_proto_init() { return nil } } - file_protobuf_proto_agent_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { + file_protobuf_proto_agent_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DeployRequestLegacy); i { case 0: return &v.state @@ -3561,7 +3453,7 @@ func file_protobuf_proto_agent_proto_init() { return nil } } - file_protobuf_proto_agent_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { + file_protobuf_proto_agent_proto_msgTypes[26].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*AgentUpdateRequest); i { case 0: return &v.state @@ -3573,7 +3465,7 @@ func file_protobuf_proto_agent_proto_init() { return nil } } - file_protobuf_proto_agent_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { + file_protobuf_proto_agent_proto_msgTypes[27].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ReplaceTokenRequest); i { case 0: return &v.state @@ -3585,7 +3477,7 @@ func file_protobuf_proto_agent_proto_init() { return nil } } - file_protobuf_proto_agent_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { + file_protobuf_proto_agent_proto_msgTypes[28].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*AgentAbortUpdate); i { case 0: return &v.state @@ -3597,7 +3489,7 @@ func file_protobuf_proto_agent_proto_init() { return nil } } - file_protobuf_proto_agent_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { + file_protobuf_proto_agent_proto_msgTypes[29].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ContainerLogRequest); i { case 0: return &v.state @@ -3609,7 +3501,7 @@ func file_protobuf_proto_agent_proto_init() { return nil } } - file_protobuf_proto_agent_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { + file_protobuf_proto_agent_proto_msgTypes[30].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*ContainerInspectRequest); i { case 0: return &v.state @@ -3621,7 +3513,7 @@ func file_protobuf_proto_agent_proto_init() { return nil } } - file_protobuf_proto_agent_proto_msgTypes[32].Exporter = func(v interface{}, i int) interface{} { + file_protobuf_proto_agent_proto_msgTypes[31].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*CloseConnectionRequest); i { case 0: return &v.state @@ -3655,23 +3547,22 @@ func file_protobuf_proto_agent_proto_init() { (*AgentCommandError_ContainerLog)(nil), (*AgentCommandError_ContainerInspect)(nil), } - file_protobuf_proto_agent_proto_msgTypes[7].OneofWrappers = []interface{}{} - file_protobuf_proto_agent_proto_msgTypes[9].OneofWrappers = []interface{}{} - file_protobuf_proto_agent_proto_msgTypes[12].OneofWrappers = []interface{}{} - file_protobuf_proto_agent_proto_msgTypes[14].OneofWrappers = []interface{}{} + file_protobuf_proto_agent_proto_msgTypes[8].OneofWrappers = []interface{}{} + file_protobuf_proto_agent_proto_msgTypes[11].OneofWrappers = []interface{}{} + file_protobuf_proto_agent_proto_msgTypes[13].OneofWrappers = []interface{}{} + file_protobuf_proto_agent_proto_msgTypes[18].OneofWrappers = []interface{}{} file_protobuf_proto_agent_proto_msgTypes[19].OneofWrappers = []interface{}{} file_protobuf_proto_agent_proto_msgTypes[20].OneofWrappers = []interface{}{} file_protobuf_proto_agent_proto_msgTypes[21].OneofWrappers = []interface{}{} file_protobuf_proto_agent_proto_msgTypes[22].OneofWrappers = []interface{}{} file_protobuf_proto_agent_proto_msgTypes[23].OneofWrappers = []interface{}{} - file_protobuf_proto_agent_proto_msgTypes[24].OneofWrappers = []interface{}{} type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_protobuf_proto_agent_proto_rawDesc, NumEnums: 1, - NumMessages: 44, + NumMessages: 43, NumExtensions: 0, NumServices: 1, }, diff --git a/protobuf/go/common/common.pb.go b/protobuf/go/common/common.pb.go index bab113cc9c..c03b607c53 100644 --- a/protobuf/go/common/common.pb.go +++ b/protobuf/go/common/common.pb.go @@ -1632,20 +1632,20 @@ func (x *KeyValue) GetValue() string { return "" } -type ListSecretsResponse struct { +type ContainerOrPrefix struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Prefix string `protobuf:"bytes,1,opt,name=prefix,proto3" json:"prefix,omitempty"` - Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` - PublicKey string `protobuf:"bytes,3,opt,name=publicKey,proto3" json:"publicKey,omitempty"` - HasKeys bool `protobuf:"varint,4,opt,name=hasKeys,proto3" json:"hasKeys,omitempty"` - Keys []string `protobuf:"bytes,5,rep,name=keys,proto3" json:"keys,omitempty"` + // Types that are assignable to Target: + // + // *ContainerOrPrefix_Container + // *ContainerOrPrefix_Prefix + Target isContainerOrPrefix_Target `protobuf_oneof:"target"` } -func (x *ListSecretsResponse) Reset() { - *x = ListSecretsResponse{} +func (x *ContainerOrPrefix) Reset() { + *x = ContainerOrPrefix{} if protoimpl.UnsafeEnabled { mi := &file_protobuf_proto_common_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1653,13 +1653,13 @@ func (x *ListSecretsResponse) Reset() { } } -func (x *ListSecretsResponse) String() string { +func (x *ContainerOrPrefix) String() string { return protoimpl.X.MessageStringOf(x) } -func (*ListSecretsResponse) ProtoMessage() {} +func (*ContainerOrPrefix) ProtoMessage() {} -func (x *ListSecretsResponse) ProtoReflect() protoreflect.Message { +func (x *ContainerOrPrefix) ProtoReflect() protoreflect.Message { mi := &file_protobuf_proto_common_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -1671,37 +1671,102 @@ func (x *ListSecretsResponse) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use ListSecretsResponse.ProtoReflect.Descriptor instead. -func (*ListSecretsResponse) Descriptor() ([]byte, []int) { +// Deprecated: Use ContainerOrPrefix.ProtoReflect.Descriptor instead. +func (*ContainerOrPrefix) Descriptor() ([]byte, []int) { return file_protobuf_proto_common_proto_rawDescGZIP(), []int{16} } -func (x *ListSecretsResponse) GetPrefix() string { - if x != nil { +func (m *ContainerOrPrefix) GetTarget() isContainerOrPrefix_Target { + if m != nil { + return m.Target + } + return nil +} + +func (x *ContainerOrPrefix) GetContainer() *ContainerIdentifier { + if x, ok := x.GetTarget().(*ContainerOrPrefix_Container); ok { + return x.Container + } + return nil +} + +func (x *ContainerOrPrefix) GetPrefix() string { + if x, ok := x.GetTarget().(*ContainerOrPrefix_Prefix); ok { return x.Prefix } return "" } -func (x *ListSecretsResponse) GetName() string { - if x != nil { - return x.Name +type isContainerOrPrefix_Target interface { + isContainerOrPrefix_Target() +} + +type ContainerOrPrefix_Container struct { + Container *ContainerIdentifier `protobuf:"bytes,1,opt,name=container,proto3,oneof"` +} + +type ContainerOrPrefix_Prefix struct { + Prefix string `protobuf:"bytes,2,opt,name=prefix,proto3,oneof"` +} + +func (*ContainerOrPrefix_Container) isContainerOrPrefix_Target() {} + +func (*ContainerOrPrefix_Prefix) isContainerOrPrefix_Target() {} + +type ListSecretsResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Target *ContainerOrPrefix `protobuf:"bytes,1,opt,name=target,proto3" json:"target,omitempty"` + PublicKey string `protobuf:"bytes,3,opt,name=publicKey,proto3" json:"publicKey,omitempty"` + Keys []string `protobuf:"bytes,4,rep,name=keys,proto3" json:"keys,omitempty"` +} + +func (x *ListSecretsResponse) Reset() { + *x = ListSecretsResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_protobuf_proto_common_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } - return "" } -func (x *ListSecretsResponse) GetPublicKey() string { +func (x *ListSecretsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListSecretsResponse) ProtoMessage() {} + +func (x *ListSecretsResponse) ProtoReflect() protoreflect.Message { + mi := &file_protobuf_proto_common_proto_msgTypes[17] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListSecretsResponse.ProtoReflect.Descriptor instead. +func (*ListSecretsResponse) Descriptor() ([]byte, []int) { + return file_protobuf_proto_common_proto_rawDescGZIP(), []int{17} +} + +func (x *ListSecretsResponse) GetTarget() *ContainerOrPrefix { if x != nil { - return x.PublicKey + return x.Target } - return "" + return nil } -func (x *ListSecretsResponse) GetHasKeys() bool { +func (x *ListSecretsResponse) GetPublicKey() string { if x != nil { - return x.HasKeys + return x.PublicKey } - return false + return "" } func (x *ListSecretsResponse) GetKeys() []string { @@ -1723,7 +1788,7 @@ type UniqueKey struct { func (x *UniqueKey) Reset() { *x = UniqueKey{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_common_proto_msgTypes[17] + mi := &file_protobuf_proto_common_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1736,7 +1801,7 @@ func (x *UniqueKey) String() string { func (*UniqueKey) ProtoMessage() {} func (x *UniqueKey) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_common_proto_msgTypes[17] + mi := &file_protobuf_proto_common_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1749,7 +1814,7 @@ func (x *UniqueKey) ProtoReflect() protoreflect.Message { // Deprecated: Use UniqueKey.ProtoReflect.Descriptor instead. func (*UniqueKey) Descriptor() ([]byte, []int) { - return file_protobuf_proto_common_proto_rawDescGZIP(), []int{17} + return file_protobuf_proto_common_proto_rawDescGZIP(), []int{18} } func (x *UniqueKey) GetId() string { @@ -1778,7 +1843,7 @@ type ContainerIdentifier struct { func (x *ContainerIdentifier) Reset() { *x = ContainerIdentifier{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_common_proto_msgTypes[18] + mi := &file_protobuf_proto_common_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1791,7 +1856,7 @@ func (x *ContainerIdentifier) String() string { func (*ContainerIdentifier) ProtoMessage() {} func (x *ContainerIdentifier) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_common_proto_msgTypes[18] + mi := &file_protobuf_proto_common_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1804,7 +1869,7 @@ func (x *ContainerIdentifier) ProtoReflect() protoreflect.Message { // Deprecated: Use ContainerIdentifier.ProtoReflect.Descriptor instead. func (*ContainerIdentifier) Descriptor() ([]byte, []int) { - return file_protobuf_proto_common_proto_rawDescGZIP(), []int{18} + return file_protobuf_proto_common_proto_rawDescGZIP(), []int{19} } func (x *ContainerIdentifier) GetPrefix() string { @@ -1833,7 +1898,7 @@ type ContainerCommandRequest struct { func (x *ContainerCommandRequest) Reset() { *x = ContainerCommandRequest{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_common_proto_msgTypes[19] + mi := &file_protobuf_proto_common_proto_msgTypes[20] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1846,7 +1911,7 @@ func (x *ContainerCommandRequest) String() string { func (*ContainerCommandRequest) ProtoMessage() {} func (x *ContainerCommandRequest) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_common_proto_msgTypes[19] + mi := &file_protobuf_proto_common_proto_msgTypes[20] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1859,7 +1924,7 @@ func (x *ContainerCommandRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ContainerCommandRequest.ProtoReflect.Descriptor instead. func (*ContainerCommandRequest) Descriptor() ([]byte, []int) { - return file_protobuf_proto_common_proto_rawDescGZIP(), []int{19} + return file_protobuf_proto_common_proto_rawDescGZIP(), []int{20} } func (x *ContainerCommandRequest) GetContainer() *ContainerIdentifier { @@ -1881,17 +1946,13 @@ type DeleteContainersRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // Types that are assignable to Target: - // - // *DeleteContainersRequest_Container - // *DeleteContainersRequest_Prefix - Target isDeleteContainersRequest_Target `protobuf_oneof:"target"` + Target *ContainerOrPrefix `protobuf:"bytes,1,opt,name=target,proto3" json:"target,omitempty"` } func (x *DeleteContainersRequest) Reset() { *x = DeleteContainersRequest{} if protoimpl.UnsafeEnabled { - mi := &file_protobuf_proto_common_proto_msgTypes[20] + mi := &file_protobuf_proto_common_proto_msgTypes[21] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1904,7 +1965,7 @@ func (x *DeleteContainersRequest) String() string { func (*DeleteContainersRequest) ProtoMessage() {} func (x *DeleteContainersRequest) ProtoReflect() protoreflect.Message { - mi := &file_protobuf_proto_common_proto_msgTypes[20] + mi := &file_protobuf_proto_common_proto_msgTypes[21] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1917,46 +1978,16 @@ func (x *DeleteContainersRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use DeleteContainersRequest.ProtoReflect.Descriptor instead. func (*DeleteContainersRequest) Descriptor() ([]byte, []int) { - return file_protobuf_proto_common_proto_rawDescGZIP(), []int{20} + return file_protobuf_proto_common_proto_rawDescGZIP(), []int{21} } -func (m *DeleteContainersRequest) GetTarget() isDeleteContainersRequest_Target { - if m != nil { - return m.Target - } - return nil -} - -func (x *DeleteContainersRequest) GetContainer() *ContainerIdentifier { - if x, ok := x.GetTarget().(*DeleteContainersRequest_Container); ok { - return x.Container +func (x *DeleteContainersRequest) GetTarget() *ContainerOrPrefix { + if x != nil { + return x.Target } return nil } -func (x *DeleteContainersRequest) GetPrefix() string { - if x, ok := x.GetTarget().(*DeleteContainersRequest_Prefix); ok { - return x.Prefix - } - return "" -} - -type isDeleteContainersRequest_Target interface { - isDeleteContainersRequest_Target() -} - -type DeleteContainersRequest_Container struct { - Container *ContainerIdentifier `protobuf:"bytes,201,opt,name=container,proto3,oneof"` -} - -type DeleteContainersRequest_Prefix struct { - Prefix string `protobuf:"bytes,202,opt,name=prefix,proto3,oneof"` -} - -func (*DeleteContainersRequest_Container) isDeleteContainersRequest_Target() {} - -func (*DeleteContainersRequest_Prefix) isDeleteContainersRequest_Target() {} - var File_protobuf_proto_common_proto protoreflect.FileDescriptor var file_protobuf_proto_common_proto_rawDesc = []byte{ @@ -2106,117 +2137,120 @@ var file_protobuf_proto_common_proto_rawDesc = []byte{ 0x75, 0x65, 0x73, 0x74, 0x73, 0x22, 0x32, 0x0a, 0x08, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x65, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x8d, 0x01, 0x0a, 0x13, 0x4c, 0x69, - 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x74, 0x0a, 0x11, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4f, 0x72, 0x50, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x3b, + 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x48, 0x00, + 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x06, 0x70, + 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x70, + 0x72, 0x65, 0x66, 0x69, 0x78, 0x42, 0x08, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x22, + 0x7a, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x31, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, + 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4f, 0x72, 0x50, 0x72, 0x65, 0x66, 0x69, + 0x78, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x75, 0x62, + 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x75, + 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, + 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x22, 0x2d, 0x0a, 0x09, 0x55, + 0x6e, 0x69, 0x71, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x64, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, + 0x65, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x41, 0x0a, 0x13, 0x43, 0x6f, + 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, + 0x72, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, - 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x18, 0x0a, 0x07, 0x68, - 0x61, 0x73, 0x4b, 0x65, 0x79, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x68, 0x61, - 0x73, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x05, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x04, 0x6b, 0x65, 0x79, 0x73, 0x22, 0x2d, 0x0a, 0x09, 0x55, 0x6e, 0x69, - 0x71, 0x75, 0x65, 0x4b, 0x65, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x64, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x65, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x22, 0x41, 0x0a, 0x13, 0x43, 0x6f, 0x6e, 0x74, - 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x12, - 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x8e, 0x01, 0x0a, 0x17, - 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, - 0x69, 0x6e, 0x65, 0x72, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x6d, - 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x65, - 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, - 0x65, 0x72, 0x12, 0x38, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x7c, 0x0a, 0x17, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3c, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, - 0x69, 0x6e, 0x65, 0x72, 0x18, 0xc9, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, - 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x48, 0x00, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, - 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x19, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, - 0xca, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, - 0x42, 0x08, 0x0a, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2a, 0x64, 0x0a, 0x0e, 0x43, 0x6f, - 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1f, 0x0a, 0x1b, - 0x43, 0x4f, 0x4e, 0x54, 0x41, 0x49, 0x4e, 0x45, 0x52, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, - 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, - 0x07, 0x52, 0x55, 0x4e, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, - 0x49, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x45, 0x58, 0x49, 0x54, 0x45, - 0x44, 0x10, 0x03, 0x12, 0x0b, 0x0a, 0x07, 0x52, 0x45, 0x4d, 0x4f, 0x56, 0x45, 0x44, 0x10, 0x04, - 0x2a, 0x8f, 0x01, 0x0a, 0x10, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x21, 0x0a, 0x1d, 0x44, 0x45, 0x50, 0x4c, 0x4f, 0x59, 0x4d, - 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, - 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x50, 0x52, 0x45, 0x50, - 0x41, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x49, 0x4e, 0x5f, 0x50, 0x52, - 0x4f, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x53, 0x55, 0x43, 0x43, - 0x45, 0x53, 0x53, 0x46, 0x55, 0x4c, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x41, 0x49, 0x4c, - 0x45, 0x44, 0x10, 0x04, 0x12, 0x0c, 0x0a, 0x08, 0x4f, 0x42, 0x53, 0x4f, 0x4c, 0x45, 0x54, 0x45, - 0x10, 0x05, 0x12, 0x0e, 0x0a, 0x0a, 0x44, 0x4f, 0x57, 0x4e, 0x47, 0x52, 0x41, 0x44, 0x45, 0x44, - 0x10, 0x06, 0x2a, 0x5e, 0x0a, 0x16, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x22, 0x0a, 0x1e, - 0x44, 0x45, 0x50, 0x4c, 0x4f, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x4d, 0x45, 0x53, 0x53, 0x41, - 0x47, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, - 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x57, 0x41, - 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, 0x4f, 0x52, - 0x10, 0x03, 0x2a, 0x4b, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, 0x6f, 0x64, - 0x65, 0x12, 0x1c, 0x0a, 0x18, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x4d, 0x4f, 0x44, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x8e, 0x01, + 0x0a, 0x17, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6d, 0x6d, 0x61, + 0x6e, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x09, 0x63, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, + 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, + 0x69, 0x6e, 0x65, 0x72, 0x12, 0x38, 0x0a, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1a, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x09, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x4c, + 0x0a, 0x17, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x31, 0x0a, 0x06, 0x74, 0x61, 0x72, + 0x67, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4f, 0x72, 0x50, 0x72, + 0x65, 0x66, 0x69, 0x78, 0x52, 0x06, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x2a, 0x64, 0x0a, 0x0e, + 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1f, + 0x0a, 0x1b, 0x43, 0x4f, 0x4e, 0x54, 0x41, 0x49, 0x4e, 0x45, 0x52, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, - 0x0a, 0x0a, 0x06, 0x42, 0x52, 0x49, 0x44, 0x47, 0x45, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x48, - 0x4f, 0x53, 0x54, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x06, 0x2a, - 0x6e, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, - 0x12, 0x16, 0x0a, 0x12, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, - 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, 0x44, 0x45, - 0x46, 0x49, 0x4e, 0x45, 0x44, 0x10, 0x01, 0x12, 0x06, 0x0a, 0x02, 0x4e, 0x4f, 0x10, 0x02, 0x12, - 0x0e, 0x0a, 0x0a, 0x4f, 0x4e, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, 0x03, 0x12, - 0x0a, 0x0a, 0x06, 0x41, 0x4c, 0x57, 0x41, 0x59, 0x53, 0x10, 0x04, 0x12, 0x12, 0x0a, 0x0e, 0x55, - 0x4e, 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x45, 0x44, 0x10, 0x05, 0x2a, - 0x5b, 0x0a, 0x12, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, - 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x23, 0x0a, 0x1f, 0x44, 0x45, 0x50, 0x4c, 0x4f, 0x59, 0x4d, - 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x52, 0x41, 0x54, 0x45, 0x47, 0x59, 0x5f, 0x55, 0x4e, 0x53, - 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x52, 0x45, - 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x52, 0x4f, 0x4c, 0x4c, - 0x49, 0x4e, 0x47, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x02, 0x2a, 0x61, 0x0a, 0x0a, - 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x17, 0x56, 0x4f, - 0x4c, 0x55, 0x4d, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, - 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x06, 0x0a, 0x02, 0x52, 0x4f, 0x10, 0x01, 0x12, - 0x07, 0x0a, 0x03, 0x52, 0x57, 0x4f, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x52, 0x57, 0x58, 0x10, - 0x03, 0x12, 0x07, 0x0a, 0x03, 0x4d, 0x45, 0x4d, 0x10, 0x04, 0x12, 0x07, 0x0a, 0x03, 0x54, 0x4d, - 0x50, 0x10, 0x05, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x45, 0x43, 0x52, 0x45, 0x54, 0x10, 0x06, 0x2a, - 0xdf, 0x01, 0x0a, 0x0a, 0x44, 0x72, 0x69, 0x76, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, - 0x0a, 0x17, 0x44, 0x52, 0x49, 0x56, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, - 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x10, 0x0a, 0x0c, 0x4e, - 0x4f, 0x44, 0x45, 0x5f, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x01, 0x12, 0x14, 0x0a, - 0x10, 0x44, 0x52, 0x49, 0x56, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4e, 0x4f, 0x4e, - 0x45, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x47, 0x43, 0x50, 0x4c, 0x4f, 0x47, 0x53, 0x10, 0x03, - 0x12, 0x09, 0x0a, 0x05, 0x4c, 0x4f, 0x43, 0x41, 0x4c, 0x10, 0x04, 0x12, 0x0d, 0x0a, 0x09, 0x4a, - 0x53, 0x4f, 0x4e, 0x5f, 0x46, 0x49, 0x4c, 0x45, 0x10, 0x05, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x59, - 0x53, 0x4c, 0x4f, 0x47, 0x10, 0x06, 0x12, 0x0c, 0x0a, 0x08, 0x4a, 0x4f, 0x55, 0x52, 0x4e, 0x41, - 0x4c, 0x44, 0x10, 0x07, 0x12, 0x08, 0x0a, 0x04, 0x47, 0x45, 0x4c, 0x46, 0x10, 0x08, 0x12, 0x0b, - 0x0a, 0x07, 0x46, 0x4c, 0x55, 0x45, 0x4e, 0x54, 0x44, 0x10, 0x09, 0x12, 0x0b, 0x0a, 0x07, 0x41, - 0x57, 0x53, 0x4c, 0x4f, 0x47, 0x53, 0x10, 0x0a, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x50, 0x4c, 0x55, - 0x4e, 0x4b, 0x10, 0x0b, 0x12, 0x0b, 0x0a, 0x07, 0x45, 0x54, 0x57, 0x4c, 0x4f, 0x47, 0x53, 0x10, - 0x0c, 0x12, 0x0e, 0x0a, 0x0a, 0x4c, 0x4f, 0x47, 0x45, 0x4e, 0x54, 0x52, 0x49, 0x45, 0x53, 0x10, - 0x0d, 0x2a, 0x5f, 0x0a, 0x0e, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x53, 0x74, 0x72, 0x61, 0x74, - 0x65, 0x67, 0x79, 0x12, 0x1f, 0x0a, 0x1b, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x53, 0x54, - 0x52, 0x41, 0x54, 0x45, 0x47, 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, - 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x4e, 0x4f, 0x4e, 0x45, 0x5f, 0x45, 0x53, 0x10, - 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x10, 0x02, 0x12, 0x13, 0x0a, - 0x0f, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x57, 0x49, 0x54, 0x48, 0x5f, 0x54, 0x4c, 0x53, - 0x10, 0x03, 0x2a, 0x79, 0x0a, 0x12, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4f, - 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x1f, 0x43, 0x4f, 0x4e, 0x54, - 0x41, 0x49, 0x4e, 0x45, 0x52, 0x5f, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, 0x4e, 0x5f, - 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x13, 0x0a, - 0x0f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x43, 0x4f, 0x4e, 0x54, 0x41, 0x49, 0x4e, 0x45, 0x52, - 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x4f, 0x50, 0x5f, 0x43, 0x4f, 0x4e, 0x54, 0x41, - 0x49, 0x4e, 0x45, 0x52, 0x10, 0x02, 0x12, 0x15, 0x0a, 0x11, 0x52, 0x45, 0x53, 0x54, 0x41, 0x52, - 0x54, 0x5f, 0x43, 0x4f, 0x4e, 0x54, 0x41, 0x49, 0x4e, 0x45, 0x52, 0x10, 0x03, 0x42, 0x36, 0x5a, - 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x79, 0x72, 0x65, - 0x63, 0x74, 0x6f, 0x72, 0x2d, 0x69, 0x6f, 0x2f, 0x64, 0x79, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, - 0x69, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x67, 0x6f, 0x2f, 0x63, - 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x0b, 0x0a, 0x07, 0x52, 0x55, 0x4e, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, + 0x57, 0x41, 0x49, 0x54, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x0a, 0x0a, 0x06, 0x45, 0x58, 0x49, + 0x54, 0x45, 0x44, 0x10, 0x03, 0x12, 0x0b, 0x0a, 0x07, 0x52, 0x45, 0x4d, 0x4f, 0x56, 0x45, 0x44, + 0x10, 0x04, 0x2a, 0x8f, 0x01, 0x0a, 0x10, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x21, 0x0a, 0x1d, 0x44, 0x45, 0x50, 0x4c, 0x4f, + 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x53, + 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x50, 0x52, + 0x45, 0x50, 0x41, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x0f, 0x0a, 0x0b, 0x49, 0x4e, 0x5f, + 0x50, 0x52, 0x4f, 0x47, 0x52, 0x45, 0x53, 0x53, 0x10, 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x53, 0x55, + 0x43, 0x43, 0x45, 0x53, 0x53, 0x46, 0x55, 0x4c, 0x10, 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x41, + 0x49, 0x4c, 0x45, 0x44, 0x10, 0x04, 0x12, 0x0c, 0x0a, 0x08, 0x4f, 0x42, 0x53, 0x4f, 0x4c, 0x45, + 0x54, 0x45, 0x10, 0x05, 0x12, 0x0e, 0x0a, 0x0a, 0x44, 0x4f, 0x57, 0x4e, 0x47, 0x52, 0x41, 0x44, + 0x45, 0x44, 0x10, 0x06, 0x2a, 0x5e, 0x0a, 0x16, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x22, + 0x0a, 0x1e, 0x44, 0x45, 0x50, 0x4c, 0x4f, 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x4d, 0x45, 0x53, + 0x53, 0x41, 0x47, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, + 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, + 0x57, 0x41, 0x52, 0x4e, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x09, 0x0a, 0x05, 0x45, 0x52, 0x52, + 0x4f, 0x52, 0x10, 0x03, 0x2a, 0x4b, 0x0a, 0x0b, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x4d, + 0x6f, 0x64, 0x65, 0x12, 0x1c, 0x0a, 0x18, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x4d, + 0x4f, 0x44, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, + 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x42, 0x52, 0x49, 0x44, 0x47, 0x45, 0x10, 0x01, 0x12, 0x08, 0x0a, + 0x04, 0x48, 0x4f, 0x53, 0x54, 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, + 0x06, 0x2a, 0x6e, 0x0a, 0x0d, 0x52, 0x65, 0x73, 0x74, 0x61, 0x72, 0x74, 0x50, 0x6f, 0x6c, 0x69, + 0x63, 0x79, 0x12, 0x16, 0x0a, 0x12, 0x50, 0x4f, 0x4c, 0x49, 0x43, 0x59, 0x5f, 0x55, 0x4e, 0x53, + 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x55, 0x4e, + 0x44, 0x45, 0x46, 0x49, 0x4e, 0x45, 0x44, 0x10, 0x01, 0x12, 0x06, 0x0a, 0x02, 0x4e, 0x4f, 0x10, + 0x02, 0x12, 0x0e, 0x0a, 0x0a, 0x4f, 0x4e, 0x5f, 0x46, 0x41, 0x49, 0x4c, 0x55, 0x52, 0x45, 0x10, + 0x03, 0x12, 0x0a, 0x0a, 0x06, 0x41, 0x4c, 0x57, 0x41, 0x59, 0x53, 0x10, 0x04, 0x12, 0x12, 0x0a, + 0x0e, 0x55, 0x4e, 0x4c, 0x45, 0x53, 0x53, 0x5f, 0x53, 0x54, 0x4f, 0x50, 0x50, 0x45, 0x44, 0x10, + 0x05, 0x2a, 0x5b, 0x0a, 0x12, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, + 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x23, 0x0a, 0x1f, 0x44, 0x45, 0x50, 0x4c, 0x4f, + 0x59, 0x4d, 0x45, 0x4e, 0x54, 0x5f, 0x53, 0x54, 0x52, 0x41, 0x54, 0x45, 0x47, 0x59, 0x5f, 0x55, + 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, + 0x52, 0x45, 0x43, 0x52, 0x45, 0x41, 0x54, 0x45, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x52, 0x4f, + 0x4c, 0x4c, 0x49, 0x4e, 0x47, 0x5f, 0x55, 0x50, 0x44, 0x41, 0x54, 0x45, 0x10, 0x02, 0x2a, 0x61, + 0x0a, 0x0a, 0x56, 0x6f, 0x6c, 0x75, 0x6d, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x17, + 0x56, 0x4f, 0x4c, 0x55, 0x4d, 0x45, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x55, 0x4e, 0x53, 0x50, + 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x06, 0x0a, 0x02, 0x52, 0x4f, 0x10, + 0x01, 0x12, 0x07, 0x0a, 0x03, 0x52, 0x57, 0x4f, 0x10, 0x02, 0x12, 0x07, 0x0a, 0x03, 0x52, 0x57, + 0x58, 0x10, 0x03, 0x12, 0x07, 0x0a, 0x03, 0x4d, 0x45, 0x4d, 0x10, 0x04, 0x12, 0x07, 0x0a, 0x03, + 0x54, 0x4d, 0x50, 0x10, 0x05, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x45, 0x43, 0x52, 0x45, 0x54, 0x10, + 0x06, 0x2a, 0xdf, 0x01, 0x0a, 0x0a, 0x44, 0x72, 0x69, 0x76, 0x65, 0x72, 0x54, 0x79, 0x70, 0x65, + 0x12, 0x1b, 0x0a, 0x17, 0x44, 0x52, 0x49, 0x56, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, + 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x10, 0x0a, + 0x0c, 0x4e, 0x4f, 0x44, 0x45, 0x5f, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x01, 0x12, + 0x14, 0x0a, 0x10, 0x44, 0x52, 0x49, 0x56, 0x45, 0x52, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4e, + 0x4f, 0x4e, 0x45, 0x10, 0x02, 0x12, 0x0b, 0x0a, 0x07, 0x47, 0x43, 0x50, 0x4c, 0x4f, 0x47, 0x53, + 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x4c, 0x4f, 0x43, 0x41, 0x4c, 0x10, 0x04, 0x12, 0x0d, 0x0a, + 0x09, 0x4a, 0x53, 0x4f, 0x4e, 0x5f, 0x46, 0x49, 0x4c, 0x45, 0x10, 0x05, 0x12, 0x0a, 0x0a, 0x06, + 0x53, 0x59, 0x53, 0x4c, 0x4f, 0x47, 0x10, 0x06, 0x12, 0x0c, 0x0a, 0x08, 0x4a, 0x4f, 0x55, 0x52, + 0x4e, 0x41, 0x4c, 0x44, 0x10, 0x07, 0x12, 0x08, 0x0a, 0x04, 0x47, 0x45, 0x4c, 0x46, 0x10, 0x08, + 0x12, 0x0b, 0x0a, 0x07, 0x46, 0x4c, 0x55, 0x45, 0x4e, 0x54, 0x44, 0x10, 0x09, 0x12, 0x0b, 0x0a, + 0x07, 0x41, 0x57, 0x53, 0x4c, 0x4f, 0x47, 0x53, 0x10, 0x0a, 0x12, 0x0a, 0x0a, 0x06, 0x53, 0x50, + 0x4c, 0x55, 0x4e, 0x4b, 0x10, 0x0b, 0x12, 0x0b, 0x0a, 0x07, 0x45, 0x54, 0x57, 0x4c, 0x4f, 0x47, + 0x53, 0x10, 0x0c, 0x12, 0x0e, 0x0a, 0x0a, 0x4c, 0x4f, 0x47, 0x45, 0x4e, 0x54, 0x52, 0x49, 0x45, + 0x53, 0x10, 0x0d, 0x2a, 0x5f, 0x0a, 0x0e, 0x45, 0x78, 0x70, 0x6f, 0x73, 0x65, 0x53, 0x74, 0x72, + 0x61, 0x74, 0x65, 0x67, 0x79, 0x12, 0x1f, 0x0a, 0x1b, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, + 0x53, 0x54, 0x52, 0x41, 0x54, 0x45, 0x47, 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, + 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0b, 0x0a, 0x07, 0x4e, 0x4f, 0x4e, 0x45, 0x5f, 0x45, + 0x53, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x10, 0x02, 0x12, + 0x13, 0x0a, 0x0f, 0x45, 0x58, 0x50, 0x4f, 0x53, 0x45, 0x5f, 0x57, 0x49, 0x54, 0x48, 0x5f, 0x54, + 0x4c, 0x53, 0x10, 0x03, 0x2a, 0x79, 0x0a, 0x12, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, + 0x72, 0x4f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x23, 0x0a, 0x1f, 0x43, 0x4f, + 0x4e, 0x54, 0x41, 0x49, 0x4e, 0x45, 0x52, 0x5f, 0x4f, 0x50, 0x45, 0x52, 0x41, 0x54, 0x49, 0x4f, + 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, + 0x13, 0x0a, 0x0f, 0x53, 0x54, 0x41, 0x52, 0x54, 0x5f, 0x43, 0x4f, 0x4e, 0x54, 0x41, 0x49, 0x4e, + 0x45, 0x52, 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x4f, 0x50, 0x5f, 0x43, 0x4f, 0x4e, + 0x54, 0x41, 0x49, 0x4e, 0x45, 0x52, 0x10, 0x02, 0x12, 0x15, 0x0a, 0x11, 0x52, 0x45, 0x53, 0x54, + 0x41, 0x52, 0x54, 0x5f, 0x43, 0x4f, 0x4e, 0x54, 0x41, 0x49, 0x4e, 0x45, 0x52, 0x10, 0x03, 0x42, + 0x36, 0x5a, 0x34, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x79, + 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2d, 0x69, 0x6f, 0x2f, 0x64, 0x79, 0x72, 0x65, 0x63, 0x74, + 0x6f, 0x72, 0x69, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x67, 0x6f, + 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -2232,7 +2266,7 @@ func file_protobuf_proto_common_proto_rawDescGZIP() []byte { } var file_protobuf_proto_common_proto_enumTypes = make([]protoimpl.EnumInfo, 10) -var file_protobuf_proto_common_proto_msgTypes = make([]protoimpl.MessageInfo, 22) +var file_protobuf_proto_common_proto_msgTypes = make([]protoimpl.MessageInfo, 23) var file_protobuf_proto_common_proto_goTypes = []interface{}{ (ContainerState)(0), // 0: common.ContainerState (DeploymentStatus)(0), // 1: common.DeploymentStatus @@ -2260,13 +2294,14 @@ var file_protobuf_proto_common_proto_goTypes = []interface{}{ (*Resource)(nil), // 23: common.Resource (*ResourceConfig)(nil), // 24: common.ResourceConfig (*KeyValue)(nil), // 25: common.KeyValue - (*ListSecretsResponse)(nil), // 26: common.ListSecretsResponse - (*UniqueKey)(nil), // 27: common.UniqueKey - (*ContainerIdentifier)(nil), // 28: common.ContainerIdentifier - (*ContainerCommandRequest)(nil), // 29: common.ContainerCommandRequest - (*DeleteContainersRequest)(nil), // 30: common.DeleteContainersRequest - nil, // 31: common.ContainerStateItem.LabelsEntry - (*timestamppb.Timestamp)(nil), // 32: google.protobuf.Timestamp + (*ContainerOrPrefix)(nil), // 26: common.ContainerOrPrefix + (*ListSecretsResponse)(nil), // 27: common.ListSecretsResponse + (*UniqueKey)(nil), // 28: common.UniqueKey + (*ContainerIdentifier)(nil), // 29: common.ContainerIdentifier + (*ContainerCommandRequest)(nil), // 30: common.ContainerCommandRequest + (*DeleteContainersRequest)(nil), // 31: common.DeleteContainersRequest + nil, // 32: common.ContainerStateItem.LabelsEntry + (*timestamppb.Timestamp)(nil), // 33: google.protobuf.Timestamp } var file_protobuf_proto_common_proto_depIdxs = []int32{ 0, // 0: common.InstanceDeploymentItem.state:type_name -> common.ContainerState @@ -2275,21 +2310,23 @@ var file_protobuf_proto_common_proto_depIdxs = []int32{ 12, // 3: common.DeploymentStatusMessage.containerProgress:type_name -> common.DeployContainerProgress 2, // 4: common.DeploymentStatusMessage.logLevel:type_name -> common.DeploymentMessageLevel 16, // 5: common.ContainerStateListMessage.data:type_name -> common.ContainerStateItem - 28, // 6: common.ContainerStateItem.id:type_name -> common.ContainerIdentifier - 32, // 7: common.ContainerStateItem.createdAt:type_name -> google.protobuf.Timestamp + 29, // 6: common.ContainerStateItem.id:type_name -> common.ContainerIdentifier + 33, // 7: common.ContainerStateItem.createdAt:type_name -> google.protobuf.Timestamp 0, // 8: common.ContainerStateItem.state:type_name -> common.ContainerState 14, // 9: common.ContainerStateItem.ports:type_name -> common.ContainerStateItemPort - 31, // 10: common.ContainerStateItem.labels:type_name -> common.ContainerStateItem.LabelsEntry + 32, // 10: common.ContainerStateItem.labels:type_name -> common.ContainerStateItem.LabelsEntry 23, // 11: common.ResourceConfig.limits:type_name -> common.Resource 23, // 12: common.ResourceConfig.requests:type_name -> common.Resource - 28, // 13: common.ContainerCommandRequest.container:type_name -> common.ContainerIdentifier - 9, // 14: common.ContainerCommandRequest.operation:type_name -> common.ContainerOperation - 28, // 15: common.DeleteContainersRequest.container:type_name -> common.ContainerIdentifier - 16, // [16:16] is the sub-list for method output_type - 16, // [16:16] is the sub-list for method input_type - 16, // [16:16] is the sub-list for extension type_name - 16, // [16:16] is the sub-list for extension extendee - 0, // [0:16] is the sub-list for field type_name + 29, // 13: common.ContainerOrPrefix.container:type_name -> common.ContainerIdentifier + 26, // 14: common.ListSecretsResponse.target:type_name -> common.ContainerOrPrefix + 29, // 15: common.ContainerCommandRequest.container:type_name -> common.ContainerIdentifier + 9, // 16: common.ContainerCommandRequest.operation:type_name -> common.ContainerOperation + 26, // 17: common.DeleteContainersRequest.target:type_name -> common.ContainerOrPrefix + 18, // [18:18] is the sub-list for method output_type + 18, // [18:18] is the sub-list for method input_type + 18, // [18:18] is the sub-list for extension type_name + 18, // [18:18] is the sub-list for extension extendee + 0, // [0:18] is the sub-list for field type_name } func init() { file_protobuf_proto_common_proto_init() } @@ -2491,7 +2528,7 @@ func file_protobuf_proto_common_proto_init() { } } file_protobuf_proto_common_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ListSecretsResponse); i { + switch v := v.(*ContainerOrPrefix); i { case 0: return &v.state case 1: @@ -2503,7 +2540,7 @@ func file_protobuf_proto_common_proto_init() { } } file_protobuf_proto_common_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UniqueKey); i { + switch v := v.(*ListSecretsResponse); i { case 0: return &v.state case 1: @@ -2515,7 +2552,7 @@ func file_protobuf_proto_common_proto_init() { } } file_protobuf_proto_common_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ContainerIdentifier); i { + switch v := v.(*UniqueKey); i { case 0: return &v.state case 1: @@ -2527,7 +2564,7 @@ func file_protobuf_proto_common_proto_init() { } } file_protobuf_proto_common_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ContainerCommandRequest); i { + switch v := v.(*ContainerIdentifier); i { case 0: return &v.state case 1: @@ -2539,6 +2576,18 @@ func file_protobuf_proto_common_proto_init() { } } file_protobuf_proto_common_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ContainerCommandRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_protobuf_proto_common_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*DeleteContainersRequest); i { case 0: return &v.state @@ -2561,9 +2610,9 @@ func file_protobuf_proto_common_proto_init() { file_protobuf_proto_common_proto_msgTypes[12].OneofWrappers = []interface{}{} file_protobuf_proto_common_proto_msgTypes[13].OneofWrappers = []interface{}{} file_protobuf_proto_common_proto_msgTypes[14].OneofWrappers = []interface{}{} - file_protobuf_proto_common_proto_msgTypes[20].OneofWrappers = []interface{}{ - (*DeleteContainersRequest_Container)(nil), - (*DeleteContainersRequest_Prefix)(nil), + file_protobuf_proto_common_proto_msgTypes[16].OneofWrappers = []interface{}{ + (*ContainerOrPrefix_Container)(nil), + (*ContainerOrPrefix_Prefix)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -2571,7 +2620,7 @@ func file_protobuf_proto_common_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_protobuf_proto_common_proto_rawDesc, NumEnums: 10, - NumMessages: 22, + NumMessages: 23, NumExtensions: 0, NumServices: 0, }, diff --git a/protobuf/proto/agent.proto b/protobuf/proto/agent.proto index 90ae346d5b..8b6e43eb40 100644 --- a/protobuf/proto/agent.proto +++ b/protobuf/proto/agent.proto @@ -36,7 +36,6 @@ service Agent { rpc ContainerLogStream(stream common.ContainerLogMessage) returns (common.Empty); - // one-shot requests rpc SecretList(common.ListSecretsResponse) returns (common.Empty); rpc DeleteContainers(common.Empty) returns (common.Empty); @@ -57,7 +56,7 @@ message AgentInfo { message AgentCommand { oneof command { - VersionDeployRequest deploy = 1; + DeployRequest deploy = 1; ContainerStateRequest containerState = 2; ContainerDeleteRequest containerDelete = 3; DeployRequestLegacy deployLegacy = 4; @@ -92,31 +91,20 @@ message AgentCommandError { */ message DeployResponse { bool started = 1; } -message VersionDeployRequest { +message DeployRequest { string id = 1; string versionName = 2; string releaseNotes = 3; + string prefix = 4; - repeated DeployRequest requests = 4; + map secrets = 5; + repeated DeployWorkloadRequest requests = 6; } /* * Request for a keys of existing secrets in a prefix, eg. namespace */ -message ListSecretsRequest { common.ContainerIdentifier container = 1; } - -/** - * Deploys a single container - * - */ -message InstanceConfig { - /* - prefix mapped into host folder structure, - used as namespace id - */ - string prefix = 1; - optional string mountPath = 2; // mount path of instance (docker only) - map environment = 3; // environment variable map - optional string repositoryPrefix = 4; // registry repo prefix +message ListSecretsRequest { + common.ContainerOrPrefix target = 1; } message RegistryAuth { @@ -241,25 +229,20 @@ message CommonContainerConfig { repeated InitContainer initContainers = 1007; } -message DeployRequest { +message DeployWorkloadRequest { string id = 1; string containerName = 2; - /* InstanceConfig is set for multiple containers */ - InstanceConfig instanceConfig = 3; - /* ContainerConfigs */ - optional CommonContainerConfig common = 4; - optional DagentContainerConfig dagent = 5; - optional CraneContainerConfig crane = 6; + optional CommonContainerConfig common = 3; + optional DagentContainerConfig dagent = 4; + optional CraneContainerConfig crane = 5; - /* Runtime info and requirements of a container */ - optional string runtimeConfig = 7; - optional string registry = 8; - string imageName = 9; - string tag = 10; + optional string registry = 6; + string imageName = 7; + string tag = 8; - optional RegistryAuth registryAuth = 11; + optional RegistryAuth registryAuth = 9; } message ContainerStateRequest { diff --git a/protobuf/proto/common.proto b/protobuf/proto/common.proto index 35c6114c61..98f5aa2c53 100644 --- a/protobuf/proto/common.proto +++ b/protobuf/proto/common.proto @@ -187,12 +187,17 @@ message KeyValue { string value = 101; } +message ContainerOrPrefix { + oneof target { + common.ContainerIdentifier container = 1; + string prefix = 2; + } +} + message ListSecretsResponse { - string prefix = 1; - string name = 2; + ContainerOrPrefix target = 1; string publicKey = 3; - bool hasKeys = 4; - repeated string keys = 5; + repeated string keys = 4; } message UniqueKey { @@ -218,8 +223,5 @@ message ContainerCommandRequest { } message DeleteContainersRequest { - oneof target { - common.ContainerIdentifier container = 201; - string prefix = 202; - } + ContainerOrPrefix target = 1; } diff --git a/web/crux-ui/e2e/utils/config-bundle.ts b/web/crux-ui/e2e/utils/config-bundle.ts index 203d482e23..4130beb1b3 100644 --- a/web/crux-ui/e2e/utils/config-bundle.ts +++ b/web/crux-ui/e2e/utils/config-bundle.ts @@ -1,13 +1,13 @@ /* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable import/prefer-default-export */ -import { PatchConfigBundleMessage, WS_TYPE_PATCH_CONFIG_BUNDLE } from '@app/models' import { Page, expect } from '@playwright/test' import { TEAM_ROUTES } from './common' import { waitSocketRef, wsPatchSent } from './websocket' +import { PatchConfigMessage, WS_TYPE_PATCH_CONFIG } from '@app/models' -const matchPatchEnvironment = (expected: Record) => (message: PatchConfigBundleMessage) => +const matchPatchEnvironment = (expected: Record) => (message: PatchConfigMessage) => Object.entries(expected).every( - ([key, value]) => message.environment?.find(it => it.key === key && it.value === value), + ([key, value]) => message.config?.environment?.find(it => it.key === key && it.value === value), ) export const createConfigBundle = async (page: Page, name: string, data: Record): Promise => { @@ -30,7 +30,7 @@ export const createConfigBundle = async (page: Page, name: string, data: Record< await page.locator('button:has-text("Edit")').click() - const wsPatchReceived = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG_BUNDLE, matchPatchEnvironment(data)) + const wsPatchReceived = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, matchPatchEnvironment(data)) const entries = Object.entries(data) for (let i = 0; i < entries.length; i++) { diff --git a/web/crux-ui/e2e/utils/projects.ts b/web/crux-ui/e2e/utils/projects.ts index 3c0bf4db22..d3be58a65c 100644 --- a/web/crux-ui/e2e/utils/projects.ts +++ b/web/crux-ui/e2e/utils/projects.ts @@ -69,7 +69,7 @@ export const createImage = async (page: Page, projectId: string, versionId: stri await page.waitForSelector('button:has-text("Add image")') - const settingsButton = await page.waitForSelector(`[src="/image_config_icon.svg"]:right-of(:text("${image}"))`) + const settingsButton = await page.waitForSelector(`[src="/container_config.svg"]:right-of(:text("${image}"))`) await settingsButton.click() await page.waitForSelector(`h2:has-text("Image")`) diff --git a/web/crux-ui/e2e/with-login/deployment/deployment-copy.spec.ts b/web/crux-ui/e2e/with-login/deployment/deployment-copy.spec.ts index 23fdddb7f3..72b356a475 100644 --- a/web/crux-ui/e2e/with-login/deployment/deployment-copy.spec.ts +++ b/web/crux-ui/e2e/with-login/deployment/deployment-copy.spec.ts @@ -44,7 +44,7 @@ const openContainerConfigByDeploymentTable = async (page: Page, containerName: s await expect(page.locator(`td:has-text("${containerName}")`).first()).toBeVisible() const containerSettingsButton = await page.waitForSelector( - `[src="/instance_config_icon.svg"]:right-of(:text("${containerName}"))`, + `[src="/concrete_container_config.svg"]:right-of(:text("${containerName}"))`, ) await containerSettingsButton.click() diff --git a/web/crux-ui/e2e/with-login/deployment/deployment-mutability-versioned.spec.ts b/web/crux-ui/e2e/with-login/deployment/deployment-mutability-versioned.spec.ts index ae42ce4b5a..853ec75928 100644 --- a/web/crux-ui/e2e/with-login/deployment/deployment-mutability-versioned.spec.ts +++ b/web/crux-ui/e2e/with-login/deployment/deployment-mutability-versioned.spec.ts @@ -1,8 +1,8 @@ import { expect } from '@playwright/test' -import { test } from '../../utils/test.fixture' import { DAGENT_NODE, TEAM_ROUTES } from '../../utils/common' import { deployWithDagent } from '../../utils/node-helper' import { addDeploymentToVersion, createImage, createProject, createVersion } from '../../utils/projects' +import { test } from '../../utils/test.fixture' const image = 'nginx' @@ -19,7 +19,9 @@ test.describe('Versioned Project incremental version', () => { await expect(await page.locator('button:has-text("Edit")')).toHaveCount(1) - const configButton = await page.locator(`[src="/instance_config_icon.svg"]:right-of(:has-text("${image}"))`).first() + const configButton = await page + .locator(`[src="/concrete_container_config.svg"]:right-of(:has-text("${image}"))`) + .first() await configButton.click() await page.waitForSelector('input[id="common.containerName"]') @@ -45,7 +47,9 @@ test.describe('Versioned Project incremental version', () => { await expect(await page.locator('button:has-text("Edit")')).toHaveCount(0) - const configButton = await page.locator(`[src="/instance_config_icon.svg"]:right-of(:has-text("${image}"))`).first() + const configButton = await page + .locator(`[src="/concrete_container_config.svg"]:right-of(:has-text("${image}"))`) + .first() await configButton.click() await page.waitForSelector('input[id="common.containerName"]') @@ -73,7 +77,9 @@ test.describe('Versioned Project incremental version', () => { await expect(await page.locator('button:has-text("Edit")')).toHaveCount(0) - const configButton = await page.locator(`[src="/instance_config_icon.svg"]:right-of(:has-text("${image}"))`).first() + const configButton = await page + .locator(`[src="/concrete_container_config.svg"]:right-of(:has-text("${image}"))`) + .first() await configButton.click() await page.waitForSelector('input[id="common.containerName"]') diff --git a/web/crux-ui/e2e/with-login/deployment/deployment-mutability-versionless.spec.ts b/web/crux-ui/e2e/with-login/deployment/deployment-mutability-versionless.spec.ts index 90f74364dc..7f0e925ee9 100644 --- a/web/crux-ui/e2e/with-login/deployment/deployment-mutability-versionless.spec.ts +++ b/web/crux-ui/e2e/with-login/deployment/deployment-mutability-versionless.spec.ts @@ -1,8 +1,8 @@ import { expect } from '@playwright/test' -import { test } from '../../utils/test.fixture' import { DAGENT_NODE, screenshotPath, TEAM_ROUTES } from '../../utils/common' import { deployWithDagent } from '../../utils/node-helper' import { addDeploymentToVersionlessProject, addImageToVersionlessProject, createProject } from '../../utils/projects' +import { test } from '../../utils/test.fixture' const image = 'nginx' @@ -14,7 +14,9 @@ test.describe('Versionless Project', () => { await expect(await page.locator('button:has-text("Edit")')).toHaveCount(1) - const configButton = await page.locator(`[src="/instance_config_icon.svg"]:right-of(:has-text("${image}"))`).first() + const configButton = await page + .locator(`[src="/concrete_container_config.svg"]:right-of(:has-text("${image}"))`) + .first() await configButton.click() await page.waitForSelector('input[id="common.containerName"]') @@ -36,7 +38,9 @@ test.describe('Versionless Project', () => { await expect(await page.locator('button:has-text("Edit")')).toHaveCount(1) - const configButton = await page.locator(`[src="/instance_config_icon.svg"]:right-of(:has-text("${image}"))`).first() + const configButton = await page + .locator(`[src="/concrete_container_config.svg"]:right-of(:has-text("${image}"))`) + .first() await configButton.click() await page.waitForSelector('input[id="common.containerName"]') diff --git a/web/crux-ui/e2e/with-login/resource-copy.spec.ts b/web/crux-ui/e2e/with-login/resource-copy.spec.ts index 42730f7585..86dd95955b 100644 --- a/web/crux-ui/e2e/with-login/resource-copy.spec.ts +++ b/web/crux-ui/e2e/with-login/resource-copy.spec.ts @@ -80,7 +80,7 @@ test.describe('Deleting default version', () => { await expect(imagesRows).toHaveCount(1) await expect(page.locator('td:has-text("nginx")').first()).toBeVisible() - const settingsButton = await page.waitForSelector(`[src="/image_config_icon.svg"]:right-of(:text("nginx"))`) + const settingsButton = await page.waitForSelector(`[src="/container_config.svg"]:right-of(:text("nginx"))`) await settingsButton.click() await page.waitForSelector(`h2:has-text("Image")`) @@ -185,7 +185,7 @@ test.describe('Deleting default version', () => { await expect(instancesRows).toHaveCount(1) await expect(page.locator('td:has-text("nginx")').first()).toBeVisible() - const settingsButton = await page.waitForSelector(`[src="/instance_config_icon.svg"]:right-of(:text("nginx"))`) + const settingsButton = await page.waitForSelector(`[src="/concrete_container_config.svg"]:right-of(:text("nginx"))`) await settingsButton.click() await page.waitForSelector(`h2:has-text("Container")`) @@ -224,7 +224,7 @@ test.describe('Deleting default version', () => { await expect(page.locator('td:has-text("nginx")').first()).toBeVisible() const newVersionDeploymentSettingsButton = await page.waitForSelector( - `[src="/instance_config_icon.svg"]:right-of(:text("nginx"))`, + `[src="/concrete_container_config.svg"]:right-of(:text("nginx"))`, ) await newVersionDeploymentSettingsButton.click() @@ -284,7 +284,7 @@ test.describe("Deleting copied deployment's parent", () => { await expect(instancesRows).toHaveCount(1) await expect(page.locator('td:has-text("nginx")').first()).toBeVisible() - const settingsButton = await page.waitForSelector(`[src="/instance_config_icon.svg"]:right-of(:text("nginx"))`) + const settingsButton = await page.waitForSelector(`[src="/concrete_container_config.svg"]:right-of(:text("nginx"))`) await settingsButton.click() await page.waitForURL(`${TEAM_ROUTES.deployment.list()}/**/instances/**`) await page.waitForSelector('h2:text-is("Container")') @@ -329,7 +329,7 @@ test.describe("Deleting copied deployment's parent", () => { await expect(page.locator('td:has-text("nginx")').first()).toBeVisible() const newDeploymentSettingsButton = await page.waitForSelector( - `[src="/instance_config_icon.svg"]:right-of(:text("nginx"))`, + `[src="/concrete_container_config.svg"]:right-of(:text("nginx"))`, ) await newDeploymentSettingsButton.click() diff --git a/web/crux-ui/i18n.json b/web/crux-ui/i18n.json index a258c3f158..93449db909 100644 --- a/web/crux-ui/i18n.json +++ b/web/crux-ui/i18n.json @@ -45,6 +45,7 @@ "/[teamSlug]/dashboard": ["dashboard"], "/[teamSlug]/storages": ["storages"], "/[teamSlug]/storages/[storageId]": ["storages"], + "/[teamSlug]/container-configurations/[configId]": ["container"], "/[teamSlug]/config-bundles": ["config-bundles"], "/[teamSlug]/config-bundles/[configBundleId]": ["config-bundles"], "/[teamSlug]/packages": ["packages"], diff --git a/web/crux-ui/locales/en/common.json b/web/crux-ui/locales/en/common.json index d609c92d21..841215e310 100644 --- a/web/crux-ui/locales/en/common.json +++ b/web/crux-ui/locales/en/common.json @@ -250,5 +250,6 @@ }, "imageConfig": "Image config", - "instanceConfig": "Container config" + "instanceConfig": "Instance config", + "deploymentConfig": "Deployment config" } diff --git a/web/crux-ui/locales/en/container.json b/web/crux-ui/locales/en/container.json index bdfa3952fc..5a5c23c8ed 100644 --- a/web/crux-ui/locales/en/container.json +++ b/web/crux-ui/locales/en/container.json @@ -1,4 +1,8 @@ { + "containerConfigName": "Container Configs - {{name}}", + "editor": "Editor", + "json": "JSON", + "sensitiveKey": "Sensitive config data should be stored as secrets", "base": { @@ -150,5 +154,10 @@ "pathOverlapsSomePortranges": "{{path}} overlaps some port ranges", "missingExternalPort": "{{path}} is missing the external port definition", "shouldStartWithSlash": "{{path}} should start with a slash" + }, + + "errors": { + "ambiguousInConfigs": "Ambigous in {{configs}}", + "ambiguousKeyInConfigs": "Ambigous {{key}} in {{configs}}" } } diff --git a/web/crux-ui/locales/en/deployments.json b/web/crux-ui/locales/en/deployments.json index 7aa3040bf5..c624ea893c 100644 --- a/web/crux-ui/locales/en/deployments.json +++ b/web/crux-ui/locales/en/deployments.json @@ -24,6 +24,5 @@ "areYouSureDeployNodePrefix": "Are you sure you want start the deployment to {{node}} with the {{prefix}} prefix?", "noInstancesSelected": "No instances selected to deploy.", "protected": "Protected", - "configBundle": "Config bundle", - "bundleNameVariableWillBeOverwritten": "Bundle {{configBundle}} variable will be overwritten." + "configBundle": "Config bundle" } diff --git a/web/crux-ui/locales/en/images.json b/web/crux-ui/locales/en/images.json index 51975459b0..5c89e765a4 100644 --- a/web/crux-ui/locales/en/images.json +++ b/web/crux-ui/locales/en/images.json @@ -6,8 +6,6 @@ "imageNameAndTag": "Image name and tag", "containerName": "Container name", "tag": "Tag", - "json": "JSON", - "editor": "Editor", "filterMinChars": "Minimum filter length is {{length}} character", "availableTags": "Available tags", "environment": "Environment", diff --git a/web/crux-ui/public/instance_config_icon.svg b/web/crux-ui/public/concrete_container_config.svg similarity index 100% rename from web/crux-ui/public/instance_config_icon.svg rename to web/crux-ui/public/concrete_container_config.svg diff --git a/web/crux-ui/public/concrete_container_config_turquoise.svg b/web/crux-ui/public/concrete_container_config_turquoise.svg new file mode 100644 index 0000000000..c079c359e3 --- /dev/null +++ b/web/crux-ui/public/concrete_container_config_turquoise.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/crux-ui/public/image_config_icon.svg b/web/crux-ui/public/container_config.svg similarity index 100% rename from web/crux-ui/public/image_config_icon.svg rename to web/crux-ui/public/container_config.svg diff --git a/web/crux-ui/public/container_config_turquoise.svg b/web/crux-ui/public/container_config_turquoise.svg new file mode 100644 index 0000000000..00a19a2ac8 --- /dev/null +++ b/web/crux-ui/public/container_config_turquoise.svg @@ -0,0 +1,4 @@ + + + + diff --git a/web/crux-ui/src/components/composer/converted-container.tsx b/web/crux-ui/src/components/composer/converted-container.tsx index cf37397e1e..8c9fd60e61 100644 --- a/web/crux-ui/src/components/composer/converted-container.tsx +++ b/web/crux-ui/src/components/composer/converted-container.tsx @@ -2,12 +2,12 @@ import { DyoCard } from '@app/elements/dyo-card' import { DyoHeading } from '@app/elements/dyo-heading' import DyoIcon from '@app/elements/dyo-icon' import DyoIndicator from '@app/elements/dyo-indicator' -import { ConvertedContainer, imageConfigToJsonContainerConfig } from '@app/models' +import { ConvertedContainer, containerConfigToJsonConfig } from '@app/models' import { writeToClipboard } from '@app/utils' import clsx from 'clsx' import useTranslation from 'next-translate/useTranslation' import { OFFLINE_EDITOR_STATE } from '../editor/use-item-editor-state' -import EditImageJson from '../projects/versions/images/edit-image-json' +import ContainerConfigJsonEditor from '../container-configs/container-config-json-editor' type ConvertedContainerCardProps = { className?: string @@ -44,13 +44,13 @@ const ConvertedContainerCard = (props: ConvertedContainerCardProps) => { {!hasRegistry && {t('missingRegistry')}}
- {}} onParseError={() => {}} - convertConfigToJson={imageConfigToJsonContainerConfig} + convertConfigToJson={containerConfigToJsonConfig} />
diff --git a/web/crux-ui/src/components/config-bundles/config-bundle-card.tsx b/web/crux-ui/src/components/config-bundles/config-bundle-card.tsx index a3abdc670e..61b567fa42 100644 --- a/web/crux-ui/src/components/config-bundles/config-bundle-card.tsx +++ b/web/crux-ui/src/components/config-bundles/config-bundle-card.tsx @@ -1,4 +1,5 @@ -import { DyoCard, DyoCardProps } from '@app/elements/dyo-card' +import DyoButton from '@app/elements/dyo-button' +import { DyoCard } from '@app/elements/dyo-card' import DyoExpandableText from '@app/elements/dyo-expandable-text' import { DyoHeading } from '@app/elements/dyo-heading' import DyoLink from '@app/elements/dyo-link' @@ -8,12 +9,14 @@ import clsx from 'clsx' import useTranslation from 'next-translate/useTranslation' import Image from 'next/image' -interface ConfigBundleCardProps extends Omit { +type ConfigBundleCardProps = { + className?: string configBundle: ConfigBundle + showConfigIcon?: boolean } const ConfigBundleCard = (props: ConfigBundleCardProps) => { - const { configBundle, className } = props + const { configBundle, className, showConfigIcon } = props const { t } = useTranslation('config-bundles') const routes = useTeamRoutes() @@ -37,6 +40,23 @@ const ConfigBundleCard = (props: ConfigBundleCardProps) => { buttonClassName="ml-auto" modalTitle={configBundle.name} /> + + {showConfigIcon && ( +
+ +
+ {t('common:config')} + {t('common:config')} +
+
+
+ )} ) } diff --git a/web/crux-ui/src/components/config-bundles/config-bundle-page-menu.tsx b/web/crux-ui/src/components/config-bundles/config-bundle-page-menu.tsx deleted file mode 100644 index 138b0e4fe7..0000000000 --- a/web/crux-ui/src/components/config-bundles/config-bundle-page-menu.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import DyoButton from '@app/elements/dyo-button' -import { DyoConfirmationModal } from '@app/elements/dyo-modal' -import useConfirmation from '@app/hooks/use-confirmation' -import useTranslation from 'next-translate/useTranslation' -import { QA_DIALOG_LABEL_DELETE_CONFIG_BUNDLE } from 'quality-assurance' - -export type DetailsPageTexts = { - edit?: string - delete?: string - addDetailsItem?: string - discard?: string - save?: string -} - -export interface ConfigBundlePageMenuProps { - editing: boolean - deleteModalTitle: string - deleteModalDescription?: string - onDelete: VoidFunction - setEditing: (editing: boolean) => void -} - -export const ConfigBundlePageMenu = (props: ConfigBundlePageMenuProps) => { - const { editing, deleteModalTitle, deleteModalDescription, setEditing, onDelete } = props - - const { t } = useTranslation('common') - - const [deleteModalConfig, confirmDelete] = useConfirmation() - - const deleteClick = async () => { - const confirmed = await confirmDelete({ - qaLabel: QA_DIALOG_LABEL_DELETE_CONFIG_BUNDLE, - title: deleteModalTitle, - description: deleteModalDescription, - confirmText: t('delete'), - confirmColor: 'bg-error-red', - }) - - if (!confirmed) { - return - } - - onDelete() - } - - return editing ? ( - setEditing(false)}> - {t('back')} - - ) : ( - <> - setEditing(true)}> - {t('edit')} - - - {!onDelete ? null : ( - - {t('delete')} - - )} - - - - ) -} diff --git a/web/crux-ui/src/components/config-bundles/add-config-bundle-card.tsx b/web/crux-ui/src/components/config-bundles/edit-config-bundle-card.tsx similarity index 58% rename from web/crux-ui/src/components/config-bundles/add-config-bundle-card.tsx rename to web/crux-ui/src/components/config-bundles/edit-config-bundle-card.tsx index d5059204f9..692459d0aa 100644 --- a/web/crux-ui/src/components/config-bundles/add-config-bundle-card.tsx +++ b/web/crux-ui/src/components/config-bundles/edit-config-bundle-card.tsx @@ -3,58 +3,71 @@ import { DyoCard } from '@app/elements/dyo-card' import DyoForm from '@app/elements/dyo-form' import { DyoHeading } from '@app/elements/dyo-heading' import { DyoInput } from '@app/elements/dyo-input' +import { DyoLabel } from '@app/elements/dyo-label' import DyoTextArea from '@app/elements/dyo-text-area' import { defaultApiErrorHandler } from '@app/errors' import useDyoFormik from '@app/hooks/use-dyo-formik' import { SubmitHook } from '@app/hooks/use-submit' import useTeamRoutes from '@app/hooks/use-team-routes' -import { ConfigBundle, CreateConfigBundle } from '@app/models' +import { ConfigBundleDetails, CreateConfigBundle, PatchConfigBundle } from '@app/models' import { sendForm } from '@app/utils' -import { configBundleCreateSchema } from '@app/validations' +import { configBundleSchema } from '@app/validations' import useTranslation from 'next-translate/useTranslation' +import { useState } from 'react' -interface AddConfigBundleCardProps { +type EditConfigBundleCardProps = { className?: string - configBundle?: ConfigBundle - onCreated: (configBundle: ConfigBundle) => void + configBundle?: ConfigBundleDetails + onConfigBundleEdited: (configBundle: ConfigBundleDetails) => void submit: SubmitHook } -const AddConfigBundleCard = (props: AddConfigBundleCardProps) => { - const { className, configBundle: propsConfigBundle, onCreated, submit } = props +const EditConfigBundleCard = (props: EditConfigBundleCardProps) => { + const { className, configBundle, onConfigBundleEdited, submit } = props const { t } = useTranslation('config-bundles') const routes = useTeamRoutes() + const [bundle, setBundle] = useState( + configBundle ?? { + id: null, + name: null, + description: null, + config: { + id: null, + type: 'config-bundle', + }, + }, + ) + + const editing = !!bundle.id + const handleApiError = defaultApiErrorHandler(t) const formik = useDyoFormik({ submit, - initialValues: { - name: propsConfigBundle?.name ?? '', - description: propsConfigBundle?.description ?? '', - }, - validationSchema: configBundleCreateSchema, + initialValues: bundle, + validationSchema: configBundleSchema, t, onSubmit: async (values, { setFieldError }) => { - const body: CreateConfigBundle = { + const body: CreateConfigBundle | PatchConfigBundle = { ...values, } - const res = await sendForm('POST', routes.configBundle.api.list(), body) + const res = await (!editing + ? sendForm('POST', routes.configBundle.api.list(), body) + : sendForm('PATCH', routes.configBundle.api.details(bundle.id), body)) if (res.ok) { - let result: ConfigBundle + let result: ConfigBundleDetails if (res.status !== 204) { - result = (await res.json()) as ConfigBundle + result = (await res.json()) as ConfigBundleDetails } else { - result = { - id: propsConfigBundle.id, - ...values, - } + result = values } - onCreated(result) + setBundle(result) + onConfigBundleEdited(result) } else { await handleApiError(res, setFieldError) } @@ -64,9 +77,11 @@ const AddConfigBundleCard = (props: AddConfigBundleCardProps) => { return ( - {t('new')} + {t(!editing ? 'new' : 'common:editName', configBundle)} + {t('tips')} +
{ ) } -export default AddConfigBundleCard +export default EditConfigBundleCard diff --git a/web/crux-ui/src/components/config-bundles/use-config-bundle-details-state.tsx b/web/crux-ui/src/components/config-bundles/use-config-bundle-details-state.tsx deleted file mode 100644 index 3834d549b2..0000000000 --- a/web/crux-ui/src/components/config-bundles/use-config-bundle-details-state.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { CONFIG_BUNDLE_EDIT_WS_REQUEST_DELAY } from '@app/const' -import useTeamRoutes from '@app/hooks/use-team-routes' -import { useThrottling } from '@app/hooks/use-throttleing' -import useWebSocket from '@app/hooks/use-websocket' -import { - ConfigBundleDetails, - ConfigBundleUpdatedMessage, - PatchConfigBundleMessage, - UniqueKeyValue, - WS_TYPE_CONFIG_BUNDLE_PATCH_RECEIVED, - WS_TYPE_CONFIG_BUNDLE_UPDATED, - WS_TYPE_PATCH_CONFIG_BUNDLE, - WebSocketSaveState, -} from '@app/models' -import { toastWarning } from '@app/utils' -import { configBundlePatchSchema, getValidationError } from '@app/validations' -import useTranslation from 'next-translate/useTranslation' -import { useRouter } from 'next/router' -import { useEffect, useState } from 'react' -import { ValidationError } from 'yup' -import EditorBadge from '../editor/editor-badge' -import useEditorState from '../editor/use-editor-state' -import useItemEditorState, { ItemEditorState } from '../editor/use-item-editor-state' - -export type ConfigBundleStateOptions = { - configBundle: ConfigBundleDetails - onWsError: (error: Error) => void - onApiError: (error: Response) => void -} - -export type ConfigBundleState = { - configBundle: ConfigBundleDetails - editing: boolean - saveState: WebSocketSaveState - editorState: ItemEditorState - fieldErrors: ValidationError[] - topBarContent: React.ReactNode -} - -export type ConfigBundleActions = { - setEditing: (editing: boolean) => void - onDelete: () => Promise - onEditEnv: (envs: UniqueKeyValue[]) => void - onEditName: (name: string) => void - onEditDescription: (description: string) => void -} - -export const useConfigBundleDetailsState = ( - options: ConfigBundleStateOptions, -): [ConfigBundleState, ConfigBundleActions] => { - const { configBundle: propsConfigBundle, onWsError, onApiError } = options - - const { t } = useTranslation('config-bundles') - const router = useRouter() - const routes = useTeamRoutes() - - const throttle = useThrottling(CONFIG_BUNDLE_EDIT_WS_REQUEST_DELAY) - - const [configBundle, setConfigBundle] = useState(propsConfigBundle) - const [editing, setEditing] = useState(false) - const [saveState, setSaveState] = useState(null) - const [fieldErrors, setFieldErrors] = useState([]) - const [topBarContent, setTopBarContent] = useState(null) - - const sock = useWebSocket(routes.configBundle.detailsSocket(configBundle.id), { - onOpen: () => setSaveState('connected'), - onClose: () => setSaveState('disconnected'), - onSend: message => { - if (message.type === WS_TYPE_PATCH_CONFIG_BUNDLE) { - setSaveState('saving') - } - }, - onReceive: message => { - if (message.type === WS_TYPE_CONFIG_BUNDLE_PATCH_RECEIVED) { - setSaveState('saved') - } - }, - onError: onWsError, - }) - - const editor = useEditorState(sock) - const editorState = useItemEditorState(editor, sock, configBundle.id) - - sock.on(WS_TYPE_CONFIG_BUNDLE_UPDATED, (message: ConfigBundleUpdatedMessage) => { - setConfigBundle(it => ({ - ...it, - ...message, - })) - }) - - const onDelete = async (): Promise => { - const res = await fetch(routes.configBundle.api.details(configBundle.id), { - method: 'DELETE', - }) - - if (res.ok) { - await router.replace(routes.configBundle.list()) - } else if (res.status === 412) { - toastWarning(t('inUse')) - } else { - onApiError(res) - } - } - - const onPatch = (patch: Partial) => { - const newBundle = { - ...configBundle, - ...patch, - } - setConfigBundle(newBundle) - - const validationErrors = getValidationError(configBundlePatchSchema, newBundle, { abortEarly: false })?.inner ?? [] - setFieldErrors(validationErrors) - - if (validationErrors.length < 1) { - throttle(() => { - sock.send(WS_TYPE_PATCH_CONFIG_BUNDLE, { - name: newBundle.name, - description: newBundle.description, - environment: newBundle.environment, - } as PatchConfigBundleMessage) - }) - } - } - - const onEditEnv = (envs: UniqueKeyValue[]) => onPatch({ environment: envs }) - - const onEditName = (name: string) => onPatch({ name }) - - const onEditDescription = (description: string) => onPatch({ description }) - - useEffect(() => { - const reactNode = ( - <> - {editorState.editors.map((it, index) => ( - - ))} - - ) - - setTopBarContent(reactNode) - }, [editorState.editors]) - - return [ - { - configBundle, - editing, - saveState, - editorState, - fieldErrors, - topBarContent, - }, - { - setEditing, - onDelete, - onEditEnv, - onEditName, - onEditDescription, - }, - ] -} diff --git a/web/crux-ui/src/components/projects/versions/images/config/common-config-section.tsx b/web/crux-ui/src/components/container-configs/common-config-section.tsx similarity index 70% rename from web/crux-ui/src/components/projects/versions/images/config/common-config-section.tsx rename to web/crux-ui/src/components/container-configs/common-config-section.tsx index 5ce6ad7166..3d3e0ea6e3 100644 --- a/web/crux-ui/src/components/projects/versions/images/config/common-config-section.tsx +++ b/web/crux-ui/src/components/container-configs/common-config-section.tsx @@ -11,25 +11,30 @@ import DyoMessage from '@app/elements/dyo-message' import DyoToggle from '@app/elements/dyo-toggle' import useTeamRoutes from '@app/hooks/use-team-routes' import { - COMMON_CONFIG_PROPERTIES, + COMMON_CONFIG_KEYS, CONTAINER_EXPOSE_STRATEGY_VALUES, CONTAINER_VOLUME_TYPE_VALUES, - CommonConfigDetails, - CommonConfigProperty, + CommonConfigKey, + ConcreteCommonConfigData, + ConcreteContainerConfigData, + ConcreteCraneConfigData, ContainerConfigData, + ContainerConfigErrors, ContainerConfigExposeStrategy, - ContainerConfigPort, - ContainerConfigVolume, - CraneConfigDetails, - ImageConfigProperty, + ContainerConfigKey, + ContainerConfigSectionType, InitContainerVolumeLink, - InstanceCommonConfigDetails, - InstanceCraneConfigDetails, + Port, StorageOption, + UniqueSecretKeyValue, + Volume, VolumeType, + booleanResettable, filterContains, filterEmpty, - mergeConfigs, + numberResettable, + portRangeToString, + stringResettable, } from '@app/models' import { fetcher, toNumber } from '@app/utils' import { @@ -47,34 +52,21 @@ import { v4 as uuid } from 'uuid' import ConfigSectionLabel from './config-section-label' import ExtendableItemList from './extendable-item-list' -type CommonConfigSectionBaseProps = { +type CommonConfigSectionProps = { disabled?: boolean - selectedFilters: ImageConfigProperty[] + selectedFilters: ContainerConfigKey[] editorOptions: ItemEditorState - config: T - resetableConfig?: T - onChange: (config: Partial) => void - onResetSection: (section: CommonConfigProperty) => void + resettableConfig: ContainerConfigData | ConcreteContainerConfigData + config: ContainerConfigData | ConcreteContainerConfigData + onChange: (config: ContainerConfigData | ConcreteContainerConfigData) => void + onResetSection: (section: CommonConfigKey) => void fieldErrors: ContainerConfigValidationErrors + conflictErrors: ContainerConfigErrors definedSecrets?: string[] publicKey?: string + baseConfig: ContainerConfigData | null } -type ImageCommonConfigSectionProps = CommonConfigSectionBaseProps< - CommonConfigDetails & Pick -> & { - configType: 'image' -} - -type InstanceCommonConfigSectionProps = CommonConfigSectionBaseProps< - InstanceCommonConfigDetails & Pick -> & { - configType: 'instance' - imageConfig: ContainerConfigData -} - -type CommonConfigSectionProps = ImageCommonConfigSectionProps | InstanceCommonConfigSectionProps - const CommonConfigSection = (props: CommonConfigSectionProps) => { const { t } = useTranslation('container') const routes = useTeamRoutes() @@ -86,24 +78,21 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { selectedFilters, editorOptions, fieldErrors, - configType, + conflictErrors, definedSecrets, publicKey, - config: propsConfig, - resetableConfig: propsResetableConfig, + resettableConfig, + config, + baseConfig, } = props const { data: storages } = useSWR(routes.storage.api.options(), fetcher) - const disabledOnImage = configType === 'image' || disabled - // eslint-disable-next-line react/destructuring-assignment - const imageConfig = configType === 'instance' ? props.imageConfig : null - const resetableConfig = propsResetableConfig ?? propsConfig - const config = configType === 'instance' ? mergeConfigs(imageConfig, propsConfig) : propsConfig + const sectionType: ContainerConfigSectionType = baseConfig ? 'concrete' : 'base' const exposedPorts = config.ports?.filter(it => !!it.internal) ?? [] - const onVolumesChanged = (it: ContainerConfigVolume[]) => + const onVolumesChanged = (it: Volume[]) => onChange({ volumes: it, storage: @@ -115,8 +104,8 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { : undefined, }) - const onPortsChanged = (ports: ContainerConfigPort[]) => { - let patch: Partial> = { + const onPortsChanged = (ports: Port[]) => { + let patch: Partial> = { ports, } @@ -147,7 +136,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { const environmentWarning = getValidationError(unsafeUniqueKeyValuesSchema, config.environment, undefined, t)?.message ?? null - return !filterEmpty([...COMMON_CONFIG_PROPERTIES], selectedFilters) ? null : ( + return !filterEmpty([...COMMON_CONFIG_KEYS], selectedFilters) ? null : (
{t('base.common').toUpperCase()} @@ -160,7 +149,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => {
onResetSection('name')} > {t('common.containerName').toUpperCase()} @@ -175,7 +164,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { placeholder={t('common.containerName')} onPatch={it => onChange({ name: it })} editorOptions={editorOptions} - message={findErrorFor(fieldErrors, 'name')} + message={findErrorFor(fieldErrors, 'name') ?? conflictErrors?.name} disabled={disabled} />
@@ -188,11 +177,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => {
onResetSection('user')} > {t('common.user').toUpperCase()} @@ -203,14 +188,14 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { containerClassName="max-w-lg mb-3" labelClassName="text-bright font-semibold tracking-wide mb-2 my-auto mr-4" grow - value={config.user === -1 ? '' : config.user} + value={config.user !== -1 ? config.user : ''} placeholder={t('common.placeholders.containerDefault')} onPatch={it => { const val = toNumber(it) - onChange({ user: configType === 'instance' || val === 0 ? val : val ?? -1 }) + onChange({ user: typeof val !== 'number' ? -1 : val }) }} editorOptions={editorOptions} - message={findErrorFor(fieldErrors, 'user')} + message={findErrorFor(fieldErrors, 'user') ?? conflictErrors?.user} disabled={disabled} />
@@ -222,7 +207,9 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => {
onResetSection('workingDirectory')} > {t('common.workingDirectory').toUpperCase()} @@ -237,7 +224,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { placeholder={t('common.placeholders.containerDefault')} onPatch={it => onChange({ workingDirectory: it })} editorOptions={editorOptions} - message={findErrorFor(fieldErrors, 'workingDirectory')} + message={findErrorFor(fieldErrors, 'workingDirectory') ?? conflictErrors?.workingDirectory} disabled={disabled} />
@@ -248,8 +235,9 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { {filterContains('expose', selectedFilters) && (
onResetSection('expose')} + error={conflictErrors?.expose} > {t('common.exposeStrategy').toUpperCase()} @@ -271,8 +259,9 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { {filterContains('tty', selectedFilters) && (
onResetSection('tty')} + error={conflictErrors?.tty} > {t('common.tty').toUpperCase()} @@ -291,8 +280,9 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { {filterContains('configContainer', selectedFilters) && (
onResetSection('configContainer')} + error={conflictErrors?.configContainer} > {t('common.configContainer').toUpperCase()} @@ -357,8 +347,9 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { {filterContains('routing', selectedFilters) && (
onResetSection('routing')} + error={conflictErrors?.routing} > {t('common.routing').toUpperCase()} @@ -458,13 +449,14 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { labelClassName="text-bright font-semibold tracking-wide mb-2" label={t('common.environment').toUpperCase()} onChange={it => onChange({ environment: it })} - onResetSection={resetableConfig.environment ? () => onResetSection('environment') : null} + onResetSection={resettableConfig.environment ? () => onResetSection('environment') : null} items={config.environment} editorOptions={editorOptions} disabled={disabled} findErrorMessage={index => findErrorStartsWith(fieldErrors, `environment[${index}]`)} message={findErrorFor(fieldErrors, `environment`) ?? environmentWarning} messageType={!findErrorFor(fieldErrors, `environment`) && environmentWarning ? 'info' : 'error'} + errors={conflictErrors?.environment} />
)} @@ -472,14 +464,14 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { {/* secrets */} {filterContains('secrets', selectedFilters) && (
- {configType === 'instance' ? ( + {sectionType === 'concrete' ? ( onChange({ secrets: it })} - items={propsConfig.secrets} + items={config.secrets as UniqueSecretKeyValue[]} editorOptions={editorOptions} definedSecrets={definedSecrets} publicKey={publicKey} @@ -492,7 +484,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { labelClassName="text-bright font-semibold tracking-wide mb-2" label={t('common.secrets').toUpperCase()} onChange={it => onChange({ secrets: it.map(sit => ({ ...sit })) })} - onResetSection={resetableConfig.secrets ? () => onResetSection('secrets') : null} + onResetSection={resettableConfig.secrets ? () => onResetSection('secrets') : null} items={config.secrets} editorOptions={editorOptions} disabled={disabled} @@ -511,7 +503,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { label={t('common.commands').toUpperCase()} labelClassName="text-bright font-semibold tracking-wide mb-2" onChange={it => onChange({ commands: it })} - onResetSection={resetableConfig.commands ? () => onResetSection('commands') : null} + onResetSection={resettableConfig.commands ? () => onResetSection('commands') : null} items={config.commands} editorOptions={editorOptions} disabled={disabled} @@ -529,7 +521,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { label={t('common.arguments').toUpperCase()} labelClassName="text-bright font-semibold tracking-wide mb-2" onChange={it => onChange({ args: it })} - onResetSection={resetableConfig.args ? () => onResetSection('args') : null} + onResetSection={resettableConfig.args ? () => onResetSection('args') : null} items={config.args} editorOptions={editorOptions} disabled={disabled} @@ -543,8 +535,9 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { {filterContains('storage', selectedFilters) && (
onResetSection('storage')} + error={conflictErrors?.storage} > {t('common.storage').toUpperCase()} @@ -580,7 +573,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { placeholder={t('common.bucketPath')} onPatch={it => onChange({ storage: { ...config.storage, bucket: it } })} editorOptions={editorOptions} - disabled={disabled || !config.storage?.storageId} + disabled={disabled || !resettableConfig.storage?.storageId} message={findErrorFor(fieldErrors, 'storage.bucket')} /> @@ -600,7 +593,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { storage: { ...config.storage, path: it }, }) } - disabled={disabled || !config.storage?.storageId} + disabled={disabled || !resettableConfig.storage?.storageId} />
{ items={config.ports} label={t('common.ports')} onPatch={it => onPortsChanged(it)} - onResetSection={resetableConfig.ports ? () => onResetSection('ports') : null} + onResetSection={resettableConfig.ports ? () => onResetSection('ports') : null} findErrorMessage={index => findErrorStartsWith(fieldErrors, `ports[${index}]`)} emptyItemFactory={() => ({ external: null, internal: null, })} renderItem={(item, error, removeButton, onPatch) => ( -
-
- { - const value = Number.parseInt(it, 10) - const external = Number.isNaN(value) ? null : value - - onPatch({ - external, - }) - }} - editorOptions={editorOptions} - disabled={disabled} - /> -
+ <> +
+
+ { + const value = Number.parseInt(it, 10) + const external = Number.isNaN(value) ? null : value + + onPatch({ + external, + }) + }} + editorOptions={editorOptions} + disabled={disabled} + /> +
-
- - onPatch({ - internal: toNumber(it), - }) - } - editorOptions={editorOptions} - disabled={disabled} - /> +
+ + onPatch({ + internal: toNumber(it), + }) + } + editorOptions={editorOptions} + disabled={disabled} + /> +
+ + {removeButton()}
- {removeButton()} -
+ {(conflictErrors?.ports ?? {})[item.internal] && ( + + )} + )} /> )} @@ -688,72 +687,81 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { })} findErrorMessage={index => findErrorStartsWith(fieldErrors, `portRanges[${index}]`)} onPatch={it => onChange({ portRanges: it })} - onResetSection={resetableConfig.portRanges ? () => onResetSection('portRanges') : null} + onResetSection={resettableConfig.portRanges ? () => onResetSection('portRanges') : null} renderItem={(item, error, removeButton, onPatch) => ( -
- {t('common.internal').toUpperCase()} - -
- onPatch({ ...item, internal: { ...item.internal, from: toNumber(it) } })} - editorOptions={editorOptions} - disabled={disabled} - invalid={matchError(error, 'internal.from')} - /> + <> +
+ {t('common.internal').toUpperCase()} + +
+ onPatch({ ...item, internal: { ...item.internal, from: toNumber(it) } })} + editorOptions={editorOptions} + disabled={disabled} + invalid={matchError(error, 'internal.from')} + /> - onPatch({ ...item, internal: { ...item.internal, to: toNumber(it) } })} - editorOptions={editorOptions} - disabled={disabled} - invalid={matchError(error, 'internal.to')} - /> -
+ onPatch({ ...item, internal: { ...item.internal, to: toNumber(it) } })} + editorOptions={editorOptions} + disabled={disabled} + invalid={matchError(error, 'internal.to')} + /> +
- {/* external part */} - {t('common.external').toUpperCase()} - -
- onPatch({ ...item, external: { ...item.external, from: toNumber(it) } })} - editorOptions={editorOptions} - disabled={disabled} - invalid={matchError(error, 'external.from')} - /> + {/* external part */} + {t('common.external').toUpperCase()} + +
+ onPatch({ ...item, external: { ...item.external, from: toNumber(it) } })} + editorOptions={editorOptions} + disabled={disabled} + invalid={matchError(error, 'external.from')} + /> - onPatch({ ...item, external: { ...item.external, to: toNumber(it) } })} - editorOptions={editorOptions} - disabled={disabled} - invalid={matchError(error, 'external.from')} - /> + onPatch({ ...item, external: { ...item.external, to: toNumber(it) } })} + editorOptions={editorOptions} + disabled={disabled} + invalid={matchError(error, 'external.from')} + /> - {removeButton()} + {removeButton()} +
-
+ + {(conflictErrors?.portRanges ?? {})[portRangeToString(item.internal)] && ( + + )} + )} /> )} @@ -772,89 +780,95 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { })} findErrorMessage={index => findErrorStartsWith(fieldErrors, `volumes[${index}]`)} onPatch={it => onVolumesChanged(it)} - onResetSection={resetableConfig.volumes ? () => onResetSection('volumes') : null} + onResetSection={resettableConfig.volumes ? () => onResetSection('volumes') : null} renderItem={(item, error, removeButton, onPatch) => ( -
-
- onPatch({ name: it })} - editorOptions={editorOptions} - disabled={disabled} - invalid={matchError(error, 'name')} - /> + <> +
+
+ onPatch({ name: it })} + editorOptions={editorOptions} + disabled={disabled} + invalid={matchError(error, 'name')} + /> - onPatch({ size: it })} - editorOptions={editorOptions} - disabled={disabled} - invalid={matchError(error, 'size')} - /> -
+ onPatch({ size: it })} + editorOptions={editorOptions} + disabled={disabled} + invalid={matchError(error, 'size')} + /> +
-
- onPatch({ path: it })} - editorOptions={editorOptions} - disabled={disabled} - invalid={matchError(error, 'path')} - /> +
+ onPatch({ path: it })} + editorOptions={editorOptions} + disabled={disabled} + invalid={matchError(error, 'path')} + /> - onPatch({ class: it })} - editorOptions={editorOptions} - disabled={disabled} - invalid={matchError(error, 'class')} - /> -
+ onPatch({ class: it })} + editorOptions={editorOptions} + disabled={disabled} + invalid={matchError(error, 'class')} + /> +
-
-
- {t('common.type')} - -
- t(`common.volumeTypes.${it}`)} - onSelectionChange={it => onPatch({ type: it })} - disabled={disabled} - qaLabel={chipsQALabelFromValue} - /> - - {removeButton('self-center ml-auto')} +
+
+ {t('common.type')} + +
+ t(`common.volumeTypes.${it}`)} + onSelectionChange={it => onPatch({ type: it })} + disabled={disabled} + qaLabel={chipsQALabelFromValue} + /> + + {removeButton('self-center ml-auto')} +
-
+ + {(conflictErrors?.volumes ?? {})[item.path] && ( + + )} + )} /> )} @@ -865,6 +879,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { disabled={disabled} items={config.initContainers} label={t('common.initContainers')} + error={conflictErrors?.initContainers} emptyItemFactory={() => ({ id: uuid(), name: null, @@ -877,7 +892,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { })} findErrorMessage={index => findErrorStartsWith(fieldErrors, `initContainers[${index}]`)} onPatch={it => onChange({ initContainers: it })} - onResetSection={resetableConfig.initContainers ? () => onResetSection('initContainers') : null} + onResetSection={resettableConfig.initContainers ? () => onResetSection('initContainers') : null} renderItem={(item, error, removeButton, onPatch) => (
{ + const { disabled, className, labelClassName, children, onResetSection, error } = props + + return ( +
+
+ {children} + + {!disabled && ( +
+ +
+ )} +
+ + {error && } +
+ ) +} + +export default ConfigSectionLabel diff --git a/web/crux-ui/src/components/projects/versions/images/config/config-to-filters.ts b/web/crux-ui/src/components/container-configs/config-to-filters.ts similarity index 71% rename from web/crux-ui/src/components/projects/versions/images/config/config-to-filters.ts rename to web/crux-ui/src/components/container-configs/config-to-filters.ts index 5dbbba9852..6734ce529d 100644 --- a/web/crux-ui/src/components/projects/versions/images/config/config-to-filters.ts +++ b/web/crux-ui/src/components/container-configs/config-to-filters.ts @@ -1,12 +1,12 @@ -import { ALL_CONFIG_PROPERTIES, ContainerConfigData, ImageConfigProperty } from '@app/models' +import { CONTAINER_CONFIG_KEYS, ContainerConfigData, ContainerConfigKey } from '@app/models' import { ContainerConfigValidationErrors, findErrorStartsWith } from '@app/validations' const configToFilters = ( - current: ImageConfigProperty[], + current: ContainerConfigKey[], configData: T, fieldErrors?: ContainerConfigValidationErrors, -): ImageConfigProperty[] => { - const newFilters = ALL_CONFIG_PROPERTIES.filter(it => { +): ContainerConfigKey[] => { + const newFilters = CONTAINER_CONFIG_KEYS.filter(it => { const value = configData[it] if (fieldErrors && findErrorStartsWith(fieldErrors, it)) { @@ -21,10 +21,6 @@ const configToFilters = ( return false } - if (Array.isArray(value) && value.length < 1) { - return false - } - if (typeof value === 'object') { return Object.keys(value).length > 0 } diff --git a/web/crux-ui/src/components/projects/versions/images/image-config-filters.tsx b/web/crux-ui/src/components/container-configs/container-config-filters.tsx similarity index 71% rename from web/crux-ui/src/components/projects/versions/images/image-config-filters.tsx rename to web/crux-ui/src/components/container-configs/container-config-filters.tsx index 4ebea6c70f..9bc52e6093 100644 --- a/web/crux-ui/src/components/projects/versions/images/image-config-filters.tsx +++ b/web/crux-ui/src/components/container-configs/container-config-filters.tsx @@ -1,39 +1,39 @@ import { DyoLabel } from '@app/elements/dyo-label' import { - ALL_CONFIG_PROPERTIES, - BaseImageConfigFilterType, - COMMON_CONFIG_PROPERTIES, - CRANE_CONFIG_PROPERTIES, - DAGENT_CONFIG_PROPERTIES, - ImageConfigProperty, + ContainerConfigFilterType, + COMMON_CONFIG_KEYS, + CONTAINER_CONFIG_KEYS, + ContainerConfigKey, + CRANE_CONFIG_KEYS, + DAGENT_CONFIG_KEYS, } from '@app/models' import clsx from 'clsx' import useTranslation from 'next-translate/useTranslation' -type FilterSet = Record +type FilterSet = Record export const defaultFilterSet: FilterSet = { - all: [...ALL_CONFIG_PROPERTIES], - common: [...COMMON_CONFIG_PROPERTIES], - crane: [...CRANE_CONFIG_PROPERTIES].filter(it => it !== 'extraLBAnnotations'), - dagent: [...DAGENT_CONFIG_PROPERTIES], + all: [...CONTAINER_CONFIG_KEYS], + common: [...COMMON_CONFIG_KEYS], + crane: [...CRANE_CONFIG_KEYS].filter(it => it !== 'extraLBAnnotations'), + dagent: [...DAGENT_CONFIG_KEYS], } export const k8sFilterSet: FilterSet = { - all: [...COMMON_CONFIG_PROPERTIES, ...CRANE_CONFIG_PROPERTIES], - common: [...COMMON_CONFIG_PROPERTIES], - crane: [...CRANE_CONFIG_PROPERTIES].filter(it => it !== 'extraLBAnnotations'), + all: [...COMMON_CONFIG_KEYS, ...CRANE_CONFIG_KEYS], + common: [...COMMON_CONFIG_KEYS], + crane: [...CRANE_CONFIG_KEYS].filter(it => it !== 'extraLBAnnotations'), dagent: null, } export const dockerFilterSet: FilterSet = { - all: [...COMMON_CONFIG_PROPERTIES, ...DAGENT_CONFIG_PROPERTIES], - common: [...COMMON_CONFIG_PROPERTIES], + all: [...COMMON_CONFIG_KEYS, ...DAGENT_CONFIG_KEYS], + common: [...COMMON_CONFIG_KEYS], crane: null, - dagent: [...DAGENT_CONFIG_PROPERTIES], + dagent: [...DAGENT_CONFIG_KEYS], } -const getBorderColor = (type: BaseImageConfigFilterType): string => { +const getBorderColor = (type: ContainerConfigFilterType): string => { switch (type) { case 'common': return 'border-dyo-orange/50' @@ -46,7 +46,7 @@ const getBorderColor = (type: BaseImageConfigFilterType): string => { } } -const getBgColor = (type: BaseImageConfigFilterType): string => { +const getBgColor = (type: ContainerConfigFilterType): string => { switch (type) { case 'common': return 'bg-dyo-orange/50' @@ -59,22 +59,22 @@ const getBgColor = (type: BaseImageConfigFilterType): string => { } } -type ImageConfigFilterProps = { - onChange: (filters: ImageConfigProperty[]) => void - filters: ImageConfigProperty[] +type ContainerConfigFilterProps = { + onChange: (filters: ContainerConfigKey[]) => void + filters: ContainerConfigKey[] filterSet?: FilterSet } -const ImageConfigFilters = (props: ImageConfigFilterProps) => { +const ContainerConfigFilters = (props: ContainerConfigFilterProps) => { const { onChange, filters, filterSet = defaultFilterSet } = props const filterSetKeys = Object.entries(filterSet) .filter(([_, value]) => !!value) - .map(([key]) => key) as BaseImageConfigFilterType[] + .map(([key]) => key) as ContainerConfigFilterType[] const { t } = useTranslation('container') - const onBaseFilterChanged = (value: BaseImageConfigFilterType) => { + const onBaseFilterChanged = (value: ContainerConfigFilterType) => { const baseFilters = filterSet[value] const select = filters.filter(it => baseFilters.includes(it)).length === baseFilters.length @@ -85,7 +85,7 @@ const ImageConfigFilters = (props: ImageConfigFilterProps) => { } } - const onFilterChanged = (value: ImageConfigProperty) => { + const onFilterChanged = (value: ContainerConfigKey) => { const newFilters = filters.indexOf(value) !== -1 ? filters.filter(it => it !== value) : [...filters, value] onChange(newFilters) } @@ -98,7 +98,7 @@ const ImageConfigFilters = (props: ImageConfigFilterProps) => { {filterSetKeys.map((base, index) => { const selected = base === 'all' - ? filters.length === ALL_CONFIG_PROPERTIES.length + ? filters.length === CONTAINER_CONFIG_KEYS.length : filters.filter(it => filterSet[base].includes(it)).length === filterSet[base].length return ( @@ -146,4 +146,4 @@ const ImageConfigFilters = (props: ImageConfigFilterProps) => { ) } -export default ImageConfigFilters +export default ContainerConfigFilters diff --git a/web/crux-ui/src/components/projects/versions/images/edit-image-json.tsx b/web/crux-ui/src/components/container-configs/container-config-json-editor.tsx similarity index 83% rename from web/crux-ui/src/components/projects/versions/images/edit-image-json.tsx rename to web/crux-ui/src/components/container-configs/container-config-json-editor.tsx index 7480af8247..8f29ac24df 100644 --- a/web/crux-ui/src/components/projects/versions/images/edit-image-json.tsx +++ b/web/crux-ui/src/components/container-configs/container-config-json-editor.tsx @@ -1,13 +1,11 @@ import { ItemEditorState } from '@app/components/editor/use-item-editor-state' import useMultiInputState from '@app/components/editor/use-multi-input-state' import JsonEditor from '@app/components/shared/json-editor-dynamic-module' -import { IMAGE_WS_REQUEST_DELAY } from '@app/const' -import { CANCEL_THROTTLE, useThrottling } from '@app/hooks/use-throttleing' import { editIdOf } from '@app/models' import clsx from 'clsx' import { CSSProperties, useCallback, useState } from 'react' -interface EditImageJsonProps { +type EditImageJsonProps = { disabled?: boolean className?: string config: Config @@ -19,7 +17,7 @@ interface EditImageJsonProps { const JSON_EDITOR_COMPARATOR = (one: Json, other: Json): boolean => JSON.stringify(one) === JSON.stringify(other) -const EditImageJson = (props: EditImageJsonProps) => { +const ContainerConfigJsonEditor = (props: EditImageJsonProps) => { const { disabled, editorOptions, @@ -30,8 +28,6 @@ const EditImageJson = (props: EditImageJsonProps) => convertConfigToJson, } = props - const throttle = useThrottling(IMAGE_WS_REQUEST_DELAY) - const [jsonError, setJsonError] = useState(false) const onMergeValues = (remote: Json, local: Json): Json => { @@ -64,12 +60,9 @@ const EditImageJson = (props: EditImageJsonProps) => const onParseError = useCallback( (err: Error) => { setJsonError(true) - - throttle(CANCEL_THROTTLE) - propOnParseError(err) }, - [throttle, propOnParseError], + [propOnParseError], ) const { highlightColor } = editorState @@ -96,4 +89,4 @@ const EditImageJson = (props: EditImageJsonProps) => ) } -export default EditImageJson +export default ContainerConfigJsonEditor diff --git a/web/crux-ui/src/components/projects/versions/images/config/crane-config-section.tsx b/web/crux-ui/src/components/container-configs/crane-config-section.tsx similarity index 90% rename from web/crux-ui/src/components/projects/versions/images/config/crane-config-section.tsx rename to web/crux-ui/src/components/container-configs/crane-config-section.tsx index 5c32427df6..47972190ff 100644 --- a/web/crux-ui/src/components/projects/versions/images/config/crane-config-section.tsx +++ b/web/crux-ui/src/components/container-configs/crane-config-section.tsx @@ -5,76 +5,57 @@ import KeyValueInput from '@app/components/shared/key-value-input' import DyoChips, { chipsQALabelFromValue } from '@app/elements/dyo-chips' import { DyoHeading } from '@app/elements/dyo-heading' import { DyoLabel } from '@app/elements/dyo-label' +import DyoMessage from '@app/elements/dyo-message' import DyoToggle from '@app/elements/dyo-toggle' import { - CRANE_CONFIG_FILTER_VALUES, - CraneConfigProperty, - ImageConfigProperty, - filterContains, - filterEmpty, CONTAINER_DEPLOYMENT_STRATEGY_VALUES, - CommonConfigDetails, + CRANE_CONFIG_FILTER_VALUES, + ConcreteContainerConfigData, ContainerConfigData, + ContainerConfigErrors, + ContainerConfigKey, ContainerDeploymentStrategyType, - CraneConfigDetails, - InstanceContainerConfigData, - InstanceCraneConfigDetails, - mergeConfigs, + CraneConfigKey, + booleanResettable, + filterContains, + filterEmpty, + stringResettable, } from '@app/models' import { nullify, toNumber } from '@app/utils' +import { ContainerConfigValidationErrors, findErrorFor } from '@app/validations' import useTranslation from 'next-translate/useTranslation' -import ConfigSectionLabel from './config-section-label' -import DyoMessage from '@app/elements/dyo-message' import { useEffect } from 'react' -import { ContainerConfigValidationErrors, findErrorFor } from '@app/validations' +import ConfigSectionLabel from './config-section-label' -type CraneConfigSectionBaseProps = { - config: T - resetableConfig?: T - onChange: (config: Partial) => void - onResetSection: (section: CraneConfigProperty) => void - selectedFilters: ImageConfigProperty[] +type CraneConfigSectionProps = { + config: ContainerConfigData | ConcreteContainerConfigData + onChange: (config: ContainerConfigData | ConcreteContainerConfigData) => void + onResetSection: (section: CraneConfigKey) => void + selectedFilters: ContainerConfigKey[] editorOptions: ItemEditorState disabled?: boolean fieldErrors: ContainerConfigValidationErrors + conflictErrors: ContainerConfigErrors + baseConfig: ContainerConfigData | null + resettableConfig: ContainerConfigData | ConcreteContainerConfigData } -type ImageCraneConfigSectionProps = CraneConfigSectionBaseProps< - CraneConfigDetails & Pick -> & { - configType: 'image' -} - -type InstanceCraneConfigSectionProps = CraneConfigSectionBaseProps< - InstanceCraneConfigDetails & Pick -> & { - configType: 'instance' - imageConfig: ContainerConfigData -} - -export type CraneConfigSectionProps = ImageCraneConfigSectionProps | InstanceCraneConfigSectionProps - const CraneConfigSection = (props: CraneConfigSectionProps) => { const { - config: propsConfig, - resetableConfig: propsResetableConfig, - configType, + config, + resettableConfig, + baseConfig, selectedFilters, onChange, onResetSection, editorOptions, disabled, fieldErrors, + conflictErrors, } = props const { t } = useTranslation('container') - const disabledOnImage = configType === 'image' || disabled - // eslint-disable-next-line react/destructuring-assignment - const imageConfig = configType === 'instance' ? props.imageConfig : null - const resetableConfig = propsResetableConfig ?? propsConfig - const config = configType === 'instance' ? mergeConfigs(imageConfig, propsConfig) : propsConfig - const ports = config.ports?.filter(it => !!it.internal) ?? [] useEffect(() => { @@ -99,8 +80,9 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { {filterContains('deploymentStrategy', selectedFilters) && (
onResetSection('deploymentStrategy')} + error={conflictErrors?.deploymentStrategy} > {t('crane.deploymentStrategy').toUpperCase()} @@ -122,8 +104,9 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { {filterContains('healthCheckConfig', selectedFilters) && (
onResetSection('healthCheckConfig')} + error={conflictErrors?.healthCheckConfig} > {t('crane.healthCheckConfig').toUpperCase()} @@ -223,7 +206,7 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { onChange={it => onChange({ customHeaders: it })} editorOptions={editorOptions} disabled={disabled} - onResetSection={resetableConfig.customHeaders ? () => onResetSection('customHeaders') : null} + onResetSection={resettableConfig.customHeaders ? () => onResetSection('customHeaders') : null} />
@@ -233,8 +216,9 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { {filterContains('resourceConfig', selectedFilters) && (
onResetSection('resourceConfig')} + error={conflictErrors?.resourceConfig} > {t('crane.resourceConfig').toUpperCase()} @@ -343,8 +327,9 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { {filterContains('proxyHeaders', selectedFilters) && (
onResetSection('proxyHeaders')} + error={conflictErrors?.proxyHeaders} > {t('crane.proxyHeaders').toUpperCase()} @@ -364,8 +349,9 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => {
onResetSection('useLoadBalancer')} + error={conflictErrors?.useLoadBalancer} > {t('crane.useLoadBalancer').toUpperCase()} @@ -387,7 +373,7 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { items={config.extraLBAnnotations ?? []} editorOptions={editorOptions} onChange={it => onChange({ extraLBAnnotations: it })} - onResetSection={resetableConfig.extraLBAnnotations ? () => onResetSection('extraLBAnnotations') : null} + onResetSection={resettableConfig.extraLBAnnotations ? () => onResetSection('extraLBAnnotations') : null} disabled={disabled} /> )} @@ -397,10 +383,7 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { {/* Labels */} {filterContains('labels', selectedFilters) && (
- onResetSection('labels')} - > + onResetSection('labels')}> {t('crane.labels').toUpperCase()} @@ -413,6 +396,7 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { items={config.labels?.deployment ?? []} editorOptions={editorOptions} disabled={disabled} + errors={conflictErrors?.labels?.deployment} />
@@ -426,6 +410,7 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { items={config.labels?.service ?? []} editorOptions={editorOptions} disabled={disabled} + errors={conflictErrors?.labels?.service} />
@@ -439,6 +424,7 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { items={config.labels?.ingress ?? []} editorOptions={editorOptions} disabled={disabled} + errors={conflictErrors?.labels?.ingress} />
@@ -449,7 +435,7 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { {filterContains('annotations', selectedFilters) && (
onResetSection('annotations')} > {t('crane.annotations').toUpperCase()} @@ -464,6 +450,7 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { items={config.annotations?.deployment ?? []} editorOptions={editorOptions} disabled={disabled} + errors={conflictErrors?.annotations?.deployment} />
@@ -477,6 +464,7 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { items={config.annotations?.service ?? []} editorOptions={editorOptions} disabled={disabled} + errors={conflictErrors?.annotations.service} />
@@ -490,6 +478,7 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { items={config.annotations?.ingress ?? []} editorOptions={editorOptions} disabled={disabled} + errors={conflictErrors?.annotations.ingress} />
@@ -500,8 +489,9 @@ const CraneConfigSection = (props: CraneConfigSectionProps) => { {filterContains('metrics', selectedFilters) && (
onResetSection('metrics')} + error={conflictErrors?.metrics} > {t('crane.metrics').toUpperCase()} diff --git a/web/crux-ui/src/components/projects/versions/images/config/dagent-config-section.tsx b/web/crux-ui/src/components/container-configs/dagent-config-section.tsx similarity index 83% rename from web/crux-ui/src/components/projects/versions/images/config/dagent-config-section.tsx rename to web/crux-ui/src/components/container-configs/dagent-config-section.tsx index d7d717ab51..971c19ea32 100644 --- a/web/crux-ui/src/components/projects/versions/images/config/dagent-config-section.tsx +++ b/web/crux-ui/src/components/container-configs/dagent-config-section.tsx @@ -1,79 +1,65 @@ +import MultiInput from '@app/components/editor/multi-input' import { ItemEditorState } from '@app/components/editor/use-item-editor-state' import KeyOnlyInput from '@app/components/shared/key-only-input' import KeyValueInput from '@app/components/shared/key-value-input' import DyoChips, { chipsQALabelFromValue } from '@app/elements/dyo-chips' import { DyoHeading } from '@app/elements/dyo-heading' import { DyoLabel } from '@app/elements/dyo-label' +import DyoMessage from '@app/elements/dyo-message' import { - DagentConfigProperty, - DAGENT_CONFIG_PROPERTIES, - filterContains, - filterEmpty, - ImageConfigProperty, + ConcreteContainerConfigData, + CONTAINER_LOG_DRIVER_VALUES, + CONTAINER_NETWORK_MODE_VALUES, + CONTAINER_RESTART_POLICY_TYPE_VALUES, + CONTAINER_STATE_VALUES, ContainerConfigData, + ContainerConfigErrors, + ContainerConfigKey, ContainerLogDriverType, ContainerNetworkMode, ContainerRestartPolicyType, - CONTAINER_LOG_DRIVER_VALUES, - CONTAINER_NETWORK_MODE_VALUES, - CONTAINER_RESTART_POLICY_TYPE_VALUES, - DagentConfigDetails, - InstanceDagentConfigDetails, - mergeConfigs, ContainerState, - CONTAINER_STATE_VALUES, + DAGENT_CONFIG_KEYS, + DagentConfigKey, + filterContains, + filterEmpty, + stringResettable, } from '@app/models' +import { toNumber } from '@app/utils' +import { ContainerConfigValidationErrors, findErrorFor } from '@app/validations' import useTranslation from 'next-translate/useTranslation' import ConfigSectionLabel from './config-section-label' -import DyoMessage from '@app/elements/dyo-message' -import { ContainerConfigValidationErrors, findErrorFor } from '@app/validations' -import MultiInput from '@app/components/editor/multi-input' -import { toNumber } from '@app/utils' -type DagentConfigSectionBaseProps = { - config: T - resetableConfig?: T - onChange: (config: Partial) => void - onResetSection: (section: DagentConfigProperty) => void - selectedFilters: ImageConfigProperty[] +type DagentConfigSectionProps = { + config: ContainerConfigData | ConcreteContainerConfigData + onChange: (config: ContainerConfigData | ConcreteContainerConfigData) => void + onResetSection: (section: DagentConfigKey) => void + selectedFilters: ContainerConfigKey[] editorOptions: ItemEditorState disabled?: boolean fieldErrors: ContainerConfigValidationErrors + conflictErrors: ContainerConfigErrors + baseConfig: ContainerConfigData | null + resettableConfig: ContainerConfigData | ConcreteContainerConfigData } -type ImageDagentConfigSectionProps = DagentConfigSectionBaseProps & { - configType: 'image' -} - -type InstanceDagentConfigSectionProps = DagentConfigSectionBaseProps & { - configType: 'instance' - imageConfig: ContainerConfigData -} - -type DagentConfigSectionProps = ImageDagentConfigSectionProps | InstanceDagentConfigSectionProps - const DagentConfigSection = (props: DagentConfigSectionProps) => { const { - config: propsConfig, - resetableConfig: propsResetableConfig, - configType, + config, + resettableConfig, + baseConfig, onResetSection, selectedFilters, onChange, editorOptions, disabled, fieldErrors, + conflictErrors, } = props const { t } = useTranslation('container') - const disabledOnImage = configType === 'image' || disabled - // eslint-disable-next-line react/destructuring-assignment - const imageConfig = configType === 'instance' ? props.imageConfig : null - const resetableConfig = propsResetableConfig ?? propsConfig - const config = configType === 'instance' ? mergeConfigs(imageConfig, propsConfig) : propsConfig - - return !filterEmpty([...DAGENT_CONFIG_PROPERTIES], selectedFilters) ? null : ( + return !filterEmpty([...DAGENT_CONFIG_KEYS], selectedFilters) ? null : (
{t('base.dagent')} @@ -84,8 +70,9 @@ const DagentConfigSection = (props: DagentConfigSectionProps) => { {filterContains('networkMode', selectedFilters) && (
onResetSection('networkMode')} + error={conflictErrors?.networkMode} > {t('dagent.networkMode').toUpperCase()} @@ -112,7 +99,7 @@ const DagentConfigSection = (props: DagentConfigSectionProps) => { items={config.networks ?? []} keyPlaceholder={t('dagent.placeholders.network')} onChange={it => onChange({ networks: it })} - onResetSection={resetableConfig.networks ? () => onResetSection('networks') : null} + onResetSection={resettableConfig.networks ? () => onResetSection('networks') : null} unique={false} editorOptions={editorOptions} disabled={disabled} @@ -129,10 +116,11 @@ const DagentConfigSection = (props: DagentConfigSectionProps) => { labelClassName="text-bright font-semibold tracking-wide mb-2" label={t('dagent.dockerLabels').toUpperCase()} onChange={it => onChange({ dockerLabels: it })} - onResetSection={resetableConfig.dockerLabels ? () => onResetSection('dockerLabels') : null} + onResetSection={resettableConfig.dockerLabels ? () => onResetSection('dockerLabels') : null} items={config.dockerLabels ?? []} editorOptions={editorOptions} disabled={disabled} + errors={conflictErrors?.dockerLabels} />
@@ -142,8 +130,9 @@ const DagentConfigSection = (props: DagentConfigSectionProps) => { {filterContains('restartPolicy', selectedFilters) && (
onResetSection('restartPolicy')} + error={conflictErrors?.restartPolicy} > {t('dagent.restartPolicy').toUpperCase()} @@ -165,8 +154,9 @@ const DagentConfigSection = (props: DagentConfigSectionProps) => { {filterContains('logConfig', selectedFilters) && (
onResetSection('logConfig')} + error={conflictErrors?.logConfig} > {t('dagent.logConfig').toUpperCase()} @@ -203,8 +193,9 @@ const DagentConfigSection = (props: DagentConfigSectionProps) => { {filterContains('expectedState', selectedFilters) && (
onResetSection('expectedState')} + error={conflictErrors?.expectedState} > {t('dagent.expectedState').toUpperCase()} diff --git a/web/crux-ui/src/components/container-configs/edit-container-config-card.tsx b/web/crux-ui/src/components/container-configs/edit-container-config-card.tsx new file mode 100644 index 0000000000..3564fde5ee --- /dev/null +++ b/web/crux-ui/src/components/container-configs/edit-container-config-card.tsx @@ -0,0 +1,116 @@ +import EditContainerConfigHeading from '@app/components/container-configs/edit-container-config-heading' +import useContainerConfigSocket from '@app/components/container-configs/use-container-config-socket' +import useContainerConfigState, { setParseError } from '@app/components/container-configs/use-container-config-state' +import usePatchContainerConfig from '@app/components/container-configs/use-patch-container-config' +import useEditorState from '@app/components/editor/use-editor-state' +import useItemEditorState from '@app/components/editor/use-item-editor-state' +import { DyoCard } from '@app/elements/dyo-card' +import DyoImgButton from '@app/elements/dyo-img-button' +import DyoMessage from '@app/elements/dyo-message' +import { DyoConfirmationModal } from '@app/elements/dyo-modal' +import useConfirmation from '@app/hooks/use-confirmation' +import { ContainerConfig, ContainerConfigParent, VersionImage } from '@app/models' +import { createContainerConfigSchema, getValidationError } from '@app/validations' +import useTranslation from 'next-translate/useTranslation' +import { QA_DIALOG_LABEL_DELETE_IMAGE } from 'quality-assurance' +import ContainerConfigJsonEditor from './container-config-json-editor' + +type EditContainerCardProps = { + configParent: ContainerConfigParent + containerConfig: Config + image?: VersionImage + onDelete?: VoidFunction + convertConfigToJson: (config: Config) => Json + mergeJsonWithConfig: (config: Config, json: Json) => Config +} + +const EditContainerConfigCard = (props: EditContainerCardProps) => { + const { configParent, containerConfig, image, onDelete, convertConfigToJson, mergeJsonWithConfig } = props + const disabled = !configParent.mutable + + const { t } = useTranslation('images') + + const [state, dispatch] = useContainerConfigState({ + config: containerConfig, + parseError: null, + saveState: 'disconnected', + }) + const sock = useContainerConfigSocket(containerConfig.id, dispatch) + const [wsPatchConfig, cancelPatch] = usePatchContainerConfig(sock, dispatch) + const [deleteModal, confirmDelete] = useConfirmation() + + const name = containerConfig.name ?? configParent.name + + const deleteImage = async () => { + const confirmed = await confirmDelete({ + qaLabel: QA_DIALOG_LABEL_DELETE_IMAGE, + title: t('common:areYouSureDeleteName', { name }), + description: t('common:proceedYouLoseAllDataToName', { name }), + confirmText: t('common:delete'), + confirmColor: 'bg-error-red', + }) + + if (!confirmed) { + return + } + + onDelete() + } + + const editor = useEditorState(sock) + const editorState = useItemEditorState(editor, sock, containerConfig.id) + + const onPatch = (patch: Json) => { + const config = mergeJsonWithConfig(state.config as Config, patch) + wsPatchConfig({ + config, + }) + } + + const onParseError = (error: Error) => { + cancelPatch() + dispatch(setParseError(error.message)) + } + + const errorMessage = + state.parseError ?? + getValidationError(createContainerConfigSchema(image?.labels ?? {}), state.config, null, t)?.message + + return ( + <> + +
+ + + {!disabled && onDelete && ( + + )} +
+ + {errorMessage ? ( + + ) : null} + +
+ +
+
+ + + + ) +} + +export default EditContainerConfigCard diff --git a/web/crux-ui/src/components/projects/versions/images/edit-image-heading.tsx b/web/crux-ui/src/components/container-configs/edit-container-config-heading.tsx similarity index 82% rename from web/crux-ui/src/components/projects/versions/images/edit-image-heading.tsx rename to web/crux-ui/src/components/container-configs/edit-container-config-heading.tsx index 1ab59f6917..7c7313233a 100644 --- a/web/crux-ui/src/components/projects/versions/images/edit-image-heading.tsx +++ b/web/crux-ui/src/components/container-configs/edit-container-config-heading.tsx @@ -2,14 +2,14 @@ import { DyoHeading } from '@app/elements/dyo-heading' import { DyoLabel } from '@app/elements/dyo-label' import useTranslation from 'next-translate/useTranslation' -interface EditImageHeadingProps { +type EditContainerConfigHeadingProps = { className?: string imageName: string - imageTag: string + imageTag: string | null containerName: string } -const EditImageHeading = (props: EditImageHeadingProps) => { +const EditContainerConfigHeading = (props: EditContainerConfigHeadingProps) => { const { imageName, imageTag, containerName, className } = props const { t } = useTranslation('common') @@ -36,4 +36,4 @@ const EditImageHeading = (props: EditImageHeadingProps) => { ) } -export default EditImageHeading +export default EditContainerConfigHeading diff --git a/web/crux-ui/src/components/projects/versions/images/config/extendable-item-list.tsx b/web/crux-ui/src/components/container-configs/extendable-item-list.tsx similarity index 97% rename from web/crux-ui/src/components/projects/versions/images/config/extendable-item-list.tsx rename to web/crux-ui/src/components/container-configs/extendable-item-list.tsx index e49530091d..d39ddb10ca 100644 --- a/web/crux-ui/src/components/projects/versions/images/config/extendable-item-list.tsx +++ b/web/crux-ui/src/components/container-configs/extendable-item-list.tsx @@ -1,11 +1,11 @@ import DyoImgButton from '@app/elements/dyo-img-button' import DyoMessage from '@app/elements/dyo-message' import useRepatch, { RepatchAction } from '@app/hooks/use-repatch' +import { ErrorWithPath } from '@app/validations' import clsx from 'clsx' import { useEffect } from 'react' import { v4 as uuid } from 'uuid' import ConfigSectionLabel from './config-section-label' -import { ErrorWithPath } from '@app/validations' const addItem = (item: Omit): RepatchAction> => @@ -92,11 +92,13 @@ type InternalState = { editedItemId?: string } -interface ExtendableItemListProps { +type ExtendableItemListProps = { itemClassName?: string disabled?: boolean label: string + error?: string items: T[] + // id to error message renderItem: ( item: T, error: ErrorWithPath, @@ -120,6 +122,7 @@ const ExtendableItemList = (props: ExtendableItemListProps) = onResetSection, emptyItemFactory, itemClassName, + error, } = props const [state, dispatch] = useRepatch>({ @@ -150,6 +153,7 @@ const ExtendableItemList = (props: ExtendableItemListProps) = labelClassName="text-bright font-semibold tracking-wide" disabled={!hasValue || disabled || !onResetSection} onResetSection={onResetSection} + error={error} > {label.toUpperCase()} diff --git a/web/crux-ui/src/components/container-configs/use-container-config-socket.ts b/web/crux-ui/src/components/container-configs/use-container-config-socket.ts new file mode 100644 index 0000000000..9501373662 --- /dev/null +++ b/web/crux-ui/src/components/container-configs/use-container-config-socket.ts @@ -0,0 +1,38 @@ +import useTeamRoutes from '@app/hooks/use-team-routes' +import useWebSocket from '@app/hooks/use-websocket' +import { ConfigUpdatedMessage, WS_TYPE_CONFIG_UPDATED, WS_TYPE_PATCH_CONFIG, WS_TYPE_PATCH_RECEIVED } from '@app/models' +import WebSocketClientEndpoint from '@app/websockets/websocket-client-endpoint' +import { useCallback } from 'react' +import { ContainerConfigDispatch, patchConfig, setSaveState } from './use-container-config-state' + +const useContainerConfigSocket = (configId: string, dispatch: ContainerConfigDispatch): WebSocketClientEndpoint => { + const routes = useTeamRoutes() + + const sock = useWebSocket(routes.containerConfig.detailsSocket(configId), { + onOpen: () => dispatch(setSaveState('connected')), + onClose: () => dispatch(setSaveState('disconnected')), + onSend: message => { + if (message.type === WS_TYPE_PATCH_CONFIG) { + dispatch(setSaveState('saving')) + } + }, + onReceive: message => { + if (message.type === WS_TYPE_PATCH_RECEIVED) { + dispatch(setSaveState('saved')) + } + }, + }) + + const onConfigUpdated = useCallback( + (config: ConfigUpdatedMessage) => { + dispatch(patchConfig(config)) + }, + [dispatch], + ) + + sock.on(WS_TYPE_CONFIG_UPDATED, onConfigUpdated) + + return sock +} + +export default useContainerConfigSocket diff --git a/web/crux-ui/src/components/container-configs/use-container-config-state.ts b/web/crux-ui/src/components/container-configs/use-container-config-state.ts new file mode 100644 index 0000000000..a1809bce81 --- /dev/null +++ b/web/crux-ui/src/components/container-configs/use-container-config-state.ts @@ -0,0 +1,68 @@ +import useRepatch, { RepatchAction } from '@app/hooks/use-repatch' +import { + ConcreteContainerConfig, + ContainerConfig, + ContainerConfigData, + ContainerConfigKey, + WebSocketSaveState, +} from '@app/models' +import { Dispatch } from 'react' + +// state +export type ContainerConfigState = { + config: ContainerConfig | ConcreteContainerConfig + saveState: WebSocketSaveState + parseError: string +} + +export type ContainerConfigAction = RepatchAction +export type ContainerConfigDispatch = Dispatch + +// actions +export const setSaveState = + (saveState: WebSocketSaveState): ContainerConfigAction => + state => ({ + ...state, + saveState, + }) + +export const setParseError = + (error: string): ContainerConfigAction => + state => ({ + ...state, + parseError: error, + }) + +export const patchConfig = + (config: ContainerConfigData): ContainerConfigAction => + state => ({ + ...state, + saveState: 'saving', + config: { + ...state.config, + ...config, + }, + }) + +export const resetSection = + (section: ContainerConfigKey): ContainerConfigAction => + state => { + const newConfg: ContainerConfig = { + ...state.config, + } + + newConfg[section as string] = null + return { + ...state, + saveState: 'saving', + config: newConfg, + } + } + +// selectors + +// hook +const useContainerConfigState = (initialState: ContainerConfigState): [ContainerConfigState, ContainerConfigDispatch] => + useRepatch(initialState) + +export default useContainerConfigState diff --git a/web/crux-ui/src/components/container-configs/use-patch-container-config.ts b/web/crux-ui/src/components/container-configs/use-patch-container-config.ts new file mode 100644 index 0000000000..7e93d50b35 --- /dev/null +++ b/web/crux-ui/src/components/container-configs/use-patch-container-config.ts @@ -0,0 +1,54 @@ +import { WS_PATCH_DELAY } from '@app/const' +import { CANCEL_THROTTLE, useThrottling } from '@app/hooks/use-throttleing' +import { ContainerConfigData, PatchConfigMessage, WS_TYPE_PATCH_CONFIG } from '@app/models' +import WebSocketClientEndpoint from '@app/websockets/websocket-client-endpoint' +import { useCallback, useRef } from 'react' +import { ContainerConfigDispatch, patchConfig, resetSection } from './use-container-config-state' + +type PatchConfig = (config: PatchConfigMessage) => void + +const usePatchContainerConfig = ( + sock: WebSocketClientEndpoint, + dispatch: ContainerConfigDispatch, +): [PatchConfig, VoidFunction] => { + const configPatches = useRef({}) + + const throttle = useThrottling(WS_PATCH_DELAY) + + const wsSend = sock.send + + const patchCallback = useCallback( + (patch: PatchConfigMessage) => { + if (patch.resetSection) { + dispatch(resetSection(patch.resetSection)) + wsSend(WS_TYPE_PATCH_CONFIG, patch) + return + } + + configPatches.current = { + ...configPatches.current, + ...patch.config, + } + + dispatch(patchConfig(patch.config)) + + throttle(() => { + const wsPatch: PatchConfigMessage = { + config: configPatches.current, + } + + wsSend(WS_TYPE_PATCH_CONFIG, wsPatch) + }) + }, + [wsSend, throttle, dispatch], + ) + + const cancelThrottle = useCallback(() => { + throttle(CANCEL_THROTTLE) + configPatches.current = {} + }, [throttle]) + + return [patchCallback, cancelThrottle] +} + +export default usePatchContainerConfig diff --git a/web/crux-ui/src/components/dashboard/onboarding.tsx b/web/crux-ui/src/components/dashboard/onboarding.tsx index 50af662ee2..a5507b9523 100644 --- a/web/crux-ui/src/components/dashboard/onboarding.tsx +++ b/web/crux-ui/src/components/dashboard/onboarding.tsx @@ -47,7 +47,7 @@ const onboardingLinkFactories = (routes: TeamRoutes): Record { diff --git a/web/crux-ui/src/components/deployments/deployment-container-status-list.tsx b/web/crux-ui/src/components/deployments/deployment-container-status-list.tsx index 6665339cd8..391ce58ca8 100644 --- a/web/crux-ui/src/components/deployments/deployment-container-status-list.tsx +++ b/web/crux-ui/src/components/deployments/deployment-container-status-list.tsx @@ -33,7 +33,7 @@ interface DeploymentContainerStatusListProps { } type ContainerWithInstance = Container & { - instanceId: string + configId: string } const DeploymentContainerStatusList = (props: DeploymentContainerStatusListProps) => { @@ -46,10 +46,10 @@ const DeploymentContainerStatusList = (props: DeploymentContainerStatusListProps const [containers, setContainers] = useState(() => deployment.instances.map(it => ({ - instanceId: it.id, + configId: it.config.id, id: { prefix: deployment.prefix, - name: it.config?.name ?? it.image.config.name, + name: it.config.name ?? it.image.config.name, }, createdAt: null, state: null, @@ -88,7 +88,7 @@ const DeploymentContainerStatusList = (props: DeploymentContainerStatusListProps ? it : { ...strong[index], - instanceId: it.instanceId, + configId: it.configId, } }) } @@ -109,7 +109,7 @@ const DeploymentContainerStatusList = (props: DeploymentContainerStatusListProps } return !containers ? null : ( - + ( @@ -122,8 +122,8 @@ const DeploymentContainerStatusList = (props: DeploymentContainerStatusListProps - progress[it.instanceId]?.progress < 1 ? ( - + progress[it.configId]?.progress < 1 ? ( + ) : ( {`${it.imageName}:${it.imageTag}`} ) @@ -149,11 +149,8 @@ const DeploymentContainerStatusList = (props: DeploymentContainerStatusListProps
)} - - + + )} diff --git a/web/crux-ui/src/components/deployments/deployment-details-section.tsx b/web/crux-ui/src/components/deployments/deployment-details-section.tsx index 5b2fa4111e..c1d2cefd9d 100644 --- a/web/crux-ui/src/components/deployments/deployment-details-section.tsx +++ b/web/crux-ui/src/components/deployments/deployment-details-section.tsx @@ -1,42 +1,25 @@ -import useItemEditorState from '@app/components/editor/use-item-editor-state' -import KeyValueInput from '@app/components/shared/key-value-input' +import DyoButton from '@app/elements/dyo-button' import { DyoCard } from '@app/elements/dyo-card' -import DyoIcon from '@app/elements/dyo-icon' import { DyoLabel } from '@app/elements/dyo-label' -import DyoLink from '@app/elements/dyo-link' -import DyoMultiSelect from '@app/elements/dyo-multi-select' import useTeamRoutes from '@app/hooks/use-team-routes' -import { ConfigBundleOption } from '@app/models' -import { auditToLocaleDate, fetcher } from '@app/utils' +import { auditToLocaleDate } from '@app/utils' import clsx from 'clsx' import useTranslation from 'next-translate/useTranslation' -import useSWR from 'swr' +import Image from 'next/image' import DeploymentStatusTag from './deployment-status-tag' -import { DeploymentActions, DeploymentState } from './use-deployment-state' +import { DeploymentState } from './use-deployment-state' -interface DeploymentDetailsSectionProps { +type DeploymentDetailsSectionProps = { className?: string state: DeploymentState - actions: DeploymentActions } -const ITEM_ID = 'deployment' - const DeploymentDetailsSection = (props: DeploymentDetailsSectionProps) => { - const { state, actions, className } = props - const { deployment, mutable, editor, sock } = state + const { state, className } = props + const { deployment } = state const { t } = useTranslation('deployments') - const teamRoutes = useTeamRoutes() - - const editorState = useItemEditorState(editor, sock, ITEM_ID) - - const { data: configBundleOptions } = useSWR(teamRoutes.configBundle.api.options(), fetcher) - - const configBundlesHref = - deployment.configBundleIds?.length === 1 - ? teamRoutes.configBundle.details(deployment.configBundleIds[0]) - : teamRoutes.configBundle.list() + const routes = useTeamRoutes() return ( @@ -50,42 +33,20 @@ const DeploymentDetailsSection = (props: DeploymentDetailsSectionProps) => {
- - {t('configBundle').toUpperCase()} - - -
- it.id} - labelConverter={it => it.name} - selection={deployment.configBundleIds ?? []} - onSelectionChange={it => actions.onConfigBundlesSelected(it)} - /> - - - - +
+ +
+ {t('common:config')} + {t('common:config')} +
+
- - - deployment.configBundleEnvironment[key] - ? t('bundleNameVariableWillBeOverwritten', { - configBundle: deployment.configBundleEnvironment[key], - }) - : null - } - /> ) } diff --git a/web/crux-ui/src/components/deployments/deployment-view-list.tsx b/web/crux-ui/src/components/deployments/deployment-view-list.tsx index 94ce2606d9..1efc23fd5c 100644 --- a/web/crux-ui/src/components/deployments/deployment-view-list.tsx +++ b/web/crux-ui/src/components/deployments/deployment-view-list.tsx @@ -3,7 +3,7 @@ import DyoIcon from '@app/elements/dyo-icon' import DyoLink from '@app/elements/dyo-link' import DyoTable, { DyoColumn, dyoCheckboxColumn, sortDate, sortString } from '@app/elements/dyo-table' import useTeamRoutes from '@app/hooks/use-team-routes' -import { Instance } from '@app/models' +import { Instance, containerNameOfInstance } from '@app/models' import { utcDateToLocale } from '@app/utils' import useTranslation from 'next-translate/useTranslation' import { DeploymentActions, DeploymentState } from './use-deployment-state' @@ -18,7 +18,7 @@ const DeploymentViewList = (props: DeploymentViewListProps) => { const routes = useTeamRoutes() const { state, actions } = props - const { instances, deployInstances, project, version, deployment } = state + const { instances, deployInstances } = state return ( @@ -34,9 +34,9 @@ const DeploymentViewList = (props: DeploymentViewListProps) => { header={t('containerName')} className="w-4/12" sortable - sortField={(it: Instance) => it.config?.name ?? it.image.config.name} + sortField={containerNameOfInstance} sort={sortString} - body={(it: Instance) => it.config?.name ?? it.image.config.name} + body={containerNameOfInstance} /> { <>
- +
- + )} diff --git a/web/crux-ui/src/components/deployments/deployment-view-tile.tsx b/web/crux-ui/src/components/deployments/deployment-view-tile.tsx index d9246abedf..6f6fdc28bc 100644 --- a/web/crux-ui/src/components/deployments/deployment-view-tile.tsx +++ b/web/crux-ui/src/components/deployments/deployment-view-tile.tsx @@ -1,20 +1,39 @@ import DyoWrap from '@app/elements/dyo-wrap' -import EditInstanceCard from './instances/edit-instance-card' -import { DeploymentActions, DeploymentState } from './use-deployment-state' +import { + concreteContainerConfigToJsonConfig, + ContainerConfigParent, + mergeJsonConfigToConcreteContainerConfig, +} from '@app/models' +import EditContainerConfigCard from '../container-configs/edit-container-config-card' +import { DeploymentState } from './use-deployment-state' -export interface DeploymentViewTileProps { +export type DeploymentViewTileProps = { state: DeploymentState - actions: DeploymentActions } const DeploymentViewTile = (props: DeploymentViewTileProps) => { - const { state, actions } = props + const { state } = props return ( - {state.instances.map(it => ( - - ))} + {state.instances.map(it => { + const parent: ContainerConfigParent = { + id: it.image.id, + name: it.image.name, + mutable: state.mutable, + } + + return ( + + ) + })} ) } diff --git a/web/crux-ui/src/components/deployments/edit-deployment-card.tsx b/web/crux-ui/src/components/deployments/edit-deployment-card.tsx index 5cf93e3cf5..93f0eaf791 100644 --- a/web/crux-ui/src/components/deployments/edit-deployment-card.tsx +++ b/web/crux-ui/src/components/deployments/edit-deployment-card.tsx @@ -2,20 +2,28 @@ import DyoButton from '@app/elements/dyo-button' import { DyoCard } from '@app/elements/dyo-card' import DyoForm from '@app/elements/dyo-form' import { DyoHeading } from '@app/elements/dyo-heading' +import DyoIcon from '@app/elements/dyo-icon' import { DyoInput } from '@app/elements/dyo-input' +import { DyoLabel } from '@app/elements/dyo-label' +import DyoLink from '@app/elements/dyo-link' +import DyoMultiSelect from '@app/elements/dyo-multi-select' import DyoTextArea from '@app/elements/dyo-text-area' import DyoToggle from '@app/elements/dyo-toggle' import { defaultApiErrorHandler } from '@app/errors' import useDyoFormik from '@app/hooks/use-dyo-formik' import { SubmitHook } from '@app/hooks/use-submit' import useTeamRoutes from '@app/hooks/use-team-routes' -import { DeploymentDetails, PatchDeployment } from '@app/models' -import { sendForm } from '@app/utils' +import { ConfigBundle, Deployment, DeploymentDetails, detailsToConfigBundle, UpdateDeployment } from '@app/models' +import { fetcher, sendForm } from '@app/utils' import { updateDeploymentSchema } from '@app/validations' import useTranslation from 'next-translate/useTranslation' -import React from 'react' +import useSWR from 'swr' -interface EditDeploymentCardProps { +type EditableDeployment = Pick & { + configBundles: ConfigBundle[] +} + +type EditDeploymentCardProps = { className?: string deployment: DeploymentDetails submit: SubmitHook @@ -23,38 +31,54 @@ interface EditDeploymentCardProps { } const EditDeploymentCard = (props: EditDeploymentCardProps) => { - const { deployment, className, onDeploymentEdited, submit } = props + const { deployment: propsDeployment, className, onDeploymentEdited, submit } = props + + const deployment: EditableDeployment = { + ...propsDeployment, + configBundles: propsDeployment.configBundles.map(it => detailsToConfigBundle(it)), + } const { t } = useTranslation('deployments') const routes = useTeamRoutes() const handleApiError = defaultApiErrorHandler(t) + const { data: configBundles, error: configBundlesError } = useSWR( + routes.configBundle.api.list(), + fetcher, + ) + const formik = useDyoFormik({ submit, initialValues: deployment, validationSchema: updateDeploymentSchema, t, onSubmit: async (values, { setFieldError }) => { - const transformedValues = updateDeploymentSchema.cast(values) as any - - const body: PatchDeployment = { - ...transformedValues, + const body: UpdateDeployment = { + ...values, + configBundles: values.configBundles.map(it => it.id), } - const res = await sendForm('PATCH', routes.deployment.api.details(deployment.id), body) + let res = await sendForm('PUT', routes.deployment.api.details(deployment.id), body) if (res.ok) { - onDeploymentEdited({ - ...deployment, - ...transformedValues, - }) - } else { - await handleApiError(res, setFieldError) + res = await fetch(routes.deployment.api.details(deployment.id)) + if (res.ok) { + const deploy: DeploymentDetails = await res.json() + + onDeploymentEdited(deploy) + return + } } + await handleApiError(res, setFieldError) }, }) + const configBundlesHref = + deployment.configBundles?.length === 1 + ? routes.configBundle.details(deployment.configBundles[0].id) + : routes.configBundle.list() + return ( @@ -90,6 +114,26 @@ const EditDeploymentCard = (props: EditDeploymentCardProps) => { value={formik.values.note} /> + {t('configBundle')} + + {!configBundlesError && ( +
+ { + await formik.setFieldValue('configBundles', it) + }} + /> + + + + +
+ )} +
diff --git a/web/crux-ui/src/components/deployments/edit-deployment-instances.tsx b/web/crux-ui/src/components/deployments/edit-deployment-instances.tsx index 878523990c..b124a4db5f 100644 --- a/web/crux-ui/src/components/deployments/edit-deployment-instances.tsx +++ b/web/crux-ui/src/components/deployments/edit-deployment-instances.tsx @@ -19,7 +19,7 @@ const EditDeploymentInstances = (props: EditDeploymentInstancesProps) => {
{viewMode === 'tile' ? ( - + ) : ( )} diff --git a/web/crux-ui/src/components/deployments/instances/edit-instance-card.tsx b/web/crux-ui/src/components/deployments/instances/edit-instance-card.tsx deleted file mode 100644 index 8d7aade05e..0000000000 --- a/web/crux-ui/src/components/deployments/instances/edit-instance-card.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import useItemEditorState from '@app/components/editor/use-item-editor-state' -import { DyoCard } from '@app/elements/dyo-card' -import DyoMessage from '@app/elements/dyo-message' -import { - Instance, - instanceConfigToJsonInstanceConfig, - InstanceJsonContainerConfig, - mergeJsonConfigToInstanceContainerConfig, -} from '@app/models' -import EditImageHeading from '../../projects/versions/images/edit-image-heading' -import EditImageJson from '../../projects/versions/images/edit-image-json' -import { DeploymentActions, DeploymentState } from '../use-deployment-state' -import useInstanceState from './use-instance-state' - -interface EditInstanceCardProps { - instance: Instance - deploymentState: DeploymentState - deploymentActions: DeploymentActions -} - -const EditInstanceCard = (props: EditInstanceCardProps) => { - const { instance, deploymentState, deploymentActions } = props - const { editor, sock } = deploymentState - - const [state, actions] = useInstanceState({ - instance, - deploymentState, - deploymentActions, - }) - - const { config, errorMessage } = state - - const editorState = useItemEditorState(editor, sock, instance.id) - - return ( - -
- -
- - {errorMessage ? ( - - ) : null} - -
- - actions.onPatch(instance.id, mergeJsonConfigToInstanceContainerConfig(config, it)) - } - onParseError={actions.onParseError} - convertConfigToJson={instanceConfigToJsonInstanceConfig} - /> -
-
- ) -} - -export default EditInstanceCard diff --git a/web/crux-ui/src/components/deployments/instances/use-instance-state.ts b/web/crux-ui/src/components/deployments/instances/use-instance-state.ts index b05223fac6..8298ad9661 100644 --- a/web/crux-ui/src/components/deployments/instances/use-instance-state.ts +++ b/web/crux-ui/src/components/deployments/instances/use-instance-state.ts @@ -1,103 +1,100 @@ -import { - ContainerConfigData, - GetInstanceSecretsMessage, - ImageConfigProperty, - Instance, - InstanceContainerConfigData, - InstanceSecretsMessage, - mergeConfigs, - MergedContainerConfigData, - PatchInstanceMessage, - WS_TYPE_GET_INSTANCE_SECRETS, - WS_TYPE_INSTANCE_SECRETS, - WS_TYPE_PATCH_INSTANCE, -} from '@app/models' -import { createContainerConfigSchema, getValidationError } from '@app/validations' -import { useEffect, useState } from 'react' -import { DeploymentActions, DeploymentState } from '../use-deployment-state' -import useTranslation from 'next-translate/useTranslation' - -export type InstanceStateOptions = { - deploymentState: DeploymentState - deploymentActions: DeploymentActions - instance: Instance -} - -export type InstanceState = { - config: MergedContainerConfigData - resetableConfig: ContainerConfigData - definedSecrets: string[] - errorMessage: string -} - -export type InstanceActions = { - resetSection: (section: ImageConfigProperty) => void - onPatch: (newConfig: Partial) => void - onParseError: (error: Error) => void -} - -const useInstanceState = (options: InstanceStateOptions) => { - const { t } = useTranslation('container') - - const { instance, deploymentState, deploymentActions } = options - const { sock } = deploymentState - - const [parseError, setParseError] = useState(null) - const [definedSecrets, setDefinedSecrets] = useState([]) - - sock.on(WS_TYPE_INSTANCE_SECRETS, (message: InstanceSecretsMessage) => { - if (message.instanceId !== instance.id) { - return - } - - setDefinedSecrets(message.keys) - }) - - useEffect(() => { - sock.send(WS_TYPE_GET_INSTANCE_SECRETS, { - id: instance.id, - } as GetInstanceSecretsMessage) - }, [instance.id, sock]) - - const mergedConfig = mergeConfigs(instance.image.config, instance.config) - - const errorMessage = - parseError ?? getValidationError(createContainerConfigSchema(instance.image.labels), mergedConfig, null, t)?.message - - const resetSection = (section: ImageConfigProperty): InstanceContainerConfigData => { - const newConfig = { ...instance.config } as any - newConfig[section] = null - - deploymentActions.updateInstanceConfig(instance.id, newConfig) - - sock.send(WS_TYPE_PATCH_INSTANCE, { - instanceId: instance.id, - resetSection: section, - } as PatchInstanceMessage) - - return newConfig - } - - const onPatch = (id: string, newConfig: InstanceContainerConfigData) => { - deploymentActions.onPatchInstance(id, newConfig) - setParseError(null) - } - - const onParseError = (err: Error) => setParseError(err.message) - - return [ - { - config: mergedConfig, - resetableConfig: instance.config, - definedSecrets, - errorMessage, - }, - { - onPatch, - resetSection, - onParseError, - }, - ] -} - -export default useInstanceState +// import { +// ContainerConfigData, +// GetInstanceSecretsMessage, +// ContainerConfigProperty, +// Instance, +// InstanceSecretsMessage, +// mergeConfigs, +// ConcreteContainerConfigData, +// WS_TYPE_GET_INSTANCE_SECRETS, +// WS_TYPE_INSTANCE_SECRETS, +// } from '@app/models' +// import { createContainerConfigSchema, getValidationError } from '@app/validations' +// import { useEffect, useState } from 'react' +// import { DeploymentActions, DeploymentState } from '../use-deployment-state' +// import useTranslation from 'next-translate/useTranslation' + +// export type InstanceStateOptions = { +// deploymentState: DeploymentState +// deploymentActions: DeploymentActions +// instance: Instance +// } + +// export type InstanceState = { +// config: ConcreteContainerConfigData +// resetableConfig: ContainerConfigData +// definedSecrets: string[] +// errorMessage: string +// } + +// export type InstanceActions = { +// resetSection: (section: ContainerConfigProperty) => void +// onPatch: (newConfig: Partial) => void +// onParseError: (error: Error) => void +// } + +// const useInstanceState = (options: InstanceStateOptions) => { +// const { t } = useTranslation('container') + +// const { instance, deploymentState, deploymentActions } = options +// const { sock } = deploymentState + +// const [parseError, setParseError] = useState(null) +// const [definedSecrets, setDefinedSecrets] = useState([]) + +// sock.on(WS_TYPE_INSTANCE_SECRETS, (message: InstanceSecretsMessage) => { +// if (message.instanceId !== instance.id) { +// return +// } + +// setDefinedSecrets(message.keys) +// }) + +// useEffect(() => { +// sock.send(WS_TYPE_GET_INSTANCE_SECRETS, { +// id: instance.id, +// } as GetInstanceSecretsMessage) +// }, [instance.id, sock]) + +// const mergedConfig = mergeConfigs(instance.image.config, instance.config) + +// const errorMessage = +// parseError ?? getValidationError(createContainerConfigSchema(instance.image.labels), mergedConfig, null, t)?.message + +// const resetSection = (section: ContainerConfigProperty): ConcreteContainerConfigData => { +// const newConfig = { ...instance.config } as any +// newConfig[section] = null + +// deploymentActions.updateInstanceConfig(instance.id, newConfig) + +// sock.send(WS_TYPE_PATCH_INSTANCE, { +// instanceId: instance.id, +// resetSection: section, +// } as PatchInstanceMessage) + +// return newConfig +// } + +// const onPatch = (id: string, newConfig: ConcreteContainerConfigData) => { +// deploymentActions.onPatchInstance(id, newConfig) +// setParseError(null) +// } + +// const onParseError = (err: Error) => setParseError(err.message) + +// return [ +// { +// config: mergedConfig, +// resetableConfig: instance.config, +// definedSecrets, +// errorMessage, +// }, +// { +// onPatch, +// resetSection, +// onParseError, +// }, +// ] +// } + +// export default useInstanceState diff --git a/web/crux-ui/src/components/deployments/use-deployment-state.tsx b/web/crux-ui/src/components/deployments/use-deployment-state.tsx index 225b0e8a2a..6d04bb1db3 100644 --- a/web/crux-ui/src/components/deployments/use-deployment-state.tsx +++ b/web/crux-ui/src/components/deployments/use-deployment-state.tsx @@ -1,16 +1,14 @@ import useEditorState, { EditorState } from '@app/components/editor/use-editor-state' import useNodeState from '@app/components/nodes/use-node-state' import { ViewMode } from '@app/components/shared/view-mode-toggle' -import { DEPLOYMENT_EDIT_WS_REQUEST_DELAY } from '@app/const' import { DyoConfirmationModalConfig } from '@app/elements/dyo-modal' import useConfirmation from '@app/hooks/use-confirmation' import usePersistedViewMode from '@app/hooks/use-persisted-view-mode' import useTeamRoutes from '@app/hooks/use-team-routes' -import { useThrottling } from '@app/hooks/use-throttleing' import useWebSocket from '@app/hooks/use-websocket' import { + ConcreteContainerConfigData, DeploymentDetails, - DeploymentEnvUpdatedMessage, DeploymentInvalidatedSecrets, deploymentIsCopiable, deploymentIsDeletable, @@ -20,34 +18,24 @@ import { DeploymentRoot, DeploymentToken, DyoNode, - GetInstanceMessage, ImageDeletedMessage, Instance, - InstanceContainerConfigData, + instanceCreatedMessageToInstance, InstanceMessage, InstancesAddedMessage, - InstanceUpdatedMessage, NodeEventMessage, - PatchInstanceMessage, ProjectDetails, - UniqueKeyValue, VersionDetails, WebSocketSaveState, - WS_TYPE_DEPLOYMENT_ENV_UPDATED, - WS_TYPE_GET_INSTANCE, WS_TYPE_IMAGE_DELETED, WS_TYPE_INSTANCE, WS_TYPE_INSTANCES_ADDED, - WS_TYPE_INSTANCE_UPDATED, WS_TYPE_NODE_EVENT, - WS_TYPE_PATCH_DEPLOYMENT_ENV, - WS_TYPE_PATCH_INSTANCE, - WS_TYPE_PATCH_RECEIVED, } from '@app/models' import WebSocketClientEndpoint from '@app/websockets/websocket-client-endpoint' import useTranslation from 'next-translate/useTranslation' import { QA_DIALOG_LABEL_REVOKE_DEPLOY_TOKEN } from 'quality-assurance' -import { useRef, useState } from 'react' +import { useState } from 'react' export type DeploymentEditState = 'details' | 'edit' | 'copy' | 'create-token' @@ -80,25 +68,23 @@ export type DeploymentState = { export type DeploymentActions = { setEditState: (state: DeploymentEditState) => void onDeploymentEdited: (editedDeployment: DeploymentDetails) => void - onEnvironmentEdited: (environment: UniqueKeyValue[]) => void - onPatchInstance: (id: string, newConfig: InstanceContainerConfigData) => void - updateInstanceConfig: (id: string, newConfig: InstanceContainerConfigData) => void + onPatchInstance: (id: string, newConfig: ConcreteContainerConfigData) => void + updateInstanceConfig: (id: string, newConfig: ConcreteContainerConfigData) => void setViewMode: (viewMode: ViewMode) => void onInvalidateSecrets: (secrets: DeploymentInvalidatedSecrets[]) => void onDeploymentTokenCreated: (token: DeploymentToken) => void onRevokeDeploymentToken: VoidFunction onInstanceSelected: (id: string, deploy: boolean) => void onAllInstancesToggled: (deploy: boolean) => void - onConfigBundlesSelected: (configBundleId?: string[]) => void } -const mergeInstancePatch = (instance: Instance, message: InstanceUpdatedMessage): Instance => ({ - ...instance, - config: { - ...instance.config, - ...message, - }, -}) +// const mergeInstancePatch = (instance: Instance, message: InstanceUpdatedMessage): Instance => ({ +// ...instance, +// config: { +// ...instance.config, +// ...message, +// }, +// }) const useDeploymentState = (options: DeploymentStateOptions): [DeploymentState, DeploymentActions] => { const { t } = useTranslation('deployments') @@ -107,13 +93,13 @@ const useDeploymentState = (options: DeploymentStateOptions): [DeploymentState, const { deployment: optionDeploy, onWsError, onApiError } = options const { project, version } = optionDeploy - const throttle = useThrottling(DEPLOYMENT_EDIT_WS_REQUEST_DELAY) + // const throttle = useThrottling(DEPLOYMENT_EDIT_WS_REQUEST_DELAY) - const patch = useRef>({}) + // const patch = useRef>({}) const [deployment, setDeployment] = useState(optionDeploy) const [node, setNode] = useNodeState(optionDeploy.node) - const [saveState, setSaveState] = useState(null) + const [saveState] = useState(null) const [editState, setEditState] = useState('details') const [instances, setInstances] = useState(deployment.instances ?? []) const [viewMode, setViewMode] = usePersistedViewMode({ initialViewMode: 'list', pageName: 'deployments' }) @@ -140,51 +126,53 @@ const useDeploymentState = (options: DeploymentStateOptions): [DeploymentState, }) const sock = useWebSocket(routes.deployment.detailsSocket(deployment.id), { - onOpen: () => setSaveState('connected'), - onClose: () => setSaveState('disconnected'), - onSend: message => { - if ([WS_TYPE_PATCH_INSTANCE, WS_TYPE_PATCH_DEPLOYMENT_ENV].includes(message.type)) { - setSaveState('saving') - } - }, - onReceive: message => { - if (WS_TYPE_PATCH_RECEIVED === message.type) { - setSaveState('saved') - } - }, + // onOpen: () => setSaveState('connected'), + // onClose: () => setSaveState('disconnected'), + // onSend: message => { + // if ([WS_TYPE_PATCH_INSTANCE, WS_TYPE_PATCH_DEPLOYMENT_ENV].includes(message.type)) { + // setSaveState('saving') + // } + // }, + // onReceive: message => { + // if (WS_TYPE_PATCH_RECEIVED === message.type) { + // setSaveState('saved') + // } + // }, onError: onWsError, }) const editor = useEditorState(sock) - sock.on(WS_TYPE_DEPLOYMENT_ENV_UPDATED, (message: DeploymentEnvUpdatedMessage) => { - setDeployment({ - ...deployment, - ...message, - }) - }) + // sock.on(WS_TYPE_DEPLOYMENT_ENV_UPDATED, (message: DeploymentEnvUpdatedMessage) => { + // setDeployment({ + // ...deployment, + // ...message, + // }) + // }) - sock.on(WS_TYPE_INSTANCE_UPDATED, (message: InstanceUpdatedMessage) => { - const index = instances.findIndex(it => it.id === message.instanceId) - if (index < 0) { - sock.send(WS_TYPE_GET_INSTANCE, { - id: message.instanceId, - } as GetInstanceMessage) - return - } + // sock.on(WS_TYPE_INSTANCE_UPDATED, (message: InstanceUpdatedMessage) => { + // const index = instances.findIndex(it => it.id === message.instanceId) + // if (index < 0) { + // sock.send(WS_TYPE_GET_INSTANCE, { + // id: message.instanceId, + // } as GetInstanceMessage) + // return + // } - const oldOne = instances[index] - const instance = mergeInstancePatch(oldOne, message) + // const oldOne = instances[index] + // const instance = mergeInstancePatch(oldOne, message) - const newInstances = [...instances] - newInstances[index] = instance + // const newInstances = [...instances] + // newInstances[index] = instance - setInstances(newInstances) - }) + // setInstances(newInstances) + // }) sock.on(WS_TYPE_INSTANCE, (message: InstanceMessage) => setInstances([...instances, message])) - sock.on(WS_TYPE_INSTANCES_ADDED, (message: InstancesAddedMessage) => setInstances([...instances, ...message])) + sock.on(WS_TYPE_INSTANCES_ADDED, (message: InstancesAddedMessage) => + setInstances([...instances, ...message.map(it => instanceCreatedMessageToInstance(it))]), + ) sock.on(WS_TYPE_IMAGE_DELETED, (message: ImageDeletedMessage) => setInstances(instances.filter(it => it.image.id !== message.imageId)), @@ -195,31 +183,18 @@ const useDeploymentState = (options: DeploymentStateOptions): [DeploymentState, setEditState('details') } - const onEnvironmentEdited = environment => { - setSaveState('saving') - setDeployment({ - ...deployment, - environment, - }) - throttle(() => { - sock.send(WS_TYPE_PATCH_DEPLOYMENT_ENV, { - environment, - }) - }) - } - - const onConfigBundlesSelected = configBundleIds => { - setSaveState('saving') - setDeployment({ - ...deployment, - configBundleIds, - }) - throttle(() => { - sock.send(WS_TYPE_PATCH_DEPLOYMENT_ENV, { - configBundleIds, - }) - }) - } + // const onEnvironmentEdited = environment => { + // setSaveState('saving') + // setDeployment({ + // ...deployment, + // environment, + // }) + // throttle(() => { + // sock.send(WS_TYPE_PATCH_DEPLOYMENT_ENV, { + // environment, + // }) + // }) + // } const onInvalidateSecrets = (secrets: DeploymentInvalidatedSecrets[]) => { const newInstances = instances.map(it => { @@ -231,8 +206,8 @@ const useDeploymentState = (options: DeploymentStateOptions): [DeploymentState, return { ...it, config: { - ...(it.config ?? {}), - secrets: (it.config?.secrets ?? []).map(secret => { + ...it.config, + secrets: (it.config.secrets ?? []).map(secret => { if (invalidated.invalid.includes(secret.id)) { return { ...secret, @@ -251,66 +226,56 @@ const useDeploymentState = (options: DeploymentStateOptions): [DeploymentState, setInstances(newInstances) } - const onPatchInstance = (id: string, newConfig: InstanceContainerConfigData) => { - const index = instances.findIndex(it => it.id === id) - if (index < 0) { - return - } - - setSaveState('saving') - - const newPatch = { - ...patch.current, - ...newConfig, - } - patch.current = newPatch - - const newInstances = [...instances] - const instance = newInstances[index] - - newInstances[index] = { - ...instance, - config: instance.config - ? { - ...instance.config, - ...newConfig, - } - : newConfig, - } - - setInstances(newInstances) - - throttle(() => { - sock.send(WS_TYPE_PATCH_INSTANCE, { - instanceId: id, - config: patch.current, - } as PatchInstanceMessage) - patch.current = {} - }) + const onPatchInstance = (_: string, __: ConcreteContainerConfigData) => { + // const onPatchInstance = (id: string, newConfig: ConcreteContainerConfigData) => { + // const index = instances.findIndex(it => it.id === id) + // if (index < 0) { + // return + // } + // setSaveState('saving') + // const newPatch = { + // ...patch.current, + // ...newConfig, + // } + // patch.current = newPatch + // const newInstances = [...instances] + // const instance = newInstances[index] + // newInstances[index] = { + // ...instance, + // config: { + // ...instance.config, + // ...newConfig, + // }, + // } + // setInstances(newInstances) + // throttle(() => { + // sock.send(WS_TYPE_PATCH_INSTANCE, { + // instanceId: id, + // config: patch.current, + // } as PatchInstanceMessage) + // patch.current = {} + // }) } - const updateInstanceConfig = (id: string, newConfig: InstanceContainerConfigData) => { - const index = instances.findIndex(it => it.id === id) - if (index < 0) { - return - } - - setSaveState('saving') - - const newInstances = [...instances] - const instance = newInstances[index] - - newInstances[index] = { - ...instance, - config: instance.config - ? { - ...instance.config, - ...newConfig, - } - : newConfig, - } - - setInstances(newInstances) + const updateInstanceConfig = (_: string, __: ConcreteContainerConfigData) => { + // const updateInstanceConfig = (id: string, newConfig: ConcreteContainerConfigData) => { + // const index = instances.findIndex(it => it.id === id) + // if (index < 0) { + // return + // } + // setSaveState('saving') + // const newInstances = [...instances] + // const instance = newInstances[index] + // newInstances[index] = { + // ...instance, + // config: instance.config + // ? { + // ...instance.config, + // ...newConfig, + // } + // : newConfig, + // } + // setInstances(newInstances) } const onDeploymentTokenCreated = (token: DeploymentToken) => { @@ -382,7 +347,6 @@ const useDeploymentState = (options: DeploymentStateOptions): [DeploymentState, { setEditState, onDeploymentEdited, - onEnvironmentEdited, setViewMode, onInvalidateSecrets, onPatchInstance, @@ -391,7 +355,6 @@ const useDeploymentState = (options: DeploymentStateOptions): [DeploymentState, onInstanceSelected, onAllInstancesToggled, updateInstanceConfig, - onConfigBundlesSelected, }, ] } diff --git a/web/crux-ui/src/components/nodes/node-containers-list.tsx b/web/crux-ui/src/components/nodes/node-containers-list.tsx index b0b12f6fef..73af31fe54 100644 --- a/web/crux-ui/src/components/nodes/node-containers-list.tsx +++ b/web/crux-ui/src/components/nodes/node-containers-list.tsx @@ -18,12 +18,12 @@ import { containerIsStopable, containerPortsToString, containerPrefixNameOf, - imageName, + imageNameOfContainer, } from '@app/models' import { utcDateToLocale } from '@app/utils' import useTranslation from 'next-translate/useTranslation' -interface NodeContainersListProps { +type NodeContainersListProps = { state: NodeDetailsState actions: NodeDetailsActions showHidden?: boolean @@ -57,12 +57,10 @@ const NodeContainersList = (props: NodeContainersListProps) => { header={t('images:imageTag')} className="w-3/12" sortable - sortField={(it: Container) => imageName(it.imageName, it.imageTag)} + sortField={imageNameOfContainer} sort={sortString} bodyClassName="truncate" - body={(it: Container) => ( - {imageName(it.imageName, it.imageTag)} - )} + body={(it: Container) => {imageNameOfContainer(it)}} /> { const [images, setImages] = useState([]) const [filterOrName, setFilterOrName] = useState('') const [inputMessage, setInputMessage] = useState(null) - const throttleFilter = useThrottling(IMAGE_WS_REQUEST_DELAY) + const throttleFilter = useThrottling(WS_PATCH_DELAY) const registriesFound = registries?.length > 0 diff --git a/web/crux-ui/src/components/projects/versions/images/config/config-section-label.tsx b/web/crux-ui/src/components/projects/versions/images/config/config-section-label.tsx deleted file mode 100644 index d64a1dccb7..0000000000 --- a/web/crux-ui/src/components/projects/versions/images/config/config-section-label.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import DyoIcon from '@app/elements/dyo-icon' -import { DyoLabel } from '@app/elements/dyo-label' -import clsx from 'clsx' - -type ConfigSectionLabelProps = { - disabled: boolean - className?: string - labelClassName?: string - children: React.ReactNode - onResetSection: VoidFunction -} - -const ConfigSectionLabel = (props: ConfigSectionLabelProps) => { - const { disabled, className, labelClassName, children, onResetSection } = props - - return ( -
- {children} - - {disabled ? null : ( - - )} -
- ) -} - -export default ConfigSectionLabel diff --git a/web/crux-ui/src/components/projects/versions/images/edit-image-card.tsx b/web/crux-ui/src/components/projects/versions/images/edit-image-card.tsx deleted file mode 100644 index ff3bd4c4f2..0000000000 --- a/web/crux-ui/src/components/projects/versions/images/edit-image-card.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import useItemEditorState from '@app/components/editor/use-item-editor-state' -import { DyoCard } from '@app/elements/dyo-card' -import DyoImgButton from '@app/elements/dyo-img-button' -import DyoMessage from '@app/elements/dyo-message' -import { DyoConfirmationModal } from '@app/elements/dyo-modal' -import { - imageConfigToJsonContainerConfig, - JsonContainerConfig, - mergeJsonConfigToImageContainerConfig, - VersionImage, -} from '@app/models' -import { createContainerConfigSchema, getValidationError } from '@app/validations' -import useTranslation from 'next-translate/useTranslation' -import { VerionState, VersionActions } from '../use-version-state' -import EditImageHeading from './edit-image-heading' -import EditImageJson from './edit-image-json' -import useImageEditorState from './use-image-editor-state' - -interface EditImageCardProps { - disabled?: boolean - image: VersionImage - imagesState: VerionState - imagesActions: VersionActions -} - -const EditImageCard = (props: EditImageCardProps) => { - const { disabled, image, imagesState, imagesActions } = props - const { editor, versionSock } = imagesState - - const { t } = useTranslation('images') - - const [state, actions] = useImageEditorState({ - image, - imagesState, - imagesActions, - sock: versionSock, - t, - }) - - const editorState = useItemEditorState(editor, versionSock, image.id) - const errorMessage = - state.parseError ?? getValidationError(createContainerConfigSchema(image.labels), image.config, null, t)?.message - - return ( - <> - -
- - - {disabled ? null : ( - - )} -
- - {errorMessage ? ( - - ) : null} - -
- - actions.onPatch(mergeJsonConfigToImageContainerConfig(image.config, it)) - } - onParseError={actions.setParseError} - convertConfigToJson={imageConfigToJsonContainerConfig} - /> -
-
- - - - ) -} - -export default EditImageCard diff --git a/web/crux-ui/src/components/projects/versions/images/use-image-editor-state.ts b/web/crux-ui/src/components/projects/versions/images/use-image-editor-state.ts deleted file mode 100644 index dc5184d4d4..0000000000 --- a/web/crux-ui/src/components/projects/versions/images/use-image-editor-state.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { IMAGE_WS_REQUEST_DELAY } from '@app/const' -import { DyoConfirmationModalConfig } from '@app/elements/dyo-modal' -import useConfirmation from '@app/hooks/use-confirmation' -import { useThrottling } from '@app/hooks/use-throttleing' -import { - ContainerConfigData, - DeleteImageMessage, - PatchImageMessage, - VersionImage, - WS_TYPE_DELETE_IMAGE, - WS_TYPE_PATCH_IMAGE, -} from '@app/models' -import WebSocketClientEndpoint from '@app/websockets/websocket-client-endpoint' -import { Translate } from 'next-translate' -import { useRef, useState } from 'react' -import { selectTagsOfImage, VerionState, VersionActions } from '../use-version-state' -import { QA_DIALOG_LABEL_DELETE_IMAGE } from 'quality-assurance' - -export type ImageEditorState = { - image: VersionImage - tags: string[] - parseError: string - deleteModal: DyoConfirmationModalConfig - imageEditorSock: WebSocketClientEndpoint -} - -export type ImageEditorActions = { - selectTag: (tag: string) => void - onPatch: (config: Partial) => void - deleteImage: VoidFunction - setParseError: (error: Error) => void -} - -export type ImageEditorStateOptions = { - image: VersionImage - imagesState: VerionState - imagesActions: VersionActions - sock: WebSocketClientEndpoint - t: Translate -} - -const useImageEditorState = (options: ImageEditorStateOptions): [ImageEditorState, ImageEditorActions] => { - const { image, imagesState, imagesActions, sock, t } = options - - const [deleteModal, confirmDelete] = useConfirmation() - const [parseError, setParseError] = useState(null) - - const patch = useRef>({}) - const throttle = useThrottling(IMAGE_WS_REQUEST_DELAY) - - const tags = selectTagsOfImage(imagesState, image) - - const selectTag = (tag: string) => imagesActions.selectTagForImage(image, tag) - - const onPatch = (config: Partial) => { - setParseError(null) - imagesActions.updateImageConfig(image, config) - - const newPatch = { - ...patch.current, - ...config, - } - patch.current = newPatch - - throttle(() => { - sock.send(WS_TYPE_PATCH_IMAGE, { - id: image.id, - config: patch.current, - } as PatchImageMessage) - - patch.current = {} - }) - } - - const deleteImage = async () => { - const confirmed = await confirmDelete({ - qaLabel: QA_DIALOG_LABEL_DELETE_IMAGE, - title: t('common:areYouSureDeleteName', { name: image.name }), - description: t('common:proceedYouLoseAllDataToName', { name: image.name }), - confirmText: t('common:delete'), - confirmColor: 'bg-error-red', - }) - - if (!confirmed) { - return - } - - sock.send(WS_TYPE_DELETE_IMAGE, { - imageId: image.id, - } as DeleteImageMessage) - } - - return [ - { - image, - tags, - deleteModal, - parseError, - imageEditorSock: sock, - }, - { - selectTag, - onPatch, - deleteImage, - setParseError: (err: Error) => setParseError(err.message), - }, - ] -} - -export default useImageEditorState diff --git a/web/crux-ui/src/components/projects/versions/use-version-state.ts b/web/crux-ui/src/components/projects/versions/use-version-state.ts index f9719e8824..b93c0bc994 100644 --- a/web/crux-ui/src/components/projects/versions/use-version-state.ts +++ b/web/crux-ui/src/components/projects/versions/use-version-state.ts @@ -28,7 +28,7 @@ import { WS_TYPE_GET_IMAGE, WS_TYPE_IMAGE, WS_TYPE_IMAGE_DELETED, - WS_TYPE_IMAGE_SET_TAG, + WS_TYPE_SET_IMAGE_TAG, WS_TYPE_IMAGE_TAG_UPDATED, WS_TYPE_IMAGES_ADDED, WS_TYPE_IMAGES_WERE_REORDERED, @@ -159,7 +159,7 @@ export const useVersionState = (options: VersionStateOptions): [VerionState, Ver onOpen: viewMode !== 'tile' ? null : () => setSaveState('connected'), onClose: viewMode !== 'tile' ? null : () => setSaveState('disconnected'), onSend: message => { - if (message.type === WS_TYPE_IMAGE_SET_TAG || message.type === WS_TYPE_PATCH_CONFIG) { + if (message.type === WS_TYPE_SET_IMAGE_TAG || message.type === WS_TYPE_PATCH_CONFIG) { setSaveState('saving') } }, @@ -317,7 +317,7 @@ export const useVersionState = (options: VersionStateOptions): [VerionState, Ver setVersion({ ...version, images: newImages }) - versionSock.send(WS_TYPE_IMAGE_SET_TAG, { + versionSock.send(WS_TYPE_SET_IMAGE_TAG, { imageId: image.id, tag, } as ImageTagMessage) diff --git a/web/crux-ui/src/components/projects/versions/version-images-section.tsx b/web/crux-ui/src/components/projects/versions/version-images-section.tsx index 532eef107f..f3e5c12184 100644 --- a/web/crux-ui/src/components/projects/versions/version-images-section.tsx +++ b/web/crux-ui/src/components/projects/versions/version-images-section.tsx @@ -19,7 +19,7 @@ const VersionImagesSection = (props: VersionImagesSectionProps) => { return version.images.length ? ( viewMode === 'tile' ? ( - + ) : ( ) diff --git a/web/crux-ui/src/components/projects/versions/version-view-list.tsx b/web/crux-ui/src/components/projects/versions/version-view-list.tsx index d968e3bb62..a2a789df3d 100644 --- a/web/crux-ui/src/components/projects/versions/version-view-list.tsx +++ b/web/crux-ui/src/components/projects/versions/version-view-list.tsx @@ -5,7 +5,7 @@ import DyoModal, { DyoConfirmationModal } from '@app/elements/dyo-modal' import DyoTable, { DyoColumn, sortDate, sortString } from '@app/elements/dyo-table' import useConfirmation from '@app/hooks/use-confirmation' import useTeamRoutes from '@app/hooks/use-team-routes' -import { DeleteImageMessage, VersionImage, WS_TYPE_DELETE_IMAGE } from '@app/models' +import { DeleteImageMessage, containerNameOfImage, VersionImage, WS_TYPE_DELETE_IMAGE } from '@app/models' import { utcDateToLocale } from '@app/utils' import useTranslation from 'next-translate/useTranslation' import { QA_DIALOG_LABEL_DELETE_IMAGE, QA_MODAL_LABEL_IMAGE_TAGS } from 'quality-assurance' @@ -54,7 +54,13 @@ const VersionViewList = (props: VersionViewListProps) => { <> - + { onClick={() => onDelete(it)} />
- - + + )} diff --git a/web/crux-ui/src/components/projects/versions/version-view-tile.tsx b/web/crux-ui/src/components/projects/versions/version-view-tile.tsx index 4212fd6ff7..ea855aab36 100644 --- a/web/crux-ui/src/components/projects/versions/version-view-tile.tsx +++ b/web/crux-ui/src/components/projects/versions/version-view-tile.tsx @@ -1,26 +1,54 @@ import DyoWrap from '@app/elements/dyo-wrap' +import { + ContainerConfigParent, + containerConfigToJsonConfig, + DeleteImageMessage, + mergeJsonWithContainerConfig, + VersionImage, + WS_TYPE_DELETE_IMAGE, +} from '@app/models' import clsx from 'clsx' -import EditImageCard from './images/edit-image-card' -import { VerionState, VersionActions } from './use-version-state' +import EditContainerConfigCard from '../../container-configs/edit-container-config-card' +import { VerionState } from './use-version-state' interface VersionViewTileProps { disabled?: boolean state: VerionState - actions: VersionActions } const VersionViewTile = (props: VersionViewTileProps) => { - const { disabled, state, actions } = props + const { disabled, state } = props + + const onDelete = (it: VersionImage) => { + state.versionSock.send(WS_TYPE_DELETE_IMAGE, { + imageId: it.id, + } as DeleteImageMessage) + } return ( {state.version.images .sort((one, other) => one.order - other.order) - .map((it, index) => ( -
- -
- ))} + .map((it, index) => { + const parent: ContainerConfigParent = { + id: it.id, + name: it.name, + mutable: !disabled, + } + + return ( +
+ onDelete(it)} + convertConfigToJson={containerConfigToJsonConfig} + mergeJsonWithConfig={mergeJsonWithContainerConfig} + /> +
+ ) + })}
) } diff --git a/web/crux-ui/src/components/shared/key-only-input.tsx b/web/crux-ui/src/components/shared/key-only-input.tsx index 72158142c0..b23d9f31bb 100644 --- a/web/crux-ui/src/components/shared/key-only-input.tsx +++ b/web/crux-ui/src/components/shared/key-only-input.tsx @@ -6,7 +6,7 @@ import { useEffect } from 'react' import { v4 as uuid } from 'uuid' import MultiInput from '../editor/multi-input' import { ItemEditorState } from '../editor/use-item-editor-state' -import ConfigSectionLabel from '../projects/versions/images/config/config-section-label' +import ConfigSectionLabel from '../container-configs/config-section-label' const EMPTY_KEY = { id: uuid(), @@ -121,7 +121,7 @@ const KeyOnlyInput = (props: KeyInputProps) => { } const onResetSection = () => { - dispatch(mergeItems(items)) + // dispatch(mergeItems(items)) propsOnResetSection() } diff --git a/web/crux-ui/src/components/shared/key-value-input.tsx b/web/crux-ui/src/components/shared/key-value-input.tsx index 1ef6336132..448bda708f 100644 --- a/web/crux-ui/src/components/shared/key-value-input.tsx +++ b/web/crux-ui/src/components/shared/key-value-input.tsx @@ -1,16 +1,16 @@ import { MessageType } from '@app/elements/dyo-input' import useRepatch from '@app/hooks/use-repatch' +import DyoMessage from '@app/elements/dyo-message' import { UniqueKeyValue } from '@app/models' +import { ErrorWithPath } from '@app/validations' import clsx from 'clsx' import useTranslation from 'next-translate/useTranslation' import { Fragment, HTMLInputTypeAttribute, useEffect } from 'react' import { v4 as uuid } from 'uuid' +import ConfigSectionLabel from '../container-configs/config-section-label' import MultiInput from '../editor/multi-input' import { ItemEditorState } from '../editor/use-item-editor-state' -import ConfigSectionLabel from '../projects/versions/images/config/config-section-label' -import { ErrorWithPath } from '@app/validations' -import DyoMessage from '@app/elements/dyo-message' const EMPTY_KEY_VALUE_PAIR = { id: uuid(), @@ -65,7 +65,7 @@ const mergeItems = return result } -interface KeyValueInputProps { +type KeyValueInputProps = { disabled?: boolean valueDisabled?: boolean className?: string @@ -80,7 +80,7 @@ interface KeyValueInputProps { messageType?: MessageType onChange: (items: UniqueKeyValue[]) => void onResetSection?: VoidFunction - hint?: (key: string) => string | undefined + errors?: Record findErrorMessage?: (index: number) => ErrorWithPath } @@ -100,7 +100,7 @@ const KeyValueInput = (props: KeyValueInputProps) => { messageType, onChange: propsOnChange, onResetSection: propsOnResetSection, - hint, + errors = {}, findErrorMessage, } = props @@ -114,11 +114,11 @@ const KeyValueInput = (props: KeyValueInputProps) => { keyValues.forEach((item, index) => { const error = findErrorMessage?.call(null, index) const keyUniqueErr = result.find(it => it.key === item.key) ? t('keyMustUnique') : null - const hintErr = !keyUniqueErr && hint ? hint(item.key) : null + const itemError = (!keyUniqueErr && errors[item.key]) ?? null result.push({ ...item, - message: keyUniqueErr ?? hintErr ?? error?.message, - messageType: (keyUniqueErr || error ? 'error' : 'info') as MessageType, + message: keyUniqueErr ?? itemError ?? error?.message, + messageType: 'error' as MessageType, }) }) @@ -145,8 +145,6 @@ const KeyValueInput = (props: KeyValueInputProps) => { } const onResetSection = () => { - dispatch(mergeItems([])) - propsOnResetSection() } diff --git a/web/crux-ui/src/components/shared/secret-key-input.tsx b/web/crux-ui/src/components/shared/secret-key-input.tsx index 6bad5c8443..314caf7eb5 100644 --- a/web/crux-ui/src/components/shared/secret-key-input.tsx +++ b/web/crux-ui/src/components/shared/secret-key-input.tsx @@ -7,7 +7,7 @@ import { useEffect } from 'react' import { v4 as uuid } from 'uuid' import MultiInput from '../editor/multi-input' import { ItemEditorState } from '../editor/use-item-editor-state' -import ConfigSectionLabel from '../projects/versions/images/config/config-section-label' +import ConfigSectionLabel from '../container-configs/config-section-label' const EMPTY_KEY = { id: uuid(), diff --git a/web/crux-ui/src/components/shared/secret-key-value-input.tsx b/web/crux-ui/src/components/shared/secret-key-value-input.tsx index d758e0c7d5..98995d5f97 100644 --- a/web/crux-ui/src/components/shared/secret-key-value-input.tsx +++ b/web/crux-ui/src/components/shared/secret-key-value-input.tsx @@ -127,6 +127,7 @@ const SecretKeyValueInput = (props: SecretKeyValueInputProps) => { secretKeys.forEach(item => { const repeating = result.find(it => it.key === item.key) + console.log('status', item.key, secrets) result.push({ ...item, encrypted: item.encrypted ?? false, diff --git a/web/crux-ui/src/const.ts b/web/crux-ui/src/const.ts index d3bb2ef5b7..d8fb4a067b 100644 --- a/web/crux-ui/src/const.ts +++ b/web/crux-ui/src/const.ts @@ -3,12 +3,12 @@ export const HOUR_IN_SECONDS = 3600 export const NODE_SETUP_SCRIPT_TIMEOUT = 600 // 10 min in seconds export const GRPC_STREAM_RECONNECT_TIMEOUT = 5_000 // millis export const IMAGE_FILTER_MIN_LENGTH = 1 // characters -export const IMAGE_WS_REQUEST_DELAY = 500 // millis export const INSTANCE_WS_REQUEST_DELAY = 500 // millis export const DEPLOYMENT_EDIT_WS_REQUEST_DELAY = 500 // millis export const CONFIG_BUNDLE_EDIT_WS_REQUEST_DELAY = 500 // millis export const WS_CONNECT_DELAY_PER_TRY = 5_000 // millis export const WS_MAX_CONNECT_TRY = 20 +export const WS_PATCH_DELAY = 500 // millis export const REGISTRY_HUB_URL = 'hub.docker.com' export const REGISTRY_GITHUB_URL = 'ghcr.io' @@ -34,6 +34,7 @@ export const KRATOS_ERROR_NO_VERIFIED_EMAIL_ADDRESS = 4000010 export const STORAGE_VIEW_MODE = 'view-mode' +export const UID_MIN = -1 export const UID_MAX = 2147483647 export const STORAGE_TEAM_SLUG = 'teamSlug' diff --git a/web/crux-ui/src/elements/dyo-modal.tsx b/web/crux-ui/src/elements/dyo-modal.tsx index 2c209a435e..e88618acf6 100644 --- a/web/crux-ui/src/elements/dyo-modal.tsx +++ b/web/crux-ui/src/elements/dyo-modal.tsx @@ -122,10 +122,10 @@ export const DyoConfirmationModal = (props: DyoConfirmationModalProps) => { onClose(false)} - qaLabel={config?.qaLabel} + qaLabel={config.qaLabel} buttons={ <> onClose(true)}> diff --git a/web/crux-ui/src/elements/dyo-multi-select.tsx b/web/crux-ui/src/elements/dyo-multi-select.tsx index 9c2470345e..ea74d62de0 100644 --- a/web/crux-ui/src/elements/dyo-multi-select.tsx +++ b/web/crux-ui/src/elements/dyo-multi-select.tsx @@ -1,11 +1,16 @@ import clsx from 'clsx' import useTranslation from 'next-translate/useTranslation' -import React, { useEffect, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import DyoCheckbox from './dyo-checkbox' import DyoIcon from './dyo-icon' import DyoMessage from './dyo-message' -export type DyoMultiSelectProps = { +type MutliSelectItem = { + id: string + name: string +} + +export type DyoMultiSelectProps = { className?: string name: string disabled?: boolean @@ -13,13 +18,11 @@ export type DyoMultiSelectProps = { message?: string messageType?: 'error' | 'info' choices: readonly T[] - selection: string[] - idConverter: (choice: T) => string - labelConverter: (choice: T) => string - onSelectionChange: (selection: string[]) => void + selection: T[] + onSelectionChange: (selection: T[]) => void } -const DyoMultiSelect = (props: DyoMultiSelectProps) => { +const DyoMultiSelect = (props: DyoMultiSelectProps) => { const { message, messageType, @@ -27,9 +30,7 @@ const DyoMultiSelect = (props: DyoMultiSelectProps) => { className, grow, choices, - selection, - idConverter, - labelConverter, + selection: propsSelection, onSelectionChange, name, } = props @@ -38,7 +39,12 @@ const DyoMultiSelect = (props: DyoMultiSelectProps) => { const [selectorVisible, setSelectorVisible] = useState(false) - const toggleSelection = (ev: React.MouseEvent | React.ChangeEvent, id: string) => { + const selection = useMemo( + () => choices.filter(choice => propsSelection.find(it => it.id === choice.id)), + [choices, propsSelection], + ) + + const toggleSelection = (ev: React.MouseEvent | React.ChangeEvent, item: T) => { if (disabled) { return } @@ -46,15 +52,10 @@ const DyoMultiSelect = (props: DyoMultiSelectProps) => { ev.preventDefault() ev.stopPropagation() - if (selection.includes(id)) { - const newSelection = [...selection] - const index = selection.indexOf(id) - if (index >= 0) { - newSelection.splice(index, 1) - } - onSelectionChange(newSelection) + if (selection.includes(item)) { + onSelectionChange(selection.filter(it => it !== item)) } else { - onSelectionChange([...selection, id]) + onSelectionChange([...selection, item]) } } @@ -98,17 +99,7 @@ const DyoMultiSelect = (props: DyoMultiSelectProps) => { )} onClick={toggleDropdown} > - +
@@ -117,24 +108,21 @@ const DyoMultiSelect = (props: DyoMultiSelectProps) => { {selectorVisible && (
- {choices.map((it, index) => { - const itemId = idConverter(it) - return ( -
toggleSelection(e, itemId)} - > - toggleSelection(ev, itemId)} - qaLabel={`${name}-${index}`} - /> - -
- ) - })} + {choices.map((it, index) => ( +
toggleSelection(e, it)} + > + toggleSelection(ev, it)} + qaLabel={`${name}-${index}`} + /> + +
+ ))}
)}
diff --git a/web/crux-ui/src/hooks/use-deploy.ts b/web/crux-ui/src/hooks/use-deploy.ts index 6c8b2c4f56..b151730565 100644 --- a/web/crux-ui/src/hooks/use-deploy.ts +++ b/web/crux-ui/src/hooks/use-deploy.ts @@ -1,19 +1,19 @@ import { defaultApiErrorHandler } from '@app/errors' -import { DeploymentDetails, DyoApiError, StartDeployment, mergeConfigs } from '@app/models' +import { DeploymentDetails, DyoApiError, mergeConfigsWithConcreteConfig, StartDeployment } from '@app/models' import { TeamRoutes } from '@app/routes' import { sendForm } from '@app/utils' -import { Translate } from 'next-translate' -import { NextRouter } from 'next/router' -import { DyoConfirmationAction } from './use-confirmation' import { getValidationError, startDeploymentSchema, validationErrorToInstance, yupErrorTranslate, } from '@app/validations' +import { Translate } from 'next-translate' import useTranslation from 'next-translate/useTranslation' -import toast from 'react-hot-toast' +import { NextRouter } from 'next/router' import { QA_DIALOG_LABEL_DEPLOY_PROTECTED } from 'quality-assurance' +import toast from 'react-hot-toast' +import { DyoConfirmationAction } from './use-confirmation' export type UseDeployOptions = { router: NextRouter @@ -57,7 +57,10 @@ export const useDeploy = (opts: UseDeployOptions): UseDeployAction => { ...deployment, instances: selectedInstances.map(it => ({ ...it, - config: mergeConfigs(it.image.config, it.config), + config: { + ...it.config, + ...mergeConfigsWithConcreteConfig([it.image.config], it.config), + }, })), } @@ -73,7 +76,7 @@ export const useDeploy = (opts: UseDeployOptions): UseDeployAction => { ...translatedError, path: intanceIndex !== null - ? selectedInstances[intanceIndex].config?.name ?? selectedInstances[intanceIndex].image.config.name + ? selectedInstances[intanceIndex].config.name ?? selectedInstances[intanceIndex].image.config.name : translatedError.path, }), { @@ -158,7 +161,7 @@ export const useDeploy = (opts: UseDeployOptions): UseDeployAction => { t('errors:validationFailedForInstance', { path: intanceIndex !== null - ? deployment.instances[intanceIndex].config?.name ?? + ? deployment.instances[intanceIndex].config.name ?? deployment.instances[intanceIndex].image.config.name : property, }), diff --git a/web/crux-ui/src/hooks/use-throttleing.ts b/web/crux-ui/src/hooks/use-throttleing.ts index 53c63473e1..5d518932ec 100644 --- a/web/crux-ui/src/hooks/use-throttleing.ts +++ b/web/crux-ui/src/hooks/use-throttleing.ts @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' type Throttling = (action: VoidFunction, immediate?: boolean) => void @@ -28,9 +28,12 @@ export const useThrottling = (delay: number): Throttling => { return () => clearTimeout(schedule.current) }, [action, delay, schedule]) - return (trigger, resetTimer = false) => - setAction({ - trigger, - resetTimer, - }) + return useCallback( + (trigger, resetTimer = false) => + setAction({ + trigger, + resetTimer, + }), + [], + ) } diff --git a/web/crux-ui/src/hooks/use-websocket-translation.ts b/web/crux-ui/src/hooks/use-websocket-translation.ts index c23a0ede1b..7e311f03a4 100644 --- a/web/crux-ui/src/hooks/use-websocket-translation.ts +++ b/web/crux-ui/src/hooks/use-websocket-translation.ts @@ -14,6 +14,7 @@ const useWebsocketTranslate = (t: Translate) => { if (wsContext.client) { wsContext.client.setErrorHandler(defaultWsErrorHandler(t, router)) } + return () => { if (wsContext.client) { wsContext.client.setErrorHandler(defaultWsErrorHandler(defaultTranslate, router)) diff --git a/web/crux-ui/src/models/compose.ts b/web/crux-ui/src/models/compose.ts index 08d32ab59f..3a32f3e687 100644 --- a/web/crux-ui/src/models/compose.ts +++ b/web/crux-ui/src/models/compose.ts @@ -4,9 +4,9 @@ import { CONTAINER_NETWORK_MODE_VALUES, CONTAINER_VOLUME_TYPE_VALUES, ContainerConfigData, - ContainerConfigPort, - ContainerConfigPortRange, - ContainerConfigVolume, + Port, + ContainerPortRange, + Volume, ContainerRestartPolicyType, UniqueKey, UniqueKeyValue, @@ -96,7 +96,7 @@ const splitPortRange = (range: string): [number, number] | null => { return [Number.parseInt(from, 10), Number.parseInt(to, 10)] } -const mapPort = (port: string | number): [ContainerConfigPort, ContainerConfigPortRange] => { +const mapPort = (port: string | number): [Port, ContainerPortRange] => { try { if (typeof port === 'number') { return [ @@ -154,7 +154,7 @@ const mapPort = (port: string | number): [ContainerConfigPort, ContainerConfigPo } } -const mapVolume = (volume: string): ContainerConfigVolume => { +const mapVolume = (volume: string): Volume => { const [name, path, type] = volume.split(':', 3) return { @@ -184,7 +184,7 @@ const mapUser = (user: string): number => { try { return Number.parseInt(user, 10) } catch { - return -1 + return null } } @@ -225,8 +225,8 @@ export const mapComposeServiceToContainerConfig = ( service: ComposeService, serviceKey: string, ): ContainerConfigData => { - const ports: ContainerConfigPort[] = [] - const portRanges: ContainerConfigPortRange[] = [] + const ports: Port[] = [] + const portRanges: ContainerPortRange[] = [] service.ports?.forEach(it => { const [port, portRange] = mapPort(it) if (port) { diff --git a/web/crux-ui/src/models/config-bundle.ts b/web/crux-ui/src/models/config-bundle.ts index 627e1c98f9..26ef53588b 100644 --- a/web/crux-ui/src/models/config-bundle.ts +++ b/web/crux-ui/src/models/config-bundle.ts @@ -1,4 +1,4 @@ -import { ContainerConfigData, UniqueKeyValue } from './container' +import { ContainerConfig, ContainerConfigData } from './container' export type BasicConfigBundle = { id: string @@ -7,10 +7,11 @@ export type BasicConfigBundle = { export type ConfigBundle = BasicConfigBundle & { description?: string + configId: string } -export type ConfigBundleDetails = ConfigBundle & { - config: ContainerConfigData +export type ConfigBundleDetails = Omit & { + config: ContainerConfig } export type CreateConfigBundle = { @@ -24,8 +25,7 @@ export type PatchConfigBundle = { config?: ContainerConfigData } -export type UpdateConfigBundle = CreateConfigBundle & { - environment: UniqueKeyValue[] -} - -export type ConfigBundleOption = BasicConfigBundle +export const detailsToConfigBundle = (bundle: ConfigBundleDetails): ConfigBundle => ({ + ...bundle, + configId: bundle.config.id, +}) diff --git a/web/crux-ui/src/models/container-config.ts b/web/crux-ui/src/models/container-config.ts index ac8b56de60..2fcdb358ca 100644 --- a/web/crux-ui/src/models/container-config.ts +++ b/web/crux-ui/src/models/container-config.ts @@ -1,13 +1,45 @@ -import { ContainerConfigData } from './container' -import { ImageConfigProperty } from './image' +import { ConfigBundleDetails } from './config-bundle' +import { ContainerConfig, ContainerConfigData, ContainerConfigKey } from './container' +import { DeploymentWithConfig } from './deployment' +import { VersionImage } from './image' +import { BasicProject } from './project' +import { BasicVersion } from './version' +export type ContainerConfigRelations = { + project?: BasicProject + version?: BasicVersion + image?: VersionImage + deployment?: DeploymentWithConfig + configBundle?: ConfigBundleDetails +} + +export type ContainerConfigParent = { + id: string + name: string + mutable: boolean +} + +export type ContainerConfigDetails = ContainerConfig & { + parent: ContainerConfigParent + updatedAt?: string + updatedBy?: string +} + +// ws export const WS_TYPE_PATCH_CONFIG = 'patch-config' export type PatchConfigMessage = { config?: ContainerConfigData - resetSection?: ImageConfigProperty + resetSection?: ContainerConfigKey } export const WS_TYPE_CONFIG_UPDATED = 'config-updated' export type ConfigUpdatedMessage = ContainerConfigData & { id: string } + +export const WS_TYPE_GET_CONFIG_SECRETS = 'get-config-secrets' +export const WS_TYPE_CONFIG_SECRETS = 'config-secrets' +export type ConfigSecretsMessage = { + keys: string[] + publicKey: string +} diff --git a/web/crux-ui/src/models/container-conflict.ts b/web/crux-ui/src/models/container-conflict.ts new file mode 100644 index 0000000000..039d232d06 --- /dev/null +++ b/web/crux-ui/src/models/container-conflict.ts @@ -0,0 +1,676 @@ +import { + ConcreteContainerConfigData, + ContainerConfigData, + ContainerConfigDataWithId, + ContainerPortRange, + Log, + Marker, + Port, + PortRange, + ResourceConfig, + UniqueKeyValue, + Volume, +} from './container' + +export type ConflictedUniqueItem = { + key: string + configIds: string[] +} + +export type ConflictedPort = { + internal: number + configIds: string[] +} + +export type ConflictedPortRange = { + range: PortRange + configIds: string[] +} + +export type ConflictedLog = { + driver?: string[] + options?: ConflictedUniqueItem[] +} + +export type ConflictedResoureConfig = { + limits?: string[] + requests?: string[] +} + +export type ConflictedMarker = { + service?: ConflictedUniqueItem[] + deployment?: ConflictedUniqueItem[] + ingress?: ConflictedUniqueItem[] +} + +type ConflictedLogKeys = { + driver?: boolean + options?: string[] +} + +type ConflictedResoureConfigKeys = Partial> +type ConflictedMarkerKeys = Partial> + +// config ids where the given property is present +export type ConflictedContainerConfigData = { + // common + name?: string[] + environment?: ConflictedUniqueItem[] + routing?: string[] + expose?: string[] + user?: string[] + workingDirectory?: string[] + tty?: string[] + configContainer?: string[] + ports?: ConflictedPort[] + portRanges?: ConflictedPortRange[] + volumes?: ConflictedUniqueItem[] + initContainers?: string[] + capabilities?: ConflictedUniqueItem[] + storage?: string[] + + // dagent + logConfig?: string[] + restartPolicy?: string[] + networkMode?: string[] + dockerLabels?: ConflictedUniqueItem[] + expectedState?: string[] + + // crane + deploymentStrategy?: string[] + proxyHeaders?: string[] + useLoadBalancer?: string[] + extraLBAnnotations?: ConflictedUniqueItem[] + healthCheckConfig?: string[] + resourceConfig?: string[] + annotations?: ConflictedMarker + labels?: ConflictedMarker + metrics?: string[] +} + +export const rangesOverlap = (one: PortRange, other: PortRange): boolean => one.from <= other.to && other.from <= one.to +export const rangesAreEqual = (one: PortRange, other: PortRange): boolean => + one.from === other.from && one.to === other.to + +const appendConflict = (conflicts: string[], oneId: string, otherId: string): string[] => { + if (!conflicts) { + return [oneId, otherId] + } + + if (!conflicts.includes(oneId)) { + conflicts.push(oneId) + } + + if (!conflicts.includes(otherId)) { + conflicts.push(otherId) + } + + return conflicts +} + +const appendUniqueItemConflicts = ( + conflicts: ConflictedUniqueItem[], + oneId: string, + otherId: string, + keys: string[], +): ConflictedUniqueItem[] => { + if (!conflicts) { + return keys.map(it => ({ + key: it, + configIds: [oneId, otherId], + })) + } + + keys.forEach(key => { + let conflict = conflicts.find(it => it.key === key) + if (!conflict) { + conflict = { + key, + configIds: null, + } + + conflicts.push(conflict) + } + + conflict.configIds = appendConflict(conflict.configIds, oneId, otherId) + }) + + return conflicts +} + +const appendPortConflicts = ( + conflicts: ConflictedPort[], + oneId: string, + otherId: string, + internalPorts: number[], +): ConflictedPort[] => { + if (!conflicts) { + return internalPorts.map(it => ({ + internal: it, + configIds: [oneId, otherId], + })) + } + + internalPorts.forEach(internalPort => { + let conflict = conflicts.find(it => internalPort === it.internal) + if (!conflict) { + conflict = { + internal: internalPort, + configIds: null, + } + + conflicts.push(conflict) + } + + conflict.configIds = appendConflict(conflict.configIds, oneId, otherId) + }) + + return conflicts +} + +const appendPortRangeConflicts = ( + conflicts: ConflictedPortRange[], + oneId: string, + otherId: string, + ranges: PortRange[], +): ConflictedPortRange[] => { + if (!conflicts) { + return ranges.map(it => ({ + range: it, + configIds: [oneId, otherId], + })) + } + + ranges.forEach(range => { + let conflict = conflicts.find(it => rangesAreEqual(it.range, range)) + if (!conflict) { + conflict = { + range, + configIds: null, + } + + conflicts.push(conflict) + } + + conflict.configIds = appendConflict(conflict.configIds, oneId, otherId) + }) + + return conflicts +} + +const appendLogConflict = ( + conflicts: ConflictedLog, + oneId: string, + otherId: string, + keys: ConflictedLogKeys, +): ConflictedLog => { + if (!conflicts) { + conflicts = {} + } + + if (keys.driver) { + conflicts.driver = appendConflict(conflicts.driver, oneId, otherId) + } + + if (keys.options) { + conflicts.options = appendUniqueItemConflicts(conflicts.options, oneId, otherId, keys.options) + } + + return conflicts +} + +const appendResourceConfigConflict = ( + conflicts: ConflictedResoureConfig, + oneId: string, + otherId: string, + keys: ConflictedResoureConfigKeys, +): ConflictedResoureConfig => { + if (!conflicts) { + conflicts = {} + } + + if (keys.limits) { + conflicts.limits = appendConflict(conflicts.limits, oneId, otherId) + } + + if (keys.requests) { + conflicts.requests = appendConflict(conflicts.requests, oneId, otherId) + } + + return conflicts +} + +const appendMarkerConflict = ( + conflicts: ConflictedMarker, + oneId: string, + otherId: string, + keys: ConflictedMarkerKeys, +): ConflictedMarker => { + if (!conflicts) { + conflicts = {} + } + + if (keys.deployment) { + conflicts.deployment = appendUniqueItemConflicts(conflicts.deployment, oneId, otherId, keys.deployment) + } + + if (keys.ingress) { + conflicts.ingress = appendUniqueItemConflicts(conflicts.ingress, oneId, otherId, keys.ingress) + } + + if (keys.service) { + conflicts.service = appendUniqueItemConflicts(conflicts.service, oneId, otherId, keys.service) + } + + return conflicts +} + +const stringsConflict = (one: string, other: string): boolean => { + if (typeof one !== 'string' || typeof other !== 'string') { + // one of them are null or uninterpretable + return false + } + + return one !== other +} + +const booleansConflict = (one: boolean, other: boolean): boolean => { + if (typeof one !== 'boolean' || typeof other !== 'boolean') { + // one of them are null or uninterpretable + return false + } + + return one !== other +} + +const numbersConflict = (one: number, other: number): boolean => { + if (typeof one !== 'number' || typeof other !== 'number') { + // some of them are null or uninterpretable + return false + } + + return one !== other +} + +const objectsConflict = (one: object, other: object): boolean => { + if (typeof one !== 'object' || typeof other !== 'object') { + // some of them are null or uninterpretable + return false + } + + return JSON.stringify(one) !== JSON.stringify(other) +} + +// returns the conflicting keys +const uniqueKeyValuesConflict = (one: UniqueKeyValue[], other: UniqueKeyValue[]): string[] | null => { + if (!one || !other) { + return null + } + + const conflicts = one + .filter(item => { + const otherItem = other.find(it => it.key === item.key) + if (!otherItem) { + return false + } + + return item.value !== otherItem.value + }) + .map(it => it.key) + + if (conflicts.length < 1) { + return null + } + + return conflicts +} + +// returns the conflicting internal ports +const portsConflict = (one: Port[], other: Port[]): number[] | null => { + if (!one || !other) { + return null + } + + const conflicts = one.filter(item => other.find(it => it.internal === item.internal)).map(it => it.internal) + + if (conflicts.length < 1) { + return null + } + + return conflicts +} + +// returns the conflicting internal ranges +const portRangesConflict = (one: ContainerPortRange[], other: ContainerPortRange[]): PortRange[] | null => { + if (!one || !other) { + return null + } + + const conflicts = one + .filter(item => + other.find(it => rangesOverlap(item.internal, it.internal) || rangesOverlap(item.external, item.internal)), + ) + .map(it => it.internal) + + if (conflicts.length < 1) { + return null + } + + return conflicts +} + +// returns the conflicting paths +const volumesConflict = (one: Volume[], other: Volume[]): string[] | null => { + if (!one || !other) { + return null + } + + const conflicts = one + .filter(item => { + const otherItem = other.find(it => it.path === item.path) + + return objectsConflict(item, otherItem) + }) + .map(it => it.path) + + if (conflicts.length < 1) { + return null + } + + return conflicts +} + +const logsConflict = (one: Log, other: Log): ConflictedLogKeys | null => { + if (!one || !other) { + return null + } + + const driver = stringsConflict(one.driver, other.driver) + const options = uniqueKeyValuesConflict(one.options, other.options) + + const conflicts: ConflictedLogKeys = {} + + if (driver) { + conflicts.driver = driver + } + + if (options) { + conflicts.options = options + } + + if (Object.keys(conflicts).length < 1) { + return null + } + + return conflicts +} + +const resoureConfigsConflict = (one: ResourceConfig, other: ResourceConfig): ConflictedResoureConfigKeys | null => { + if (!one || !other) { + return null + } + + const conflicts: ConflictedResoureConfigKeys = { + limits: objectsConflict(one.limits, other.limits), + requests: objectsConflict(one.requests, other.requests), + } + + if (!Object.values(conflicts).find(it => it)) { + // no conflicts + return null + } + + return conflicts +} + +const markersConflict = (one: Marker, other: Marker): ConflictedMarkerKeys | null => { + if (!one || !other) { + return null + } + + const deployment = uniqueKeyValuesConflict(one.deployment, other.deployment) + const ingress = uniqueKeyValuesConflict(one.ingress, other.ingress) + const service = uniqueKeyValuesConflict(one.service, other.service) + + const conflicts: ConflictedMarkerKeys = {} + + if (deployment) { + conflicts.deployment = deployment + } + + if (ingress) { + conflicts.ingress = ingress + } + + if (service) { + conflicts.service = service + } + + if (Object.keys(conflicts).length < 1) { + return null + } + + return conflicts +} + +const collectConflicts = ( + conflicts: ConflictedContainerConfigData, + one: ContainerConfigDataWithId, + other: ContainerConfigDataWithId, +) => { + const checkStringConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as string + const otherValue = other[key] as string + + if (stringsConflict(oneValue, otherValue)) { + conflicts[key] = appendConflict(conflicts[key], one.id, other.id) + } + } + + const checkUniqueKeyValuesConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as UniqueKeyValue[] + const otherValue = other[key] as UniqueKeyValue[] + + const uniqueKeyValueConflicts = uniqueKeyValuesConflict(oneValue, otherValue) + if (uniqueKeyValueConflicts) { + conflicts[key] = appendUniqueItemConflicts(conflicts[key], one.id, other.id, uniqueKeyValueConflicts) + } + } + + const checkBooleanConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as boolean + const otherValue = other[key] as boolean + + if (booleansConflict(oneValue, otherValue)) { + conflicts[key] = appendConflict(conflicts[key], one.id, other.id) + } + } + + const checkNumberConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as number + const otherValue = other[key] as number + + if (numbersConflict(oneValue, otherValue)) { + conflicts[key] = appendConflict(conflicts[key], one.id, other.id) + } + } + + const checkObjectConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as object + const otherValue = other[key] as object + + if (objectsConflict(oneValue, otherValue)) { + conflicts[key] = appendConflict(conflicts[key], one.id, other.id) + } + } + + const checkPortsConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as Port[] + const otherValue = other[key] as Port[] + + const portsConflicts = portsConflict(oneValue, otherValue) + if (portsConflicts) { + conflicts[key] = appendPortConflicts(conflicts[key], one.id, other.id, portsConflicts) + } + } + + const checkPortRangesConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as ContainerPortRange[] + const otherValue = other[key] as ContainerPortRange[] + + const portRangesConflicts = portRangesConflict(oneValue, otherValue) + if (portRangesConflicts) { + conflicts[key] = appendPortRangeConflicts(conflicts[key], one.id, other.id, portRangesConflicts) + } + } + + const checkVolumesConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as Volume[] + const otherValue = other[key] as Volume[] + + const volumeConflicts = volumesConflict(oneValue, otherValue) + if (volumeConflicts) { + conflicts[key] = appendUniqueItemConflicts(conflicts[key], one.id, other.id, volumeConflicts) + } + } + + const checkStorageConflict = () => { + if (objectsConflict(one, other)) { + conflicts.storage = appendConflict(conflicts.storage, one.id, other.id) + } + } + + const checkLogsConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as Log + const otherValue = other[key] as Log + + const logConflicts = logsConflict(oneValue, otherValue) + if (logConflicts) { + conflicts[key] = appendLogConflict(conflicts[key], one.id, other.id, logConflicts) + } + } + + const checkResourceConfigConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as ResourceConfig + const otherValue = other[key] as ResourceConfig + + const resourceConfigConflicts = resoureConfigsConflict(oneValue, otherValue) + if (resourceConfigConflicts) { + conflicts[key] = appendResourceConfigConflict(conflicts[key], one.id, other.id, resourceConfigConflicts) + } + } + + const checkMarkerConflict = (key: keyof ContainerConfigData) => { + const oneValue = one[key] as Marker + const otherValue = other[key] as Marker + + const markerConflicts = markersConflict(oneValue, otherValue) + if (markerConflicts) { + conflicts[key] = appendMarkerConflict(conflicts[key], one.id, other.id, markerConflicts) + } + } + + // common + checkStringConflict('name') + checkUniqueKeyValuesConflict('environment') + // 'secrets' are keys only so duplicates are allowed + checkObjectConflict('routing') + checkStringConflict('expose') + checkNumberConflict('user') + checkStringConflict('workingDirectory') + checkBooleanConflict('tty') + checkObjectConflict('configContainer') + checkPortsConflict('ports') + checkPortRangesConflict('portRanges') + checkVolumesConflict('volumes') + // 'commands' are keys only so duplicates are allowed + // 'args' are keys only so duplicates are allowed + checkObjectConflict('initContainers') // TODO (@m8vago) compare them correctly after the init container rework + checkUniqueKeyValuesConflict('capabilities') + checkStorageConflict() + + // dagent + checkLogsConflict('logConfig') + checkStringConflict('restartPolicy') + checkStringConflict('networkMode') + // 'networks' are keys only so duplicates are allowed + checkUniqueKeyValuesConflict('dockerLabels') + checkStringConflict('expectedState') + + // crane + checkStringConflict('deploymentStrategy') + // 'customHeaders' are keys only so duplicates are allowed + checkBooleanConflict('proxyHeaders') + checkBooleanConflict('useLoadBalancer') + checkUniqueKeyValuesConflict('extraLBAnnotations') + checkObjectConflict('healthCheckConfig') + checkResourceConfigConflict('resourceConfig') + checkMarkerConflict('annotations') + checkMarkerConflict('labels') + checkObjectConflict('metrics') + + if (Object.keys(conflicts).length < 1) { + return null + } + + return conflicts +} + +type ContainerConfigDataProperty = keyof ContainerConfigData +export const checkForConflicts = ( + configs: ContainerConfigDataWithId[], + definedKeys: ContainerConfigDataProperty[] = [], +): ConflictedContainerConfigData | null => { + configs = configs.map(conf => { + const newConf: ContainerConfigDataWithId = { + ...conf, + } + + Object.keys(conf).forEach(it => { + const prop = it as ContainerConfigDataProperty + if (!definedKeys.includes(prop)) { + return + } + + delete newConf[prop] + }) + + return newConf + }) + + const conflicts: ConflictedContainerConfigData = {} + + configs.forEach(one => { + const others = configs.filter(it => it !== one) + + others.forEach(other => collectConflicts(conflicts, one, other)) + }) + + if (Object.keys(conflicts).length < 1) { + return null + } + + return conflicts +} + +const UNINTERESTED_KEYS = ['id', 'type', 'updatedAt', 'updatedBy', 'secrets'] +export const getConflictsForConcreteConfig = ( + configs: ContainerConfigDataWithId[], + concreteConfig: ConcreteContainerConfigData, +): ConflictedContainerConfigData | null => + checkForConflicts( + configs, + Object.entries(concreteConfig) + .filter(entry => { + const [key, value] = entry + if (UNINTERESTED_KEYS.includes(key)) { + return false + } + + return typeof value !== 'undefined' && value !== null + }) + .map(entry => { + const [key] = entry + return key + }) as ContainerConfigDataProperty[], + ) diff --git a/web/crux-ui/src/models/container-errors.ts b/web/crux-ui/src/models/container-errors.ts new file mode 100644 index 0000000000..6fe17da377 --- /dev/null +++ b/web/crux-ui/src/models/container-errors.ts @@ -0,0 +1,194 @@ +import { Translate } from 'next-translate' +import { PortRange } from './container' +import { + ConflictedContainerConfigData, + ConflictedMarker, + ConflictedPort, + ConflictedPortRange, + ConflictedUniqueItem, +} from './container-conflict' + +export type UniqueItemErrors = Record +export type PortErrors = Record + +// from-to string keys +export type PortRangeErrors = Record + +export type LogError = { + driver?: string + options?: UniqueItemErrors +} + +export type ResourceConfigError = { + limits?: string + requests?: string +} + +export type MarkerError = { + service?: UniqueItemErrors + deployment?: UniqueItemErrors + ingress?: UniqueItemErrors +} + +// config ids where the given property is present +export type ContainerConfigErrors = { + // common + name?: string + environment?: UniqueItemErrors + routing?: string + expose?: string + user?: string + workingDirectory?: string + tty?: string + configContainer?: string + ports?: PortErrors + portRanges?: PortRangeErrors + volumes?: UniqueItemErrors + initContainers?: string + capabilities?: UniqueItemErrors + storage?: string + + // dagent + logConfig?: string + restartPolicy?: string + networkMode?: string + dockerLabels?: UniqueItemErrors + expectedState?: string + + // crane + deploymentStrategy?: string + proxyHeaders?: string + useLoadBalancer?: string + extraLBAnnotations?: UniqueItemErrors + healthCheckConfig?: string + resourceConfig?: string + annotations?: MarkerError + labels?: MarkerError + metrics?: string +} + +export const portRangeToString = (range: PortRange) => `${range.from}-${range.to}` +export const portRangeFromString = (range: string): PortRange => { + const [from, to] = range.split('-') + return { + from: Number.parseInt(from, 10), + to: Number.parseInt(to, 10), + } +} + +export const conflictsToError = ( + t: Translate, + configNames: Record, + conflicts: ConflictedContainerConfigData, +): ContainerConfigErrors | null => { + if (!conflicts) { + return null + } + + const errors: ContainerConfigErrors = {} + + const idsToConfigNames = (ids: string[]): string => + ids + .map(it => configNames[it]) + .filter(it => !!it) + .join(', ') + + const uniqueItemConflictsToError = (conflict: ConflictedUniqueItem[]): UniqueItemErrors => + conflict.reduce((result, it) => { + result[it.key] = t('container:errors.ambiguousInConfigs', { configs: idsToConfigNames(it.configIds) }) + return result + }, {}) + + const checkStringError = (key: keyof ConflictedContainerConfigData) => { + if (key in conflicts) { + const conflict = conflicts[key] as string[] + errors[key as string] = t('container:errors.ambiguousKeyInConfigs', { key, configs: idsToConfigNames(conflict) }) + } + } + + const checkUniqueErrors = (key: keyof ConflictedContainerConfigData) => { + if (key in conflicts) { + const conflict = conflicts[key] as ConflictedUniqueItem[] + const itemConflicts = uniqueItemConflictsToError(conflict) + + errors[key as string] = itemConflicts + } + } + + const checkPortErrors = (key: keyof ConflictedContainerConfigData) => { + if (key in conflicts) { + const conflict = conflicts[key] as ConflictedPort[] + const portConflicts = conflict.reduce((result, it) => { + result[it.internal] = t('container:errors.ambiguousInConfigs', { configs: idsToConfigNames(it.configIds) }) + return result + }, {}) + errors[key as string] = portConflicts + } + } + + const checkPortRangeErrors = (key: keyof ConflictedContainerConfigData) => { + if (key in conflicts) { + const conflict = conflicts[key] as ConflictedPortRange[] + const portRangeConflicts = conflict.reduce((result, it) => { + const rangeKey = portRangeToString(it.range) + result[rangeKey] = t('container:errors.ambiguousInConfigs', { configs: idsToConfigNames(it.configIds) }) + return result + }, {}) + errors[key as string] = portRangeConflicts + } + } + + const checkMarkerErrors = (key: keyof ConflictedContainerConfigData) => { + if (key in conflicts) { + const conflict = conflicts[key] as ConflictedMarker + + const err: MarkerError = { + service: conflict.deployment ? uniqueItemConflictsToError(conflict.service) : null, + deployment: conflict.deployment ? uniqueItemConflictsToError(conflict.deployment) : null, + ingress: conflict.deployment ? uniqueItemConflictsToError(conflict.ingress) : null, + } + + errors[key as string] = err + } + } + + checkStringError('name') + checkUniqueErrors('environment') + + checkStringError('routing') + checkStringError('expose') + checkStringError('user') + checkStringError('workingDirectory') + checkStringError('tty') + checkStringError('configContainer') + checkPortErrors('ports') + checkPortRangeErrors('portRanges') + checkUniqueErrors('volumes') + checkStringError('initContainers') + checkStringError('capabilities') + checkStringError('storage') + + // dagent + checkStringError('logConfig') + checkStringError('restartPolicy') + checkStringError('networkMode') + checkUniqueErrors('dockerLabels') + checkStringError('expectedState') + + // crane + checkStringError('deploymentStrategy') + checkStringError('proxyHeaders') + checkStringError('useLoadBalancer') + checkUniqueErrors('extraLBAnnotations') + checkStringError('healthCheckConfig') + checkStringError('resourceConfig') + checkMarkerErrors('annotations') + checkMarkerErrors('labels') + checkStringError('metrics') + + if (Object.keys(errors).length < 1) { + return null + } + + return errors +} diff --git a/web/crux-ui/src/models/container-merge.ts b/web/crux-ui/src/models/container-merge.ts new file mode 100644 index 0000000000..5d7052faa1 --- /dev/null +++ b/web/crux-ui/src/models/container-merge.ts @@ -0,0 +1,240 @@ +import { + ConcreteContainerConfigData, + ContainerConfigData, + ContainerPortRange, + Marker, + Port, + UniqueKey, + UniqueSecretKey, + UniqueSecretKeyValue, + Volume, +} from './container' +import { rangesOverlap } from './container-conflict' + +const mergeNumber = (strong: number, weak: number): number => { + if (typeof strong === 'number') { + return strong + } + + if (typeof weak === 'number') { + return weak + } + + return null +} + +const mergeBoolean = (strong: boolean, weak: boolean): boolean => { + if (typeof strong === 'boolean') { + return strong + } + + if (typeof weak === 'boolean') { + return weak + } + + return null +} + +export const mergeMarkers = (strong: Marker, weak: Marker): Marker => { + if (!strong) { + return weak ?? null + } + + if (!weak) { + return strong + } + + return { + deployment: strong.deployment ?? weak.deployment ?? [], + ingress: strong.ingress ?? weak.ingress ?? [], + service: strong.service ?? weak.service ?? [], + } +} + +const mergeSecretKeys = (one: UniqueSecretKey[], other: UniqueSecretKey[]): UniqueSecretKey[] => { + if (!one) { + return other + } + + if (!other) { + return one + } + + return [...one, ...other.filter(it => !one.includes(it))] +} + +export const mergeSecrets = (strong: UniqueSecretKeyValue[], weak: UniqueSecretKey[]): UniqueSecretKeyValue[] => { + weak = weak ?? [] + strong = strong ?? [] + + const overriddenIds: Set = new Set(strong?.map(it => it.id)) + + const missing: UniqueSecretKeyValue[] = weak + .filter(it => !overriddenIds.has(it.id)) + .map(it => ({ + ...it, + value: '', + encrypted: false, + publicKey: null, + })) + + return [...missing, ...strong] +} + +export const mergeConfigs = (strong: ContainerConfigData, weak: ContainerConfigData): ContainerConfigData => ({ + // common + name: strong.name ?? weak.name, + environment: strong.environment ?? weak.environment, + secrets: mergeSecretKeys(strong.secrets, weak.secrets), + user: mergeNumber(strong.user, weak.user), + workingDirectory: strong.workingDirectory ?? weak.workingDirectory, + tty: mergeBoolean(strong.tty, weak.tty), + portRanges: strong.portRanges ?? weak.portRanges, + args: strong.args ?? weak.args, + commands: strong.commands ?? weak.commands, + expose: strong.expose ?? weak.expose, + configContainer: strong.configContainer ?? weak.configContainer, + routing: strong.routing ?? weak.routing, + volumes: strong.volumes ?? weak.volumes, + initContainers: strong.initContainers ?? weak.initContainers, + capabilities: [], // TODO (@m8vago, @nandor-magyar): capabilities feature is still missing + ports: strong.ports ?? weak.ports, + storage: strong.storage ?? weak.storage, + + // crane + customHeaders: strong.customHeaders ?? weak.customHeaders, + proxyHeaders: mergeBoolean(strong.proxyHeaders, weak.proxyHeaders), + extraLBAnnotations: strong.extraLBAnnotations ?? weak.extraLBAnnotations, + healthCheckConfig: strong.healthCheckConfig ?? weak.healthCheckConfig, + resourceConfig: strong.resourceConfig ?? weak.resourceConfig, + useLoadBalancer: mergeBoolean(strong.useLoadBalancer, weak.useLoadBalancer), + deploymentStrategy: strong.deploymentStrategy ?? weak.deploymentStrategy, + labels: mergeMarkers(strong.labels, weak.labels), + annotations: mergeMarkers(strong.annotations, weak.annotations), + metrics: strong.metrics ?? weak.metrics, + + // dagent + logConfig: strong.logConfig ?? weak.logConfig, + networkMode: strong.networkMode ?? weak.networkMode, + restartPolicy: strong.restartPolicy ?? weak.restartPolicy, + networks: strong.networks ?? weak.networks, + dockerLabels: strong.dockerLabels ?? weak.dockerLabels, + expectedState: strong.expectedState ?? weak.expectedState, +}) + +export const squashConfigs = (configs: ContainerConfigData[]): ContainerConfigData => + configs.reduce((result, conf) => mergeConfigs(conf, result), {} as ContainerConfigData) + +// this assumes that the concrete config takes care of any conflict between the other configs +export const mergeConfigsWithConcreteConfig = ( + configs: ContainerConfigData[], + concrete: ConcreteContainerConfigData, +): ConcreteContainerConfigData => { + const squashed = squashConfigs(configs.filter(it => !!it)) + concrete = concrete ?? {} + + const baseConfig = mergeConfigs(concrete, squashed) + + return { + ...baseConfig, + secrets: mergeSecrets(concrete.secrets, squashed.secrets), + } +} + +const mergeUniqueKeys = (strong: T[], weak: T[]): T[] => { + if (!strong) { + return weak ?? null + } + + if (!weak) { + return strong + } + + const missing = weak.filter(w => !strong.find(it => it.key === w.key)) + return [...strong, ...missing] +} + +const mergePorts = (strong: Port[], weak: Port[]): Port[] => { + if (!strong) { + return weak ?? null + } + + if (!weak) { + return strong + } + + const missing = weak.filter(w => !strong.find(it => it.internal === w.internal)) + return [...strong, ...missing] +} + +const mergePortRanges = (strong: ContainerPortRange[], weak: ContainerPortRange[]): ContainerPortRange[] => { + if (!strong) { + return weak ?? null + } + + if (!weak) { + return strong + } + + const missing = weak.filter( + w => !strong.find(it => rangesOverlap(w.internal, it.internal) || rangesOverlap(w.external, it.external)), + ) + return [...strong, ...missing] +} + +const mergeVolumes = (strong: Volume[], weak: Volume[]): Volume[] => { + if (!strong) { + return weak ?? null + } + + if (!weak) { + return strong + } + + const missing = weak.filter(w => !strong.find(it => it.path === w.path || it.name === w.path)) + return [...strong, ...missing] +} + +export const mergeInstanceConfigWithDeploymentConfig = ( + deployment: ConcreteContainerConfigData, + instance: ConcreteContainerConfigData, +): ConcreteContainerConfigData => ({ + // common + name: instance.name ?? deployment.name ?? null, + environment: mergeUniqueKeys(instance.environment, deployment.environment), + secrets: mergeUniqueKeys(instance.secrets, deployment.secrets), + user: mergeNumber(instance.user, deployment.user), + workingDirectory: instance.workingDirectory ?? deployment.workingDirectory ?? null, + tty: mergeBoolean(instance.tty, deployment.tty), + ports: mergePorts(instance.ports, deployment.ports), + portRanges: mergePortRanges(instance.portRanges, deployment.portRanges), + args: mergeUniqueKeys(instance.args, deployment.args), + commands: mergeUniqueKeys(instance.commands, deployment.commands), + expose: instance.expose ?? deployment.expose ?? null, + configContainer: instance.configContainer ?? deployment.configContainer ?? null, + routing: instance.routing ?? deployment.routing ?? null, + volumes: mergeVolumes(instance.volumes, deployment.volumes), + initContainers: instance.initContainers ?? deployment.initContainers ?? null, // TODO (@m8vago): merge them correctly after the init container rework + capabilities: [], // TODO (@m8vago, @nandor-magyar): capabilities feature is still missing + storage: instance.storage ?? deployment.storage, + + // crane + customHeaders: mergeUniqueKeys(deployment.customHeaders, instance.customHeaders), + proxyHeaders: mergeBoolean(instance.proxyHeaders, deployment.proxyHeaders), + extraLBAnnotations: mergeUniqueKeys(instance.extraLBAnnotations, deployment.extraLBAnnotations), + healthCheckConfig: instance.healthCheckConfig ?? deployment.healthCheckConfig ?? null, + resourceConfig: instance.resourceConfig ?? deployment.resourceConfig ?? null, + useLoadBalancer: mergeBoolean(instance.useLoadBalancer, deployment.useLoadBalancer), + deploymentStrategy: instance.deploymentStrategy ?? deployment.deploymentStrategy ?? null, + labels: mergeMarkers(instance.labels, deployment.labels), + annotations: mergeMarkers(instance.annotations, deployment.annotations), + metrics: instance.metrics ?? deployment.metrics ?? null, + + // dagent + logConfig: instance.logConfig ?? deployment.logConfig ?? null, + networkMode: instance.networkMode ?? deployment.networkMode ?? null, + restartPolicy: instance.restartPolicy ?? deployment.restartPolicy ?? null, + networks: mergeUniqueKeys(instance.networks, deployment.networks), + dockerLabels: mergeUniqueKeys(instance.dockerLabels, deployment.dockerLabels), + expectedState: instance.expectedState ?? deployment.expectedState ?? null, +}) diff --git a/web/crux-ui/src/models/container.ts b/web/crux-ui/src/models/container.ts index 6a86b917e0..f9546ba069 100644 --- a/web/crux-ui/src/models/container.ts +++ b/web/crux-ui/src/models/container.ts @@ -1,4 +1,5 @@ import { v4 as uuid } from 'uuid' +import { imageName } from './registry' export const CONTAINER_STATE_VALUES = ['running', 'waiting', 'exited', 'removed'] as const export type ContainerState = (typeof CONTAINER_STATE_VALUES)[number] @@ -57,7 +58,7 @@ export type UniqueSecretKeyValue = UniqueSecretKey & encrypted: boolean } -export type ContainerConfigPort = { +export type Port = { id: string internal: number external?: number @@ -68,7 +69,7 @@ export type PortRange = { to: number } -export type ContainerConfigPortRange = { +export type ContainerPortRange = { id: string internal: PortRange external: PortRange @@ -99,7 +100,7 @@ export type ContainerConfigRouting = { port?: number } -export type ContainerConfigVolume = { +export type Volume = { id: string name: string path: string @@ -125,7 +126,7 @@ export const CONTAINER_LOG_DRIVER_VALUES = [ ] as const export type ContainerLogDriverType = (typeof CONTAINER_LOG_DRIVER_VALUES)[number] -export type ContainerConfigLog = { +export type Log = { driver: ContainerLogDriverType options: UniqueKeyValue[] } @@ -142,7 +143,7 @@ export type ContainerConfigResource = { memory?: string } -export type ContainerConfigResourceConfig = { +export type ResourceConfig = { limits?: ContainerConfigResource requests?: ContainerConfigResource } @@ -195,79 +196,139 @@ export type ExpectedContainerState = { exitCode?: number } +export type ContainerConfigType = 'image' | 'instance' | 'deployment' | 'config-bundle' +export type ContainerConfigSectionType = 'base' | 'concrete' + +export type ContainerConfig = (ContainerConfigData | ConcreteContainerConfigData) & { + id: string + type: ContainerConfigType +} + +export type ContainerConfigDataWithId = ContainerConfig + export type ContainerConfigData = { // common - name: string + name?: string environment?: UniqueKeyValue[] secrets?: UniqueSecretKey[] routing?: ContainerConfigRouting - expose: ContainerConfigExposeStrategy + expose?: ContainerConfigExposeStrategy user?: number workingDirectory?: string - tty: boolean + tty?: boolean configContainer?: ContainerConfigContainer - ports?: ContainerConfigPort[] - portRanges?: ContainerConfigPortRange[] - volumes?: ContainerConfigVolume[] + ports?: Port[] + portRanges?: ContainerPortRange[] + volumes?: Volume[] commands?: UniqueKey[] args?: UniqueKey[] initContainers?: InitContainer[] - capabilities: UniqueKeyValue[] + capabilities?: UniqueKeyValue[] storage?: ContainerStorage // dagent - logConfig?: ContainerConfigLog - restartPolicy: ContainerRestartPolicyType - networkMode: ContainerNetworkMode + logConfig?: Log + restartPolicy?: ContainerRestartPolicyType + networkMode?: ContainerNetworkMode networks?: UniqueKey[] dockerLabels?: UniqueKeyValue[] expectedState?: ExpectedContainerState // crane - deploymentStrategy: ContainerDeploymentStrategyType + deploymentStrategy?: ContainerDeploymentStrategyType customHeaders?: UniqueKey[] - proxyHeaders: boolean - useLoadBalancer: boolean + proxyHeaders?: boolean + useLoadBalancer?: boolean extraLBAnnotations?: UniqueKeyValue[] healthCheckConfig?: ContainerConfigHealthCheck - resourceConfig?: ContainerConfigResourceConfig + resourceConfig?: ResourceConfig annotations?: Marker labels?: Marker metrics?: Metrics } -type DagentSpecificConfig = - | 'logConfig' - | 'restartPolicy' - | 'networkMode' - | 'networks' - | 'dockerLabels' - | 'expectedState' -type CraneSpecificConfig = - | 'deploymentStrategy' - | 'customHeaders' - | 'proxyHeaders' - | 'useLoadBalancer' - | 'extraLBAnnotations' - | 'healthCheckConfig' - | 'resourceConfig' - | 'labels' - | 'annotations' - | 'metrics' - -export type DagentConfigDetails = Pick -export type CraneConfigDetails = Pick -export type CommonConfigDetails = Omit - -export type InstanceDagentConfigDetails = Pick -export type InstanceCraneConfigDetails = Pick -export type InstanceCommonConfigDetails = Omit - -export type MergedContainerConfigData = Omit & { - secrets: UniqueSecretKeyValue[] -} - -export type InstanceContainerConfigData = Partial +export const COMMON_CONFIG_KEYS = [ + 'name', + 'environment', + 'secrets', + 'routing', + 'expose', + 'user', + 'tty', + 'workingDirectory', + 'configContainer', + 'ports', + 'portRanges', + 'volumes', + 'commands', + 'args', + 'initContainers', + 'storage', +] as const + +export const CRANE_CONFIG_KEYS = [ + 'deploymentStrategy', + 'customHeaders', + 'proxyHeaders', + 'useLoadBalancer', + 'extraLBAnnotations', + 'healthCheckConfig', + 'resourceConfig', + 'labels', + 'annotations', + 'metrics', +] as const + +export const DAGENT_CONFIG_KEYS = [ + 'logConfig', + 'restartPolicy', + 'networkMode', + 'networks', + 'dockerLabels', + 'expectedState', +] as const + +export const CONTAINER_CONFIG_KEYS = [...COMMON_CONFIG_KEYS, ...CRANE_CONFIG_KEYS, ...DAGENT_CONFIG_KEYS] as const + +export type CommonConfigKey = (typeof COMMON_CONFIG_KEYS)[number] +export type CraneConfigKey = (typeof CRANE_CONFIG_KEYS)[number] +export type DagentConfigKey = (typeof DAGENT_CONFIG_KEYS)[number] +export type ContainerConfigKey = (typeof CONTAINER_CONFIG_KEYS)[number] + +export type DagentConfigData = Pick +export type CraneConfigData = Pick +export type CommonConfigData = Omit + +export type ConcreteDagentConfigData = Pick +export type ConcreteCraneConfigData = Pick +export type ConcreteCommonConfigData = Omit< + ConcreteContainerConfigData, + DagentConfigKey | CraneConfigKey | 'secrets' +> & { + secrets?: UniqueSecretKeyValue[] +} + +export type ConcreteContainerConfigData = Omit & { + secrets?: UniqueSecretKeyValue[] +} + +export type ConcreteContainerConfig = ConcreteContainerConfigData & { + id: string + type: ContainerConfigType +} + +export const CRANE_CONFIG_FILTER_VALUES = CRANE_CONFIG_KEYS.filter(it => it !== 'extraLBAnnotations') + +export type ContainerConfigFilterType = 'all' | 'common' | 'dagent' | 'crane' + +export const filterContains = ( + filter: CommonConfigKey | CraneConfigKey | DagentConfigKey, + filters: ContainerConfigKey[], +): boolean => filters.includes(filter) + +export const filterEmpty = (filterValues: string[], filters: ContainerConfigKey[]): boolean => + filterValues.filter(x => filters.includes(x as ContainerConfigKey)).length > 0 + export type JsonInitContainer = { name: string image: string @@ -290,9 +351,9 @@ export type JsonMarker = { } export type JsonInitContainerVolumeLink = Omit -export type JsonContainerConfigPortRange = Omit -export type JsonContainerConfigPort = Omit -export type JsonContainerConfigVolume = Omit +export type JsonContainerConfigPortRange = Omit +export type JsonContainerConfigPort = Omit +export type JsonContainerConfigVolume = Omit export type JsonContainerConfigSecretKey = Omit export type JsonContainerConfig = { @@ -303,6 +364,7 @@ export type JsonContainerConfig = { routing?: ContainerConfigRouting expose?: ContainerConfigExposeStrategy user?: number + workingDirectory?: string tty?: boolean configContainer?: ContainerConfigContainer ports?: JsonContainerConfigPort[] @@ -320,7 +382,7 @@ export type JsonContainerConfig = { restartPolicy?: ContainerRestartPolicyType networkMode?: ContainerNetworkMode networks?: string[] - dockerLabels: JsonKeyValue + dockerLabels?: JsonKeyValue // crane deploymentStrategy?: ContainerDeploymentStrategyType @@ -329,109 +391,47 @@ export type JsonContainerConfig = { useLoadBalancer?: boolean extraLBAnnotations?: JsonKeyValue healthCheckConfig?: ContainerConfigHealthCheck - resourceConfig?: ContainerConfigResourceConfig - annotations: JsonMarker - labels: JsonMarker + resourceConfig?: ResourceConfig + annotations?: JsonMarker + labels?: JsonMarker } -export type InstanceJsonContainerConfig = Omit +export type ConcreteJsonContainerConfig = Omit -const mergeSecrets = ( - imageSecrets: UniqueSecretKey[], - instanceSecrets: UniqueSecretKeyValue[], -): UniqueSecretKeyValue[] => { - imageSecrets = imageSecrets ?? [] - instanceSecrets = instanceSecrets ?? [] - - const overriddenIds: Set = new Set(instanceSecrets?.map(it => it.id)) +export const stringResettable = (base: string, concrete: string): boolean => { + if (!concrete) { + return false + } - const missing: UniqueSecretKeyValue[] = imageSecrets - .filter(it => !overriddenIds.has(it.id)) - .map(it => ({ - ...it, - value: '', - encrypted: false, - publicKey: null, - })) + if (!base) { + return true + } - return [...missing, ...instanceSecrets] + return base !== concrete } -const mergeMarker = (image: Marker, instance: Marker): Marker => { - if (!instance) { - return image +export const numberResettable = (base: number, concrete: number): boolean => { + if (typeof concrete !== 'number') { + return false } - if (!image) { - return null + if (typeof base !== 'number') { + return true } - return { - deployment: instance.deployment ?? image.deployment, - ingress: instance.ingress ?? image.ingress, - service: instance.service ?? image.service, - } + return base !== concrete } -const mergeMetrics = (image: Metrics, instance: Metrics): Metrics => { - if (!instance) { - return image?.enabled ? image : null +export const booleanResettable = (base: boolean, concrete: boolean): boolean => { + if (typeof concrete !== 'boolean') { + return false } - return instance -} - -export const mergeConfigs = ( - image: ContainerConfigData, - instance: InstanceContainerConfigData, -): MergedContainerConfigData => { - instance = instance ?? {} - - return { - name: instance.name ?? image.name, - environment: instance.environment ?? image.environment, - secrets: mergeSecrets(image.secrets, instance.secrets), - ports: instance.ports ?? image.ports, - user: instance.user ?? image.user, - workingDirectory: instance.workingDirectory ?? image.workingDirectory, - tty: instance.tty ?? image.tty, - portRanges: instance.portRanges ?? image.portRanges, - args: instance.args ?? image.args, - commands: instance.commands ?? image.commands, - expose: instance.expose ?? image.expose, - configContainer: instance.configContainer ?? image.configContainer, - routing: instance.routing ?? image.routing, - volumes: instance.volumes ?? image.volumes, - initContainers: instance.initContainers ?? image.initContainers, - capabilities: null, - storage: instance.storage ?? image.storage, - - // crane - customHeaders: instance.customHeaders ?? image.customHeaders, - proxyHeaders: instance.proxyHeaders ?? image.proxyHeaders, - extraLBAnnotations: instance.extraLBAnnotations ?? image.extraLBAnnotations, - healthCheckConfig: instance.healthCheckConfig ?? image.healthCheckConfig, - resourceConfig: instance.resourceConfig ?? image.resourceConfig, - useLoadBalancer: instance.useLoadBalancer ?? image.useLoadBalancer, - deploymentStrategy: instance.deploymentStrategy ?? instance.deploymentStrategy ?? 'recreate', - labels: mergeMarker(image.labels, instance.labels), - annotations: mergeMarker(image.annotations, instance.annotations), - metrics: mergeMetrics(image.metrics, instance.metrics), - - // dagent - logConfig: instance.logConfig ?? image.logConfig, - networkMode: instance.networkMode ?? image.networkMode ?? 'none', - restartPolicy: instance.restartPolicy ?? image.restartPolicy ?? 'unlessStopped', - networks: instance.networks ?? image.networks, - dockerLabels: instance.dockerLabels ?? image.dockerLabels, - expectedState: - !!image.expectedState || !!instance.expectedState - ? { - ...image.expectedState, - ...instance.expectedState, - } - : null, + if (typeof base !== 'boolean') { + return true } + + return base !== concrete } const keyValueArrayToJson = (list: UniqueKeyValue[]): JsonKeyValue => @@ -446,62 +446,71 @@ const removeId = (item: T): Omit => { return newItem } -export const imageConfigToJsonContainerConfig = (config: Partial): JsonContainerConfig => { - const jsonConfig = { - ...config, - commands: keyArrayToJson(config.commands), - args: keyArrayToJson(config.args), - networks: keyArrayToJson(config.networks), - customHeaders: keyArrayToJson(config.customHeaders), - extraLBAnnotations: keyValueArrayToJson(config.extraLBAnnotations), - environment: keyValueArrayToJson(config.environment), - capabilities: keyValueArrayToJson(config.capabilities), - secrets: config.secrets?.map(it => ({ key: it.key, required: it.required })), - portRanges: config.portRanges?.map(it => removeId(it)), - ports: config.ports?.map(it => removeId(it)), - storage: config.storage, - logConfig: config.logConfig - ? { - ...config.logConfig, - options: keyValueArrayToJson(config.logConfig?.options), - } - : null, - initContainers: config.initContainers?.map(container => ({ - ...removeId(container), - command: keyArrayToJson(container.command), - args: keyArrayToJson(container.args), - environment: keyValueArrayToJson(container.environment), - volumes: container.volumes?.map(vit => removeId(vit)), - })), - volumes: config.volumes?.map(it => removeId(it)), - dockerLabels: keyValueArrayToJson(config.dockerLabels), - annotations: config.annotations - ? { - deployment: keyValueArrayToJson(config.annotations.deployment), - service: keyValueArrayToJson(config.annotations.service), - ingress: keyValueArrayToJson(config.annotations.ingress), - } - : null, - labels: config.labels - ? { - deployment: keyValueArrayToJson(config.labels.deployment), - service: keyValueArrayToJson(config.labels.service), - ingress: keyValueArrayToJson(config.labels.ingress), - } - : null, - } +export const containerConfigToJsonConfig = (config: ContainerConfigData): JsonContainerConfig => ({ + // common + name: config.name, + environment: keyValueArrayToJson(config.environment), + // secrets are ommited + routing: config.routing, + expose: config.expose, + user: config.user, + workingDirectory: config.workingDirectory, + tty: config.tty, + configContainer: config.configContainer, + ports: config.ports?.map(it => removeId(it)), + portRanges: config.portRanges?.map(it => removeId(it)), + volumes: config.volumes?.map(it => removeId(it)), + commands: keyArrayToJson(config.commands), + args: keyArrayToJson(config.args), + initContainers: config.initContainers?.map(container => ({ + ...removeId(container), + command: keyArrayToJson(container.command), + args: keyArrayToJson(container.args), + environment: keyValueArrayToJson(container.environment), + volumes: container.volumes?.map(vit => removeId(vit)), + })), + capabilities: keyValueArrayToJson(config.capabilities), + storage: config.storage, - const configObject = jsonConfig as any - delete configObject.id - delete configObject.imageId + // dagent + logConfig: config.logConfig + ? { + ...config.logConfig, + options: keyValueArrayToJson(config.logConfig?.options), + } + : null, + restartPolicy: config.restartPolicy, + networkMode: config.networkMode, + networks: keyArrayToJson(config.networks), + dockerLabels: keyValueArrayToJson(config.dockerLabels), + expectedState: config.expectedState, - return jsonConfig -} + // crane + deploymentStrategy: config.deploymentStrategy, + customHeaders: keyArrayToJson(config.customHeaders), + proxyHeaders: config.proxyHeaders, + useLoadBalancer: config.useLoadBalancer, + extraLBAnnotations: keyValueArrayToJson(config.extraLBAnnotations), + healthCheckConfig: config.healthCheckConfig, + resourceConfig: config.resourceConfig, + annotations: config.annotations + ? { + deployment: keyValueArrayToJson(config.annotations.deployment), + service: keyValueArrayToJson(config.annotations.service), + ingress: keyValueArrayToJson(config.annotations.ingress), + } + : null, + labels: config.labels + ? { + deployment: keyValueArrayToJson(config.labels.deployment), + service: keyValueArrayToJson(config.labels.service), + ingress: keyValueArrayToJson(config.labels.ingress), + } + : null, +}) -export const instanceConfigToJsonInstanceConfig = ( - config: InstanceContainerConfigData, -): InstanceJsonContainerConfig => { - const json = imageConfigToJsonContainerConfig(config) +export const concreteContainerConfigToJsonConfig = (config: ConcreteContainerConfig): ConcreteJsonContainerConfig => { + const json = containerConfigToJsonConfig(config) delete json.secrets @@ -664,11 +673,11 @@ const mergeInitContainersWithJson = (containers: InitContainer[], json: JsonInit return containers } -export const mergeJsonConfigToInstanceContainerConfig = ( - config: InstanceContainerConfigData, - json: InstanceJsonContainerConfig, -): InstanceContainerConfigData => { - const result: InstanceContainerConfigData = { +export const mergeJsonConfigToConcreteContainerConfig = ( + config: ConcreteContainerConfig, + json: ConcreteJsonContainerConfig, +): ConcreteContainerConfig => { + const result: ConcreteContainerConfig = { ...config, ...json, environment: mergeKeyValuesWithJson(config.environment, json.environment), @@ -728,19 +737,16 @@ export const mergeJsonConfigToInstanceContainerConfig = ( return result } -export const mergeJsonConfigToImageContainerConfig = ( - config: ContainerConfigData, - json: JsonContainerConfig, -): ContainerConfigData => { - const asInstanceConfig = { +export const mergeJsonWithContainerConfig = (config: ContainerConfig, json: JsonContainerConfig): ContainerConfig => { + const concreteConfig: ConcreteContainerConfig = { ...config, secrets: null, } - const instanceConf = mergeJsonConfigToInstanceContainerConfig(asInstanceConfig, json) + const mergedConf = mergeJsonConfigToConcreteContainerConfig(concreteConfig, json) return { ...config, - ...instanceConf, + ...mergedConf, secrets: mergeSecretsWithJson(config.secrets, json.secrets), } } @@ -799,6 +805,7 @@ export const containerPortsToString = (ports: ContainerPort[], truncateAfter: nu return result.join(', ') } +export const imageNameOfContainer = (container: Container): string => imageName(container.imageName, container.imageTag) export const containerPrefixNameOf = (id: ContainerIdentifier): string => !id.prefix ? id.name : `${id.prefix}-${id.name}` @@ -814,3 +821,11 @@ export const containerIsHidden = (it: Container) => { return serviceCategoryIsHidden(serviceCategory) || kubeNamespaceIsSystem(kubeNamespace) } + +export const containerConfigTypeToSectionType = (type: ContainerConfigType): ContainerConfigSectionType => { + if (type === 'instance' || type === 'deployment') { + return 'concrete' + } + + return 'base' +} diff --git a/web/crux-ui/src/models/deployment.ts b/web/crux-ui/src/models/deployment.ts index d00ede68dc..3f90d526c2 100644 --- a/web/crux-ui/src/models/deployment.ts +++ b/web/crux-ui/src/models/deployment.ts @@ -1,7 +1,7 @@ import { Audit } from './audit' import { DeploymentStatus, DyoApiError, slugify } from './common' import { ConfigBundleDetails } from './config-bundle' -import { ContainerIdentifier, ContainerState, UniqueKeyValue } from './container' +import { ConcreteContainerConfig, ContainerIdentifier, ContainerState } from './container' import { ImageDeletedMessage, VersionImage } from './image' import { Instance } from './instance' import { DyoNode } from './node' @@ -47,12 +47,15 @@ export type DeploymentTokenCreated = DeploymentToken & { curl: string } -export type DeploymentDetails = Deployment & { - environment: UniqueKeyValue[] - configBundleEnvironment: EnvironmentToConfigBundleNameMap +export type DeploymentWithConfig = Deployment & { + config: ConcreteContainerConfig publicKey?: string - configBundleIds?: string[] - token: DeploymentToken + configBundles: ConfigBundleDetails[] +} + +export type DeploymentDetails = DeploymentWithConfig & { + token?: DeploymentToken + lastTry: number instances: Instance[] } @@ -106,12 +109,11 @@ export type CreateDeployment = { note?: string | undefined } -export type PatchDeployment = { - id: string - prefix?: string +export type UpdateDeployment = { note?: string + prefix?: string protected?: boolean - environment?: UniqueKeyValue[] + configBundles?: string[] } export type CopyDeployment = { @@ -238,3 +240,11 @@ export const lastDeploymentStatusOfEvents = (events: DeploymentEvent[]): Deploym export const deploymentHasError = (dto: DyoApiError): boolean => dto.error === 'rollingVersionDeployment' || dto.error === 'alreadyHavePreparing' + +export const instanceCreatedMessageToInstance = (it: InstanceCreatedMessage): Instance => ({ + ...it, + config: { + id: it.configId, + type: 'instance', + }, +}) diff --git a/web/crux-ui/src/models/image.ts b/web/crux-ui/src/models/image.ts index 3404cf6eaa..4f366a9fe7 100644 --- a/web/crux-ui/src/models/image.ts +++ b/web/crux-ui/src/models/image.ts @@ -1,5 +1,5 @@ -import { ContainerConfigData } from './container' -import { BasicRegistry, RegistryImages } from './registry' +import { ContainerConfig, ContainerConfigData, ContainerConfigKey } from './container' +import { BasicRegistry, imageName, RegistryImages } from './registry' export const ENVIRONMENT_VALUE_TYPES = ['string', 'boolean', 'int'] as const export type EnvironmentValueType = (typeof ENVIRONMENT_VALUE_TYPES)[number] @@ -15,7 +15,7 @@ export type VersionImage = { name: string tag: string order: number - config: ContainerConfigData + config: ContainerConfig createdAt: string registry: BasicRegistry labels: Record @@ -24,7 +24,7 @@ export type VersionImage = { export type PatchVersionImage = { tag?: string config?: Partial - resetSection?: ImageConfigProperty + resetSection?: ContainerConfigKey } export type ViewState = 'editor' | 'json' @@ -55,7 +55,7 @@ export type ImagesAddedMessage = { images: VersionImage[] } -export const WS_TYPE_IMAGE_SET_TAG = 'image-set-tag' +export const WS_TYPE_SET_IMAGE_TAG = 'set-image-tag' export const WS_TYPE_IMAGE_TAG_UPDATED = 'image-tag-updated' export type ImageTagMessage = { imageId: string @@ -76,74 +76,6 @@ export type GetImageMessage = { export const WS_TYPE_IMAGE = 'image' export type ImageMessage = VersionImage -export const COMMON_CONFIG_PROPERTIES = [ - 'name', - 'environment', - 'secrets', - 'routing', - 'expose', - 'user', - 'tty', - 'workingDirectory', - 'configContainer', - 'ports', - 'portRanges', - 'volumes', - 'commands', - 'args', - 'initContainers', - 'storage', -] as const - -export const CRANE_CONFIG_PROPERTIES = [ - 'deploymentStrategy', - 'customHeaders', - 'proxyHeaders', - 'useLoadBalancer', - 'extraLBAnnotations', - 'healthCheckConfig', - 'resourceConfig', - 'labels', - 'annotations', - 'metrics', -] as const - -export const DAGENT_CONFIG_PROPERTIES = [ - 'logConfig', - 'restartPolicy', - 'networkMode', - 'networks', - 'dockerLabels', - 'expectedState', -] as const - -export const ALL_CONFIG_PROPERTIES = [ - ...COMMON_CONFIG_PROPERTIES, - ...CRANE_CONFIG_PROPERTIES, - ...DAGENT_CONFIG_PROPERTIES, -] as const - -export const CRANE_CONFIG_FILTER_VALUES = CRANE_CONFIG_PROPERTIES.filter(it => it !== 'extraLBAnnotations') - -export type CommonConfigProperty = (typeof COMMON_CONFIG_PROPERTIES)[number] -export type CraneConfigProperty = (typeof CRANE_CONFIG_PROPERTIES)[number] -export type DagentConfigProperty = (typeof DAGENT_CONFIG_PROPERTIES)[number] -export type ImageConfigProperty = (typeof ALL_CONFIG_PROPERTIES)[number] - -export type BaseImageConfigFilterType = 'all' | 'common' | 'dagent' | 'crane' - -export const filterContains = ( - filter: CommonConfigProperty | CraneConfigProperty | DagentConfigProperty, - filters: ImageConfigProperty[], -): boolean => filters.includes(filter) - -export const filterEmpty = (filterValues: string[], filters: ImageConfigProperty[]): boolean => - filterValues.filter(x => filters.includes(x as ImageConfigProperty)).length > 0 - -export const imageName = (name: string, tag?: string): string => { - if (!tag) { - return name - } - - return `${name}:${tag}` -} +export const imageNameOf = (image: VersionImage): string => imageName(image.name, image.tag) + +export const containerNameOfImage = (image: VersionImage) => image.config.name ?? image.name diff --git a/web/crux-ui/src/models/index.ts b/web/crux-ui/src/models/index.ts index 6fcc022b8d..afc641e209 100644 --- a/web/crux-ui/src/models/index.ts +++ b/web/crux-ui/src/models/index.ts @@ -1,10 +1,13 @@ export * from './audit' export * from './auth' export * from './common' +export * from './compose' export * from './config-bundle' -export * from './package' export * from './container' export * from './container-config' +export * from './container-conflict' +export * from './container-merge' +export * from './container-errors' export * from './dashboard' export * from './deployment' export * from './editor' @@ -13,13 +16,13 @@ export * from './image' export * from './instance' export * from './node' export * from './notification' +export * from './package' +export * from './pipeline' export * from './project' export * from './registry' -export * from './pipeline' export * from './storage' export * from './team' export * from './template' export * from './token' export * from './user' export * from './version' -export * from './compose' diff --git a/web/crux-ui/src/models/instance.ts b/web/crux-ui/src/models/instance.ts index 88f334eaab..f340c706ea 100644 --- a/web/crux-ui/src/models/instance.ts +++ b/web/crux-ui/src/models/instance.ts @@ -1,8 +1,11 @@ -import { InstanceContainerConfigData } from './container' -import { VersionImage } from './image' +import { ConcreteContainerConfig } from './container' +import { containerNameOfImage, VersionImage } from './image' export type Instance = { id: string image: VersionImage - config?: InstanceContainerConfigData + config: ConcreteContainerConfig } + +export const containerNameOfInstance = (instance: Instance) => + instance.config.name ?? containerNameOfImage(instance.image) diff --git a/web/crux-ui/src/models/registry.ts b/web/crux-ui/src/models/registry.ts index 1eff2bd5f6..059e3a7502 100644 --- a/web/crux-ui/src/models/registry.ts +++ b/web/crux-ui/src/models/registry.ts @@ -406,3 +406,11 @@ export const findRegistryByUrl = (registries: Registry[], url: string): Registry !registries || !url ? null : registries.filter(it => it.type !== 'unchecked').find(it => url.startsWith(it.imageUrlPrefix)) + +export const imageName = (name: string, tag?: string): string => { + if (!tag) { + return name + } + + return `${name}:${tag}` +} diff --git a/web/crux-ui/src/pages/[teamSlug]/config-bundles.tsx b/web/crux-ui/src/pages/[teamSlug]/config-bundles.tsx index c918dd0196..113742a58d 100644 --- a/web/crux-ui/src/pages/[teamSlug]/config-bundles.tsx +++ b/web/crux-ui/src/pages/[teamSlug]/config-bundles.tsx @@ -1,5 +1,5 @@ -import AddConfigBundleCard from '@app/components/config-bundles/add-config-bundle-card' import ConfigBundleCard from '@app/components/config-bundles/config-bundle-card' +import EditConfigBundleCard from '@app/components/config-bundles/edit-config-bundle-card' import { Layout } from '@app/components/layout' import { BreadcrumbLink } from '@app/components/shared/breadcrumb' import Filters from '@app/components/shared/filters' @@ -11,7 +11,7 @@ import useAnchor from '@app/hooks/use-anchor' import { TextFilter, textFilterFor, useFilters } from '@app/hooks/use-filters' import useSubmit from '@app/hooks/use-submit' import useTeamRoutes from '@app/hooks/use-team-routes' -import { ConfigBundle } from '@app/models' +import { ConfigBundle, ConfigBundleDetails } from '@app/models' import { ANCHOR_NEW, ListRouteOptions, TeamRoutes } from '@app/routes' import { withContextAuthorization } from '@app/utils' import { getCruxFromContext } from '@server/crux-api' @@ -40,7 +40,7 @@ const ConfigBundles = (props: ConfigBundlesPageProps) => { const creating = anchor === ANCHOR_NEW const submit = useSubmit() - const onCreated = async (bundle: ConfigBundle) => { + const onCreated = async (bundle: ConfigBundleDetails) => { await router.push(routes.configBundle.details(bundle.id)) } @@ -59,7 +59,9 @@ const ConfigBundles = (props: ConfigBundlesPageProps) => { - {!creating ? null : } + {!creating ? null : ( + + )} {filters.items.length ? ( <> filters.setFilter({ text: it })} /> @@ -74,6 +76,7 @@ const ConfigBundles = (props: ConfigBundlesPageProps) => { className={clsx('max-h-72 w-full p-8 my-2', modulo3Class, modulo2Class)} key={`bundle-${index}`} configBundle={it} + showConfigIcon /> ) })} diff --git a/web/crux-ui/src/pages/[teamSlug]/config-bundles/[configBundleId].tsx b/web/crux-ui/src/pages/[teamSlug]/config-bundles/[configBundleId].tsx index bdec815f33..20838d0d70 100644 --- a/web/crux-ui/src/pages/[teamSlug]/config-bundles/[configBundleId].tsx +++ b/web/crux-ui/src/pages/[teamSlug]/config-bundles/[configBundleId].tsx @@ -1,26 +1,22 @@ -import { ConfigBundlePageMenu } from '@app/components/config-bundles/config-bundle-page-menu' -import { useConfigBundleDetailsState } from '@app/components/config-bundles/use-config-bundle-details-state' -import MultiInput from '@app/components/editor/multi-input' -import MultiTextArea from '@app/components/editor/multi-textarea' +import ConfigBundleCard from '@app/components/config-bundles/config-bundle-card' +import EditConfigBundleCard from '@app/components/config-bundles/edit-config-bundle-card' import { Layout } from '@app/components/layout' import { BreadcrumbLink } from '@app/components/shared/breadcrumb' -import KeyValueInput from '@app/components/shared/key-value-input' import PageHeading from '@app/components/shared/page-heading' -import { DyoCard } from '@app/elements/dyo-card' -import { DyoHeading } from '@app/elements/dyo-heading' -import { DyoLabel } from '@app/elements/dyo-label' -import WebSocketSaveIndicator from '@app/elements/web-socket-save-indicator' +import { DetailsPageMenu } from '@app/components/shared/page-menu' import { defaultApiErrorHandler } from '@app/errors' +import useSubmit from '@app/hooks/use-submit' import useTeamRoutes from '@app/hooks/use-team-routes' -import { ConfigBundleDetails } from '@app/models' +import { ConfigBundleDetails, detailsToConfigBundle } from '@app/models' import { TeamRoutes } from '@app/routes' import { withContextAuthorization } from '@app/utils' import { getCruxFromContext } from '@server/crux-api' import { GetServerSidePropsContext } from 'next' import useTranslation from 'next-translate/useTranslation' -import toast from 'react-hot-toast' +import { useRouter } from 'next/router' +import { useState } from 'react' -interface ConfigBundleDetailsPageProps { +type ConfigBundleDetailsPageProps = { configBundle: ConfigBundleDetails } @@ -28,24 +24,27 @@ const ConfigBundleDetailsPage = (props: ConfigBundleDetailsPageProps) => { const { configBundle: propsConfigBundle } = props const { t } = useTranslation('config-bundles') + const router = useRouter() const routes = useTeamRoutes() - const onWsError = (error: Error) => { - // eslint-disable-next-line - console.error('ws', 'edit-config-bundle', error) - toast(t('errors:connectionLost')) - } - const onApiError = defaultApiErrorHandler(t) - const [state, actions] = useConfigBundleDetailsState({ - configBundle: propsConfigBundle, - onWsError, - onApiError, - }) + const [configBundle, setConfigBundle] = useState(propsConfigBundle) + const [editing, setEditing] = useState(false) - const { configBundle, editing, saveState, editorState, fieldErrors, topBarContent } = state - const { setEditing, onDelete, onEditEnv, onEditName, onEditDescription } = actions + const submit = useSubmit() + + const onDelete = async () => { + const res = await fetch(routes.configBundle.api.details(configBundle.id), { + method: 'DELETE', + }) + + if (res.ok) { + await router.replace(routes.configBundle.list()) + } else { + await onApiError(res) + } + } const pageLink: BreadcrumbLink = { name: t('common:configBundles'), @@ -53,7 +52,7 @@ const ConfigBundleDetailsPage = (props: ConfigBundleDetailsPageProps) => { } return ( - + { }, ]} > - - - { })} /> - - - - {t(editing ? 'common:editName' : 'view', configBundle)} - - - {t('tips')} - -
- {editing && ( -
-
- {t('common:name')} - - onEditName(it)} - value={configBundle.name} - editorOptions={editorState} - message={fieldErrors.find(it => it.path?.startsWith('name'))?.message} - required - grow - /> -
- -
- - {t('common:description')} - - - onEditDescription(it)} - value={configBundle.description} - editorOptions={editorState} - message={fieldErrors.find(it => it.path?.startsWith('description'))?.message} - required - grow - /> -
-
- )} - - -
-
+ {editing ? ( + + ) : ( + + )} + TODO deployment list and config
) } diff --git a/web/crux-ui/src/pages/[teamSlug]/container-configurations/[configId].tsx b/web/crux-ui/src/pages/[teamSlug]/container-configurations/[configId].tsx new file mode 100644 index 0000000000..ac1fad5f21 --- /dev/null +++ b/web/crux-ui/src/pages/[teamSlug]/container-configurations/[configId].tsx @@ -0,0 +1,525 @@ +import CommonConfigSection from '@app/components/container-configs/common-config-section' +import configToFilters from '@app/components/container-configs/config-to-filters' +import ContainerConfigFilters from '@app/components/container-configs/container-config-filters' +import ContainerConfigJsonEditor from '@app/components/container-configs/container-config-json-editor' +import CraneConfigSection from '@app/components/container-configs/crane-config-section' +import DagentConfigSection from '@app/components/container-configs/dagent-config-section' +import EditorBadge from '@app/components/editor/editor-badge' +import useEditorState from '@app/components/editor/use-editor-state' +import useItemEditorState from '@app/components/editor/use-item-editor-state' +import { Layout } from '@app/components/layout' +import { BreadcrumbLink } from '@app/components/shared/breadcrumb' +import PageHeading from '@app/components/shared/page-heading' +import { WS_PATCH_DELAY } from '@app/const' +import DyoButton from '@app/elements/dyo-button' +import { DyoCard } from '@app/elements/dyo-card' +import { DyoHeading } from '@app/elements/dyo-heading' +import DyoMessage from '@app/elements/dyo-message' +import WebSocketSaveIndicator from '@app/elements/web-socket-save-indicator' +import useTeamRoutes from '@app/hooks/use-team-routes' +import { CANCEL_THROTTLE, useThrottling } from '@app/hooks/use-throttleing' +import useWebSocket from '@app/hooks/use-websocket' +import { + ConcreteContainerConfigData, + ConfigSecretsMessage, + ConfigUpdatedMessage, + ConflictedContainerConfigData, + conflictsToError, + ContainerConfigData, + ContainerConfigDetails, + ContainerConfigKey, + ContainerConfigRelations, + containerConfigToJsonConfig, + ContainerConfigType, + containerConfigTypeToSectionType, + getConflictsForConcreteConfig, + JsonContainerConfig, + mergeConfigsWithConcreteConfig, + mergeJsonWithContainerConfig, + PatchConfigMessage, + squashConfigs, + ViewState, + WebSocketSaveState, + WS_TYPE_CONFIG_SECRETS, + WS_TYPE_CONFIG_UPDATED, + WS_TYPE_GET_CONFIG_SECRETS, + WS_TYPE_PATCH_CONFIG, + WS_TYPE_PATCH_RECEIVED, +} from '@app/models' +import { TeamRoutes } from '@app/routes' +import { withContextAuthorization } from '@app/utils' +import { + ContainerConfigValidationErrors, + getConcreteContainerConfigFieldErrors, + getContainerConfigFieldErrors, + jsonErrorOf, +} from '@app/validations' +import { WsMessage } from '@app/websockets/common' +import { getCruxFromContext } from '@server/crux-api' +import { GetServerSidePropsContext } from 'next' +import { Translate } from 'next-translate' +import useTranslation from 'next-translate/useTranslation' +import { useCallback, useEffect, useRef, useState } from 'react' + +const pageLinkOf = (t: Translate, url: string, type: ContainerConfigType): BreadcrumbLink => { + switch (type) { + case 'image': + return { + name: t('common:imageConfig'), + url, + } + case 'instance': + return { + name: t('common:instanceConfig'), + url, + } + case 'deployment': + return { + name: t('common:deploymentConfig'), + url, + } + case 'config-bundle': + return { + name: t('common:configBundles'), + url, + } + default: + return { + name: t('common:config'), + url, + } + } +} + +const sublinksOf = ( + routes: TeamRoutes, + type: ContainerConfigType, + relations: ContainerConfigRelations, +): BreadcrumbLink[] => { + const { project, version, deployment, configBundle } = relations + + switch (type) { + case 'image': + return [ + { + name: project.name, + url: routes.project.details(project.id), + }, + { + name: version.name, + url: routes.project.versions(project.id).details(version.id), + }, + ] + case 'instance': + case 'deployment': + return [ + { + name: deployment.prefix, + url: routes.deployment.details(deployment.id), + }, + ] + case 'config-bundle': + return [ + { + name: configBundle.name, + url: routes.configBundle.details(configBundle.id), + }, + ] + default: + return [] + } +} + +const getConfigErrors = ( + config: ContainerConfigDetails, + imageLabels: Record, + t: Translate, +): ContainerConfigValidationErrors => { + const type = containerConfigTypeToSectionType(config.type) + + if (type === 'concrete') { + return getConcreteContainerConfigFieldErrors(config as ConcreteContainerConfigData, imageLabels, t) + } + + return getContainerConfigFieldErrors(config, imageLabels, t) +} + +const getBaseConfig = (config: ContainerConfigDetails, relations: ContainerConfigRelations): ContainerConfigData => { + switch (config.type) { + case 'instance': + return relations.image.config + case 'deployment': + return squashConfigs(relations.deployment.configBundles.map(it => it.config)) + default: + return null + } +} + +const getBundlesNameMap = ( + config: ContainerConfigDetails, + relations: ContainerConfigRelations, +): Record => { + if (config.type !== 'deployment') { + return {} + } + + return Object.fromEntries(relations.deployment.configBundles.map(it => [it.config.id, it.name])) +} + +const getMergedConfig = ( + config: ContainerConfigDetails, + relations: ContainerConfigRelations, +): ContainerConfigDetails => { + const baseConfig = getBaseConfig(config, relations) + if (!baseConfig) { + return config + } + + const concreteConfig = mergeConfigsWithConcreteConfig([baseConfig], config as ConcreteContainerConfigData) + return { + ...config, + ...concreteConfig, + } +} + +const getImageLabels = ( + config: ContainerConfigDetails, + relations: ContainerConfigRelations, +): Record => { + switch (config.type) { + case 'image': + case 'instance': + return relations.image.labels + default: + return {} + } +} + +const getConflicts = ( + config: ContainerConfigDetails, + relations: ContainerConfigRelations, +): ConflictedContainerConfigData => { + if (config.type !== 'deployment') { + return null + } + + const bundles = relations.deployment.configBundles + + if (bundles.length < 2) { + return null + } + + return getConflictsForConcreteConfig( + bundles.map(it => it.config), + config as ConcreteContainerConfigData, + ) +} + +type ContainerConfigPageProps = { + config: ContainerConfigDetails + relations: ContainerConfigRelations +} + +const ContainerConfigPage = (props: ContainerConfigPageProps) => { + const { config: propsConfig, relations } = props + + const { t } = useTranslation('container') + const routes = useTeamRoutes() + + const [resettableConfig, setResettableConfig] = useState(propsConfig) + const [viewState, setViewState] = useState('editor') + const [fieldErrors, setFieldErrors] = useState(() => + getConfigErrors(getMergedConfig(propsConfig, relations), getImageLabels(propsConfig, relations), t), + ) + const [jsonError, setJsonError] = useState(jsonErrorOf(fieldErrors)) + const [topBarContent, setTopBarContent] = useState(null) + const [saveState, setSaveState] = useState(null) + + const [filters, setFilters] = useState(configToFilters([], resettableConfig, fieldErrors)) + const [secrets, setSecrets] = useState(null) + + const patch = useRef>({}) + const throttle = useThrottling(WS_PATCH_DELAY) + const sock = useWebSocket(routes.containerConfig.detailsSocket(resettableConfig.id), { + onOpen: () => { + setSaveState('connected') + sock.send(WS_TYPE_GET_CONFIG_SECRETS, {}) + }, + onClose: () => setSaveState('disconnected'), + onSend: (message: WsMessage) => { + if (message.type === WS_TYPE_PATCH_CONFIG) { + setSaveState('saving') + } + }, + onReceive: (message: WsMessage) => { + if (message.type === WS_TYPE_PATCH_RECEIVED) { + setSaveState('saved') + } + }, + }) + + const editor = useEditorState(sock) + const editorState = useItemEditorState(editor, sock, resettableConfig.id) + + const conflicts = getConflicts(resettableConfig, relations) + const conflictErrors = conflictsToError(t, getBundlesNameMap(resettableConfig, relations), conflicts) + const baseConfig = getBaseConfig(resettableConfig, relations) + const config = getMergedConfig(resettableConfig, relations) + + useEffect(() => { + const reactNode = ( + <> + {editorState.editors.map((it, index) => ( + + ))} + + ) + + setTopBarContent(reactNode) + }, [editorState.editors]) + + const getName = useCallback(() => { + const parentName = config.parent.name + + switch (config.type) { + case 'image': + case 'instance': { + const name = config.name ?? parentName + if (!config.name || config.name === parentName) { + return name + } + + return `${name} (${parentName})` + } + default: + return parentName + } + }, [config.name, config.parent.name, config.type]) + + const getBackHref = useCallback(() => { + switch (propsConfig.type) { + case 'image': + return routes.project.versions(relations.project.id).details(relations.version.id, { section: 'images' }) + case 'instance': + case 'deployment': + return routes.deployment.details(relations.deployment.id) + case 'config-bundle': + return routes.configBundle.details(relations.configBundle.id) + default: + throw new Error(`Unknown ContainerConfigType ${propsConfig.type}`) + } + }, [routes, relations, propsConfig.type]) + + useEffect(() => { + setFilters(current => configToFilters(current, config)) + }, [config]) + + const setErrorsForConfig = useCallback( + newConfig => { + const errors = getConfigErrors(newConfig, getImageLabels(propsConfig, relations), t) + setFieldErrors(errors) + setJsonError(jsonErrorOf(errors)) + }, + [t], + ) + + const onChange = (newConfig: ContainerConfigData) => { + setSaveState('saving') + + const value = { ...resettableConfig, ...newConfig } + setResettableConfig(value) + setErrorsForConfig(value) + + const newPatch = { + ...patch.current, + ...newConfig, + } + patch.current = newPatch + + throttle(() => { + sock.send(WS_TYPE_PATCH_CONFIG, { + id: resettableConfig.id, + config: patch.current, + } as PatchConfigMessage) + + patch.current = {} + }) + } + + const onResetSection = (section: ContainerConfigKey) => { + const newConfig = { ...resettableConfig } as any + newConfig[section] = section === 'user' ? -1 : null + + setResettableConfig(newConfig) + setErrorsForConfig(newConfig) + + throttle(CANCEL_THROTTLE) + sock.send(WS_TYPE_PATCH_CONFIG, { + id: resettableConfig.id, + resetSection: section, + } as PatchConfigMessage) + } + + sock.on(WS_TYPE_CONFIG_UPDATED, (message: ConfigUpdatedMessage) => { + if (message.id !== resettableConfig.id) { + return + } + + const newConfig = { + ...resettableConfig, + ...message, + } + + setResettableConfig(newConfig) + setErrorsForConfig(newConfig) + }) + + sock.on(WS_TYPE_CONFIG_SECRETS, setSecrets) + + const pageLink = pageLinkOf(t, routes.configBundle.details(propsConfig.id), propsConfig.type) + const sublinks = sublinksOf(routes, propsConfig.type, relations) + + const getViewStateButtons = () => ( +
+ setViewState('editor')} + heightClassName="pb-2" + className="mx-8" + > + {t('editor')} + + + setViewState('json')} + className="mx-8" + heightClassName="pb-2" + > + {t('json')} + +
+ ) + + const { mutable } = config.parent + + const sectionType = containerConfigTypeToSectionType(config.type) + const showCraneConfig = sectionType === 'base' || relations.deployment?.node?.type === 'k8s' + const showDagentConfig = sectionType === 'base' || relations.deployment?.node?.type === 'docker' + + return ( + + + + + {t('common:back')} + + + +
+ + {getName()} + + + {getViewStateButtons()} +
+ + {viewState === 'editor' && } +
+ + {viewState === 'editor' && ( + + + + {showCraneConfig && ( + + )} + + {showDagentConfig && ( + + )} + + )} + + {viewState === 'json' && ( + + {jsonError ? ( + + ) : null} + + onChange(mergeJsonWithContainerConfig(config, it))} + onParseError={err => setJsonError(err?.message)} + convertConfigToJson={containerConfigToJsonConfig} + /> + + )} +
+ ) +} + +export default ContainerConfigPage + +const getPageServerSideProps = async (context: GetServerSidePropsContext) => { + const routes = TeamRoutes.fromContext(context) + + const configId = context.query.configId as string + + const config = await getCruxFromContext(context, routes.containerConfig.api.details(configId)) + const relations = await getCruxFromContext( + context, + routes.containerConfig.api.relations(configId), + ) + + return { + props: { + config, + relations, + }, + } +} + +export const getServerSideProps = withContextAuthorization(getPageServerSideProps) diff --git a/web/crux-ui/src/pages/[teamSlug]/deployments/[deploymentId].tsx b/web/crux-ui/src/pages/[teamSlug]/deployments/[deploymentId].tsx index 915320789d..c533364ee2 100644 --- a/web/crux-ui/src/pages/[teamSlug]/deployments/[deploymentId].tsx +++ b/web/crux-ui/src/pages/[teamSlug]/deployments/[deploymentId].tsx @@ -221,7 +221,7 @@ const DeploymentDetailsPage = (props: DeploymentDetailsPageProps) => { )}
- +
diff --git a/web/crux-ui/src/pages/[teamSlug]/deployments/[deploymentId]/instances/[instanceId].tsx b/web/crux-ui/src/pages/[teamSlug]/deployments/[deploymentId]/instances/[instanceId].tsx deleted file mode 100644 index a668f4ddf0..0000000000 --- a/web/crux-ui/src/pages/[teamSlug]/deployments/[deploymentId]/instances/[instanceId].tsx +++ /dev/null @@ -1,312 +0,0 @@ -import EditorBadge from '@app/components/editor/editor-badge' -import useEditorState from '@app/components/editor/use-editor-state' -import useItemEditorState from '@app/components/editor/use-item-editor-state' -import { Layout } from '@app/components/layout' -import useInstanceState from '@app/components/deployments/instances/use-instance-state' -import useDeploymentState from '@app/components/deployments/use-deployment-state' -import CommonConfigSection from '@app/components/projects/versions/images/config/common-config-section' -import configToFilters from '@app/components/projects/versions/images/config/config-to-filters' -import CraneConfigSection from '@app/components/projects/versions/images/config/crane-config-section' -import DagentConfigSection from '@app/components/projects/versions/images/config/dagent-config-section' -import EditImageJson from '@app/components/projects/versions/images/edit-image-json' -import ImageConfigFilters, { - dockerFilterSet, - k8sFilterSet, -} from '@app/components/projects/versions/images/image-config-filters' -import { BreadcrumbLink } from '@app/components/shared/breadcrumb' -import PageHeading from '@app/components/shared/page-heading' -import DyoButton from '@app/elements/dyo-button' -import { DyoCard } from '@app/elements/dyo-card' -import { DyoHeading } from '@app/elements/dyo-heading' -import DyoMessage from '@app/elements/dyo-message' -import WebSocketSaveIndicator from '@app/elements/web-socket-save-indicator' -import { defaultApiErrorHandler } from '@app/errors' -import useTeamRoutes from '@app/hooks/use-team-routes' -import { - DeploymentDetails, - DeploymentRoot, - ImageConfigProperty, - InstanceContainerConfigData, - InstanceJsonContainerConfig, - NodeDetails, - ProjectDetails, - VersionDetails, - ViewState, - instanceConfigToJsonInstanceConfig, - mergeConfigs, - mergeJsonConfigToInstanceContainerConfig, -} from '@app/models' -import { TeamRoutes } from '@app/routes' -import { withContextAuthorization } from '@app/utils' -import { ContainerConfigValidationErrors, getMergedContainerConfigFieldErrors, jsonErrorOf } from '@app/validations' -import { getCruxFromContext } from '@server/crux-api' -import { GetServerSidePropsContext } from 'next' -import useTranslation from 'next-translate/useTranslation' -import { useCallback, useEffect, useState } from 'react' -import toast from 'react-hot-toast' - -interface InstanceDetailsPageProps { - deployment: DeploymentRoot - instanceId: string -} - -const InstanceDetailsPage = (props: InstanceDetailsPageProps) => { - const { deployment, instanceId } = props - const { project, version } = deployment - - const { t } = useTranslation('images') - const routes = useTeamRoutes() - - const onWsError = (error: Error) => { - // eslint-disable-next-line - console.error('ws', 'edit-deployment', error) - toast(t('errors:connectionLost')) - } - - const onApiError = defaultApiErrorHandler(t) - - const [deploymentState, deploymentActions] = useDeploymentState({ - deployment, - onWsError, - onApiError, - }) - - const instance = deploymentState.instances.find(it => it.id === instanceId) - - const [state, actions] = useInstanceState({ - instance, - deploymentState, - deploymentActions, - }) - - const [fieldErrors, setFieldErrors] = useState(() => - getMergedContainerConfigFieldErrors(mergeConfigs(instance.image.config, state.config), instance.image.labels, t), - ) - const [filters, setFilters] = useState(configToFilters([], state.config, fieldErrors)) - const [viewState, setViewState] = useState('editor') - const [jsonError, setJsonError] = useState(jsonErrorOf(fieldErrors)) - const [topBarContent, setTopBarContent] = useState(null) - - const editor = useEditorState(deploymentState.sock) - const editorState = useItemEditorState(editor, deploymentState.sock, instance.id) - - useEffect(() => { - const reactNode = ( - <> - {editorState.editors.map((it, index) => ( - - ))} - - ) - - setTopBarContent(reactNode) - }, [editorState.editors]) - - useEffect(() => { - setFilters(current => configToFilters(current, state.config)) - }, [state.config]) - - const setErrorsForConfig = useCallback( - (imageConfig, instanceConfig) => { - const merged = mergeConfigs(imageConfig, instanceConfig) - const errors = getMergedContainerConfigFieldErrors(merged, instance.image.labels, t) - setFieldErrors(errors) - setJsonError(jsonErrorOf(errors)) - }, - [t], - ) - - useEffect(() => { - setErrorsForConfig(instance.image.config, instance.config) - }, [instance.image.config, instance.config, setErrorsForConfig]) - - const onChange = (newConfig: Partial) => actions.onPatch(instance.id, newConfig) - - const pageLink: BreadcrumbLink = { - name: t('common:container'), - url: routes.project.list(), - } - - const sublinks: BreadcrumbLink[] = [ - { - name: project.name, - url: routes.project.details(project.id), - }, - { - name: version.name, - url: routes.project.versions(project.id).details(version.id), - }, - { - name: t('common:deployment'), - url: routes.deployment.details(deployment.id), - }, - { - name: instance.image.name, - url: routes.deployment.instanceDetails(deployment.id, instance.id), - }, - ] - - const getViewStateButtons = () => ( -
- setViewState('editor')} - heightClassName="pb-2" - className="mx-8" - > - {t('editor')} - - - setViewState('json')} - className="mx-8" - heightClassName="pb-2" - > - {t('json')} - -
- ) - - const kubeNode = deployment.node.type === 'k8s' - - return ( - - - - - {t('common:back')} - - - -
- - {instance.image.name} - {instance.image.name !== state.config?.name ? ` (${state.config?.name})` : null} - - - {getViewStateButtons()} -
- - {viewState === 'editor' && ( - - )} -
- - {viewState === 'editor' && ( - - - - {kubeNode ? ( - - ) : ( - - )} - - )} - - {viewState === 'json' && ( - - {jsonError ? ( - - ) : null} - - - onChange(mergeJsonConfigToInstanceContainerConfig(state.config, it)) - } - onParseError={err => setJsonError(err?.message)} - convertConfigToJson={instanceConfigToJsonInstanceConfig} - /> - - )} -
- ) -} - -export default InstanceDetailsPage - -const getPageServerSideProps = async (context: GetServerSidePropsContext) => { - const routes = TeamRoutes.fromContext(context) - - const deploymentId = context.query.deploymentId as string - const instanceId = context.query.instanceId as string - - const deploymentDetails = await getCruxFromContext( - context, - routes.deployment.api.details(deploymentId), - ) - const project = await getCruxFromContext( - context, - routes.project.api.details(deploymentDetails.project.id), - ) - const node = await getCruxFromContext(context, routes.node.api.details(deploymentDetails.node.id)) - - const version = await getCruxFromContext( - context, - routes.project.versions(deploymentDetails.project.id).api.details(deploymentDetails.version.id), - ) - - const deployment: DeploymentRoot = { - ...deploymentDetails, - project, - version, - node, - } - - return { - props: { - deployment, - instanceId, - }, - } -} - -export const getServerSideProps = withContextAuthorization(getPageServerSideProps) diff --git a/web/crux-ui/src/pages/[teamSlug]/projects/[projectId]/versions/[versionId]/images/[imageId].tsx b/web/crux-ui/src/pages/[teamSlug]/projects/[projectId]/versions/[versionId]/images/[imageId].tsx deleted file mode 100644 index 88f11b1c7f..0000000000 --- a/web/crux-ui/src/pages/[teamSlug]/projects/[projectId]/versions/[versionId]/images/[imageId].tsx +++ /dev/null @@ -1,347 +0,0 @@ -import EditorBadge from '@app/components/editor/editor-badge' -import useEditorState from '@app/components/editor/use-editor-state' -import useItemEditorState from '@app/components/editor/use-item-editor-state' -import { Layout } from '@app/components/layout' -import CommonConfigSection from '@app/components/projects/versions/images/config/common-config-section' -import configToFilters from '@app/components/projects/versions/images/config/config-to-filters' -import CraneConfigSection from '@app/components/projects/versions/images/config/crane-config-section' -import DagentConfigSection from '@app/components/projects/versions/images/config/dagent-config-section' -import EditImageJson from '@app/components/projects/versions/images/edit-image-json' -import ImageConfigFilters from '@app/components/projects/versions/images/image-config-filters' -import { BreadcrumbLink } from '@app/components/shared/breadcrumb' -import PageHeading from '@app/components/shared/page-heading' -import { IMAGE_WS_REQUEST_DELAY } from '@app/const' -import DyoButton from '@app/elements/dyo-button' -import { DyoCard } from '@app/elements/dyo-card' -import { DyoHeading } from '@app/elements/dyo-heading' -import DyoMessage from '@app/elements/dyo-message' -import { DyoConfirmationModal } from '@app/elements/dyo-modal' -import WebSocketSaveIndicator from '@app/elements/web-socket-save-indicator' -import useConfirmation from '@app/hooks/use-confirmation' -import useTeamRoutes from '@app/hooks/use-team-routes' -import { useThrottling } from '@app/hooks/use-throttleing' -import useWebSocket from '@app/hooks/use-websocket' -import { - ContainerConfigData, - DeleteImageMessage, - ImageConfigProperty, - imageConfigToJsonContainerConfig, - ImageUpdateMessage, - JsonContainerConfig, - mergeJsonConfigToImageContainerConfig, - PatchImageMessage, - ProjectDetails, - VersionDetails, - VersionImage, - ViewState, - WebSocketSaveState, - WS_TYPE_DELETE_IMAGE, - WS_TYPE_IMAGE_UPDATED, - WS_TYPE_PATCH_IMAGE, - WS_TYPE_PATCH_RECEIVED, -} from '@app/models' -import { TeamRoutes } from '@app/routes' -import { withContextAuthorization } from '@app/utils' -import { ContainerConfigValidationErrors, getContainerConfigFieldErrors, jsonErrorOf } from '@app/validations' -import { WsMessage } from '@app/websockets/common' -import { getCruxFromContext } from '@server/crux-api' -import { GetServerSidePropsContext } from 'next' -import useTranslation from 'next-translate/useTranslation' -import { useRouter } from 'next/router' -import { QA_DIALOG_LABEL_DELETE_IMAGE } from 'quality-assurance' -import { useCallback, useEffect, useRef, useState } from 'react' - -interface ImageDetailsPageProps { - project: ProjectDetails - version: VersionDetails - image: VersionImage -} - -const ImageDetailsPage = (props: ImageDetailsPageProps) => { - const { image, project, version } = props - - const { t } = useTranslation('images') - const routes = useTeamRoutes() - - const [config, setConfig] = useState(image.config) - const [viewState, setViewState] = useState('editor') - const [fieldErrors, setFieldErrors] = useState(() => - getContainerConfigFieldErrors(image.config, image.labels, t), - ) - const [jsonError, setJsonError] = useState(jsonErrorOf(fieldErrors)) - const [topBarContent, setTopBarContent] = useState(null) - const [saveState, setSaveState] = useState(null) - - const [filters, setFilters] = useState(configToFilters([], config, fieldErrors)) - - const patch = useRef>({}) - const throttle = useThrottling(IMAGE_WS_REQUEST_DELAY) - const router = useRouter() - const [deleteModalConfig, confirmDelete] = useConfirmation() - const versionSock = useWebSocket(routes.project.versions(project.id).detailsSocket(version.id), { - onOpen: () => setSaveState('connected'), - onClose: () => setSaveState('disconnected'), - onReceive: (message: WsMessage) => { - if (message.type === WS_TYPE_PATCH_RECEIVED) { - setSaveState('saved') - } - }, - }) - - const editor = useEditorState(versionSock) - const editorState = useItemEditorState(editor, versionSock, image.id) - - useEffect(() => { - const reactNode = ( - <> - {editorState.editors.map((it, index) => ( - - ))} - - ) - - setTopBarContent(reactNode) - }, [editorState.editors]) - - useEffect(() => { - setFilters(current => configToFilters(current, config)) - }, [config]) - - const setErrorsForConfig = useCallback( - newConfig => { - const errors = getContainerConfigFieldErrors(newConfig, image.labels, t) - setFieldErrors(errors) - setJsonError(jsonErrorOf(errors)) - }, - [t], - ) - - const onChange = (newConfig: Partial) => { - setSaveState('saving') - - const value = { ...config, ...newConfig } - setConfig(value) - setErrorsForConfig(value) - - const newPatch = { - ...patch.current, - ...newConfig, - } - patch.current = newPatch - - throttle(() => { - versionSock.send(WS_TYPE_PATCH_IMAGE, { - id: image.id, - config: patch.current, - } as PatchImageMessage) - - patch.current = {} - }) - } - - const onResetSection = (section: ImageConfigProperty) => { - const newConfig = { ...config } as any - newConfig[section] = section === 'user' ? -1 : null - - setConfig(newConfig) - setErrorsForConfig(newConfig) - - versionSock.send(WS_TYPE_PATCH_IMAGE, { - id: image.id, - resetSection: section, - } as PatchImageMessage) - } - - versionSock.on(WS_TYPE_IMAGE_UPDATED, (message: ImageUpdateMessage) => { - if (message.id !== image.id) { - return - } - - const newConfig = { - ...config, - ...message.config, - } - - setConfig(newConfig) - setErrorsForConfig(newConfig) - }) - - const onDelete = async () => { - const confirmed = await confirmDelete({ - qaLabel: QA_DIALOG_LABEL_DELETE_IMAGE, - title: t('common:areYouSureDeleteName', { name: image.name }), - description: t('common:proceedYouLoseAllDataToName', { name: image.name }), - confirmText: t('common:delete'), - confirmColor: 'bg-error-red', - }) - - if (!confirmed) { - return - } - - versionSock.send(WS_TYPE_DELETE_IMAGE, { - imageId: image.id, - } as DeleteImageMessage) - - await router.replace(routes.project.versions(project.id).details(version.id)) - } - - const pageLink: BreadcrumbLink = { - name: t('common:image'), - url: routes.project.list(), - } - - const sublinks: BreadcrumbLink[] = [ - { - name: project.name, - url: routes.project.details(project.id), - }, - { - name: version.name, - url: routes.project.versions(project.id).details(version.id), - }, - { - name: image.name, - url: routes.project.versions(project.id).imageDetails(version.id, image.id), - }, - ] - - const getViewStateButtons = () => ( -
- setViewState('editor')} - heightClassName="pb-2" - className="mx-8" - > - {t('editor')} - - - setViewState('json')} - className="mx-8" - heightClassName="pb-2" - > - {t('json')} - -
- ) - - return ( - - - - - - {t('common:back')} - - - - {t('common:delete')} - - - - -
- - {image.name} - {image.name !== config?.name ? ` (${config?.name})` : null} - - - {getViewStateButtons()} -
- - {viewState === 'editor' && } -
- - {viewState === 'editor' && ( - - - - - - - - )} - - {viewState === 'json' && ( - - {jsonError ? ( - - ) : null} - - onChange(mergeJsonConfigToImageContainerConfig(config, it))} - onParseError={err => setJsonError(err?.message)} - convertConfigToJson={imageConfigToJsonContainerConfig} - /> - - )} - - -
- ) -} - -export default ImageDetailsPage - -const getPageServerSideProps = async (context: GetServerSidePropsContext) => { - const routes = TeamRoutes.fromContext(context) - - const projectId = context.query.projectId as string - const versionId = context.query.versionId as string - const imageId = context.query.imageId as string - - const project = getCruxFromContext(context, routes.project.api.details(projectId)) - const version = getCruxFromContext(context, routes.project.versions(projectId).api.details(versionId)) - const image = getCruxFromContext( - context, - routes.project.versions(projectId).api.imageDetails(versionId, imageId), - ) - - return { - props: { - image: await image, - project: await project, - version: await version, - }, - } -} - -export const getServerSideProps = withContextAuthorization(getPageServerSideProps) diff --git a/web/crux-ui/src/routes.ts b/web/crux-ui/src/routes.ts index 0782c73417..c378199f6e 100644 --- a/web/crux-ui/src/routes.ts +++ b/web/crux-ui/src/routes.ts @@ -369,8 +369,6 @@ class VersionRoutes { details = (id: string, params?: VersionUrlParams) => appendUrlParams(`${this.root}/${id}`, params) deployments = (versionId: string) => `${this.details(versionId)}/deployments` - - imageDetails = (versionId: string, imageId: string) => `${this.details(versionId)}/images/${imageId}` } // project @@ -480,9 +478,6 @@ class DeploymentRoutes { details = (id: string) => `${this.root}/${id}` deploy = (id: string) => `${this.details(id)}/deploy` - - instanceDetails = (deploymentId: string, instanceId: string) => - `${this.details(deploymentId)}/instances/${instanceId}` } // notification @@ -625,8 +620,42 @@ class PipelineRoutes { socket = () => this.root } -// config bundle +// container config +class ContainerConfigApi { + private readonly root: string + + constructor(root: string) { + this.root = `/api${root}` + } + + details = (id: string) => `${this.root}/${id}` + + relations = (configId: string) => `${this.details(configId)}/relations` +} + +class ContainerConfigRoutes { + private readonly root: string + + constructor(root: string) { + this.root = `${root}/container-configurations` + } + + private _api: ContainerConfigApi + get api() { + if (!this._api) { + this._api = new ContainerConfigApi(this.root) + } + + return this._api + } + + details = (id: string) => `${this.root}/${id}` + + detailsSocket = (id: string) => this.details(id) +} + +// config bundle class ConfigBundleApi { private readonly root: string @@ -637,8 +666,6 @@ class ConfigBundleApi { list = () => this.root details = (id: string) => `${this.root}/${id}` - - options = () => `${this.root}/options` } class ConfigBundleRoutes { @@ -734,6 +761,8 @@ export class TeamRoutes { private _pipeline: PipelineRoutes + private _containerConfig: ContainerConfigRoutes + private _configBundle: ConfigBundleRoutes private _package: PackageRoutes @@ -810,6 +839,14 @@ export class TeamRoutes { return this._pipeline } + get containerConfig() { + if (!this._containerConfig) { + this._containerConfig = new ContainerConfigRoutes(this.root) + } + + return this._containerConfig + } + get configBundle() { if (!this._configBundle) { this._configBundle = new ConfigBundleRoutes(this.root) diff --git a/web/crux-ui/src/utils.ts b/web/crux-ui/src/utils.ts index 90ed125e5f..627a8a74a3 100644 --- a/web/crux-ui/src/utils.ts +++ b/web/crux-ui/src/utils.ts @@ -489,7 +489,7 @@ export const toNumber = (value: string): number => { const parsedValue = Number(value) if (Number.isNaN(parsedValue)) { - return NaN + return null } return parsedValue diff --git a/web/crux-ui/src/validations/common.ts b/web/crux-ui/src/validations/common.ts index 3b043c9317..415715f7a7 100644 --- a/web/crux-ui/src/validations/common.ts +++ b/web/crux-ui/src/validations/common.ts @@ -86,7 +86,7 @@ export const iconRule = yup .label('common:icon') export const nameRule = yup.string().required().trim().min(3).max(70).label('common:name') -export const descriptionRule = yup.string().optional().label('common:description') +export const descriptionRule = yup.string().optional().nullable().label('common:description') export const identityNameRule = yup.string().trim().max(16) export const passwordLengthRule = yup.string().min(8).max(70).label('common:password') export const stringArrayRule = yup.array().of(yup.string()) diff --git a/web/crux-ui/src/validations/config-bundle.ts b/web/crux-ui/src/validations/config-bundle.ts index 0a6a86abea..bf95f5d832 100644 --- a/web/crux-ui/src/validations/config-bundle.ts +++ b/web/crux-ui/src/validations/config-bundle.ts @@ -1,12 +1,8 @@ /* eslint-disable import/prefer-default-export */ import yup from './yup' -import { nameRule } from './common' +import { descriptionRule, nameRule } from './common' -export const configBundleCreateSchema = yup.object().shape({ +export const configBundleSchema = yup.object().shape({ name: nameRule, -}) - -export const configBundlePatchSchema = yup.object().shape({ - name: nameRule.optional().nullable(), - environment: yup.array().optional().nullable().label('config-bundles:environment'), + description: descriptionRule, }) diff --git a/web/crux-ui/src/validations/container.ts b/web/crux-ui/src/validations/container.ts index 07f939077a..6c70158ef0 100644 --- a/web/crux-ui/src/validations/container.ts +++ b/web/crux-ui/src/validations/container.ts @@ -1,4 +1,4 @@ -import { UID_MAX } from '@app/const' +import { UID_MAX, UID_MIN } from '@app/const' import { CONTAINER_DEPLOYMENT_STRATEGY_VALUES, CONTAINER_EXPOSE_STRATEGY_VALUES, @@ -8,7 +8,7 @@ import { CONTAINER_STATE_VALUES, CONTAINER_VOLUME_TYPE_VALUES, ContainerConfigExposeStrategy, - ContainerConfigPortRange, + ContainerPortRange, ContainerDeploymentStrategyType, ContainerLogDriverType, ContainerNetworkMode, @@ -94,29 +94,33 @@ const portNumberRule = portNumberBaseRule.nullable().required() const exposeRule = yup .mixed() .oneOf([...CONTAINER_EXPOSE_STRATEGY_VALUES]) - .default('none') - .required() + .default(null) + .nullable() + .optional() .label('container:common.expose') const restartPolicyRule = yup .mixed() .oneOf([...CONTAINER_RESTART_POLICY_TYPE_VALUES]) - .default('no') - .required() + .default(null) + .nullable() + .optional() .label('container:dagent.restartPolicy') const networkModeRule = yup .mixed() .oneOf([...CONTAINER_NETWORK_MODE_VALUES]) - .default('bridge') - .required() + .default(null) + .nullable() + .optional() .label('container:dagent.networkMode') const deploymentStrategyRule = yup .mixed() .oneOf([...CONTAINER_DEPLOYMENT_STRATEGY_VALUES]) - .default('recreate') - .required() + .default(null) + .nullable() + .optional() .label('container:crane.deploymentStrategy') const logDriverRule = yup @@ -141,7 +145,7 @@ const configContainerRule = yup path: yup.string().required().label('container:common.path'), keepFiles: yup.boolean().default(false).required().label('container:common.keepFiles'), }) - .default({}) + .default(null) .nullable() .optional() .label('container:common.configContainer') @@ -154,7 +158,7 @@ const healthCheckConfigRule = yup readinessProbe: yup.string().nullable().optional().label('container:crane.readinessProbe'), startupProbe: yup.string().nullable().optional().label('container:crane.startupProbe'), }) - .default({}) + .default(null) .optional() .nullable() .label('container:crane.healthCheckConfig') @@ -180,7 +184,7 @@ const resourceConfigRule = yup .optional(), livenessProbe: yup.string().nullable().label('container:crane.livenessProbe'), }) - .default({}) + .default(null) .nullable() .optional() .label('container:crane.resourceConfig') @@ -201,15 +205,15 @@ const storageRule = yup bucket: storageFieldRule.label('container:common.bucketPath'), path: storageFieldRule.label('container:common.volume'), }) - .default({}) + .default(null) .nullable() .optional() .label('container:common.storage') const createOverlapTest = ( schema: yup.NumberSchema, - portRanges: ContainerConfigPortRange[], - field: Exclude, + portRanges: ContainerPortRange[], + field: Exclude, ) => // eslint-disable-next-line no-template-curly-in-string schema.test('port-range-overlap', 'container:validation.pathOverlapsSomePortranges', value => @@ -247,7 +251,7 @@ const portConfigRule = yup.mixed().when('portRanges', ([portRanges]) => { external: createOverlapTest(portNumberOptionalRule, portRanges, 'external').label('container:common.external'), }), ) - .default([]) + .default(null) .nullable() .optional() .label('container:common.ports') @@ -274,7 +278,7 @@ const portRangeConfigRule = yup .required(), }), ) - .default([]) + .default(null) .nullable() .optional() .label('container:common.portRanges') @@ -293,7 +297,7 @@ const volumeConfigRule = yup type: volumeTypeRule, }), ) - .default([]) + .default(null) .nullable() .optional() .label('container:common.volumes') @@ -317,7 +321,7 @@ const initContainerRule = yup volumes: initContainerVolumeLinkRule.default([]).nullable().label('container:common.volumes'), }), ) - .default([]) + .default(null) .nullable() .optional() .label('container:common.initContainer') @@ -328,7 +332,7 @@ const logConfigRule = yup driver: logDriverRule, options: uniqueKeyValuesSchema.default([]).nullable().label('container:dagent.options'), }) - .default({}) + .default(null) .nullable() .optional() .label('container:dagent.logConfig') @@ -340,7 +344,7 @@ const markerRule = yup service: uniqueKeyValuesSchema.default([]).nullable().label('container:crane.service'), ingress: uniqueKeyValuesSchema.default([]).nullable().label('container:crane.ingress'), }) - .default({}) + .default(null) .nullable() .optional() @@ -365,6 +369,9 @@ const routingRule = yup .optional() .default(null), ) + .default(null) + .nullable() + .optional() .label('container:common.routing') const createMetricsPortRule = (ports: ContainerPort[]) => { @@ -396,9 +403,9 @@ const metricsRule = yup.mixed().when(['ports'], ([ports]) => { port: portRule, }), }) + .default(null) .nullable() .optional() - .default(null) .label('container:crane.metrics') }) @@ -409,7 +416,7 @@ const expectedContainerStateRule = yup timeout: yup.number().default(null).nullable().min(0).label('container:dagent.expectedStateTimeout'), exitCode: yup.number().default(0).nullable().min(-127).max(128).label('container:dagent.expectedExitCode'), }) - .default({}) + .default(null) .nullable() .optional() .label('container:dagent.expectedState') @@ -444,7 +451,7 @@ const validateEnvironmentRule = (rule: EnvironmentRule, index: number, env: Uniq } const testEnvironment = (imageLabels: Record) => (arr: UniqueKeyValue[]) => { - if (!imageLabels) { + if (!imageLabels || !arr) { return true } @@ -486,41 +493,46 @@ const testEnvironment = (imageLabels: Record) => (arr: UniqueKey const createContainerConfigBaseSchema = (imageLabels: Record) => yup.object().shape({ - name: matchNoWhitespace(yup.string().required().label('container:common.containerName')), + name: matchNoWhitespace(yup.string().default(null).nullable().optional().label('container:common.containerName')), environment: uniqueKeyValuesSchema - .default([]) + .default(null) .nullable() + .optional() .label('container:common.environment') .test('ruleValidation', 'errors:yup.mixed.required', testEnvironment(imageLabels)), routing: routingRule, expose: exposeRule, - user: yup.number().default(null).min(-1).max(UID_MAX).nullable().label('container:common.user'), + user: yup.number().default(null).min(UID_MIN).max(UID_MAX).nullable().optional().label('container:common.user'), workingDirectory: yup.string().default(null).nullable().optional().label('container:common.workingDirectory'), - tty: yup.boolean().default(false).required().label('container:common.tty'), + tty: yup.boolean().default(null).nullable().optional().label('container:common.tty'), configContainer: configContainerRule, ports: portConfigRule, portRanges: portRangeConfigRule, volumes: volumeConfigRule, - commands: shellCommandSchema.default([]).nullable(), - args: shellCommandSchema.default([]).nullable(), + commands: shellCommandSchema.default(null).nullable().optional(), + args: shellCommandSchema.default(null).nullable().optional(), initContainers: initContainerRule, - capabilities: uniqueKeyValuesSchema.default([]).nullable().label('container:common.capabilities'), + capabilities: uniqueKeyValuesSchema.default(null).nullable().optional().label('container:common.capabilities'), storage: storageRule, // dagent: logConfig: logConfigRule, restartPolicy: restartPolicyRule, networkMode: networkModeRule, - networks: uniqueKeysOnlySchema.default([]).nullable().label('container:dagent.networks'), - dockerLabels: uniqueKeyValuesSchema.default([]).nullable().label('container:dagent.dockerLabels'), + networks: uniqueKeysOnlySchema.default(null).nullable().optional().label('container:dagent.networks'), + dockerLabels: uniqueKeyValuesSchema.default(null).nullable().optional().label('container:dagent.dockerLabels'), expectedState: expectedContainerStateRule, // crane deploymentStrategy: deploymentStrategyRule, - customHeaders: uniqueKeysOnlySchema.default([]).nullable().label('container:crane.customHeaders'), - proxyHeaders: yup.boolean().default(false).required().label('container:crane.proxyHeaders'), - useLoadBalancer: yup.boolean().default(false).required().label('container:crane.useLoadBalancer'), - extraLBAnnotations: uniqueKeyValuesSchema.default([]).nullable().label('container:crane.extraLBAnnotations'), + customHeaders: uniqueKeysOnlySchema.default(null).nullable().optional().label('container:crane.customHeaders'), + proxyHeaders: yup.boolean().default(null).nullable().optional().label('container:crane.proxyHeaders'), + useLoadBalancer: yup.boolean().default(null).nullable().optional().label('container:crane.useLoadBalancer'), + extraLBAnnotations: uniqueKeyValuesSchema + .default(null) + .nullable() + .optional() + .label('container:crane.extraLBAnnotations'), healthCheckConfig: healthCheckConfigRule, resourceConfig: resourceConfigRule, labels: markerRule.label('container:crane.labels'), @@ -530,10 +542,10 @@ const createContainerConfigBaseSchema = (imageLabels: Record) => export const createContainerConfigSchema = (imageLabels: Record) => createContainerConfigBaseSchema(imageLabels).shape({ - secrets: uniqueKeySchema.default([]).nullable().label('container:common.secrets'), + secrets: uniqueKeySchema.default(null).nullable().optional().label('container:common.secrets'), }) -export const createMergedContainerConfigSchema = (imageLabels: Record) => +export const createConcreteContainerConfigSchema = (imageLabels: Record) => createContainerConfigBaseSchema(imageLabels).shape({ - secrets: uniqueKeyValuesSchema.default([]).nullable().label('container:common.secrets'), + secrets: uniqueKeyValuesSchema.default(null).nullable().optional().label('container:common.secrets'), }) diff --git a/web/crux-ui/src/validations/deployment.ts b/web/crux-ui/src/validations/deployment.ts index 0f03a89d61..88f9eeca76 100644 --- a/web/crux-ui/src/validations/deployment.ts +++ b/web/crux-ui/src/validations/deployment.ts @@ -1,6 +1,6 @@ import yup from './yup' import { nameRule } from './common' -import { createMergedContainerConfigSchema, uniqueKeyValuesSchema } from './container' +import { createConcreteContainerConfigSchema, uniqueKeyValuesSchema } from './container' export const prefixRule = yup .string() @@ -10,8 +10,9 @@ export const prefixRule = yup .label('common:prefix') export const updateDeploymentSchema = yup.object().shape({ - note: yup.string().label('common:note'), + note: yup.string().optional().nullable().label('common:note'), prefix: prefixRule, + protected: yup.bool().required(), }) export const createDeploymentSchema = updateDeploymentSchema.concat( @@ -40,7 +41,7 @@ export const startDeploymentSchema = yup.object({ instances: yup .array( yup.object().shape({ - config: createMergedContainerConfigSchema(null), + config: createConcreteContainerConfigSchema(null), }), ) .ensure() diff --git a/web/crux-ui/src/validations/instance.ts b/web/crux-ui/src/validations/instance.ts index aaec2ecd66..e4f038f645 100644 --- a/web/crux-ui/src/validations/instance.ts +++ b/web/crux-ui/src/validations/instance.ts @@ -1,12 +1,12 @@ -import { MergedContainerConfigData } from '@app/models' +import { ConcreteContainerConfigData } from '@app/models' import { Translate } from 'next-translate' import { getConfigFieldErrorsForSchema, ContainerConfigValidationErrors } from './image' -import { createMergedContainerConfigSchema } from './container' +import { createConcreteContainerConfigSchema } from './container' // eslint-disable-next-line import/prefer-default-export -export const getMergedContainerConfigFieldErrors = ( - newConfig: MergedContainerConfigData, +export const getConcreteContainerConfigFieldErrors = ( + newConfig: ConcreteContainerConfigData, validation: Record, t: Translate, ): ContainerConfigValidationErrors => - getConfigFieldErrorsForSchema(createMergedContainerConfigSchema(validation), newConfig, t) + getConfigFieldErrorsForSchema(createConcreteContainerConfigSchema(validation), newConfig, t) diff --git a/web/crux-ui/src/websockets/common.ts b/web/crux-ui/src/websockets/common.ts index 81a8725944..15c48f75be 100644 --- a/web/crux-ui/src/websockets/common.ts +++ b/web/crux-ui/src/websockets/common.ts @@ -25,7 +25,7 @@ export type WsMessageCallback = (message: T) => void export type WsErrorHandler = (message: WsErrorMessage) => void -export type WebSocketSendMessage = (message: WsMessage) => boolean +export type WebSocketClientSendMessage = (message: WsMessage) => boolean export type WebSocketClientOptions = { onOpen?: VoidFunction diff --git a/web/crux-ui/src/websockets/websocket-client-endpoint.ts b/web/crux-ui/src/websockets/websocket-client-endpoint.ts index f2fd5ea8dc..b0e3f652ab 100644 --- a/web/crux-ui/src/websockets/websocket-client-endpoint.ts +++ b/web/crux-ui/src/websockets/websocket-client-endpoint.ts @@ -1,7 +1,7 @@ -import { WebSocketClientOptions, WebSocketSendMessage, WsMessage, WsMessageCallback } from './common' +import { WebSocketClientOptions, WebSocketClientSendMessage, WsMessage, WsMessageCallback } from './common' class WebSocketClientEndpoint { - private sendClientMessage: WebSocketSendMessage = null + private sendClientMessage: WebSocketClientSendMessage = null private callbacks: Map> = new Map() @@ -78,7 +78,7 @@ class WebSocketClientEndpoint { callbacks?.forEach(it => it(msg.data)) } - onSubscribed(sendClientMessage: WebSocketSendMessage): void { + onSubscribed(sendClientMessage: WebSocketClientSendMessage): void { this.sendClientMessage = sendClientMessage if (this.readyStateChanged) { diff --git a/web/crux-ui/src/websockets/websocket-client-route.ts b/web/crux-ui/src/websockets/websocket-client-route.ts index 01b755f7a6..b95812ed95 100644 --- a/web/crux-ui/src/websockets/websocket-client-route.ts +++ b/web/crux-ui/src/websockets/websocket-client-route.ts @@ -1,5 +1,5 @@ import { Logger } from '@app/logger' -import { SubscriptionMessage, SubscriptionMessageType, WebSocketSendMessage, WsMessage } from './common' +import { SubscriptionMessage, SubscriptionMessageType, WebSocketClientSendMessage, WsMessage } from './common' import WebSocketClientEndpoint from './websocket-client-endpoint' type WebSocketClientRouteState = 'subscribed' | 'unsubscribed' | 'in-progress' @@ -7,7 +7,7 @@ type WebSocketClientRouteState = 'subscribed' | 'unsubscribed' | 'in-progress' class WebSocketClientRoute { private logger: Logger - private readonly sendClientMessage: WebSocketSendMessage + private readonly sendClientMessage: WebSocketClientSendMessage private state: WebSocketClientRouteState = 'unsubscribed' @@ -19,7 +19,7 @@ class WebSocketClientRoute { constructor( logger: Logger, - private readonly sendMessage: WebSocketSendMessage, + private readonly sendMessage: WebSocketClientSendMessage, private endpointPath: string, ) { this.logger = logger.derive(endpointPath) diff --git a/web/crux/prisma/migrations/20241017094935_config_rework/migration.sql b/web/crux/prisma/migrations/20241017094935_config_rework/migration.sql index eb812731c4..920069a33e 100644 --- a/web/crux/prisma/migrations/20241017094935_config_rework/migration.sql +++ b/web/crux/prisma/migrations/20241017094935_config_rework/migration.sql @@ -62,20 +62,38 @@ ALTER COLUMN "proxyHeaders" DROP NOT NULL, ALTER COLUMN "tty" DROP NOT NULL, ALTER COLUMN "useLoadBalancer" DROP NOT NULL; +-- Image + +-- add missing configs +SELECT "i"."id" +INTO "_prisma_migrations_ConfiglessImages" +FROM "Image" AS "i" +WHERE "i"."configId" IS NULL; + +UPDATE "Image" +SET "configId" = gen_random_uuid() +WHERE "Image"."id" IN (SELECT id FROM "_prisma_migrations_ConfiglessImages"); + +INSERT INTO "ContainerConfig" +("id", "type", "updatedAt", "updatedBy") +SELECT "i"."configId", 'image'::"ContainerConfigType", "i"."updatedAt", "i"."updatedBy" +FROM "Image" AS "i" +WHERE "i"."id" IN (SELECT id FROM "_prisma_migrations_ConfiglessImages"); + +DROP TABLE "_prisma_migrations_ConfiglessImages"; + -- ConfigBundle ALTER TABLE "ConfigBundle" ADD COLUMN "configId" UUID; UPDATE "ConfigBundle" -SET "configId" = gen_random_uuid() -WHERE "data" IS NOT NULL; +SET "configId" = gen_random_uuid(); INSERT INTO "ContainerConfig" ("id", "type", "updatedAt", "updatedBy", "environment") SELECT "configId", 'configBundle'::"ContainerConfigType", "updatedAt", "updatedBy", "data" -FROM "ConfigBundle" -WHERE "data" IS NOT NULL; +FROM "ConfigBundle"; ALTER TABLE "ConfigBundle" DROP COLUMN "data"; @@ -86,14 +104,12 @@ ALTER TABLE "Deployment" ADD COLUMN "configId" UUID; UPDATE "Deployment" -SET "configId" = gen_random_uuid() -WHERE "environment" IS NOT NULL; +SET "configId" = gen_random_uuid(); INSERT INTO "ContainerConfig" ("id", "type", "updatedAt", "updatedBy", "environment") SELECT "configId", 'deployment'::"ContainerConfigType", "updatedAt", "updatedBy", "environment" -FROM "Deployment" -WHERE "environment" IS NOT NULL; +FROM "Deployment"; ALTER TABLE "Deployment" @@ -170,6 +186,46 @@ DROP COLUMN "updatedAt"; DROP TABLE "InstanceContainerConfig"; +-- add missing configs +SELECT "i"."id" +INTO "_prisma_migrations_ConfiglessInstances" +FROM "Instance" AS "i" +WHERE "i"."configId" IS NULL; + +UPDATE "Instance" +SET "configId" = gen_random_uuid() +WHERE "Instance"."id" IN (SELECT id FROM "_prisma_migrations_ConfiglessInstances"); + +INSERT INTO "ContainerConfig" +("id", "type", "updatedAt", "updatedBy") +SELECT "i"."configId", 'instance'::"ContainerConfigType", "d"."updatedAt", "d"."updatedBy" +FROM "Instance" AS "i" +INNER JOIN "Deployment" AS "d" ON "d".id = "i"."deploymentId" +WHERE "i"."id" IN (SELECT id FROM "_prisma_migrations_ConfiglessInstances"); + +DROP TABLE "_prisma_migrations_ConfiglessInstances"; + +-- AlterTable +ALTER TABLE "Deployment" ADD COLUMN "deployedAt" TIMESTAMPTZ(6), +ADD COLUMN "deployedBy" UUID; + +UPDATE "Deployment" +SET "deployedAt" = "d"."updatedAt" +FROM (select "id", "updatedAt" FROM "Deployment") AS "d" +WHERE "d"."id" = "Deployment"."id"; + +-- AlterTable +ALTER TABLE "ConfigBundle" ALTER COLUMN "configId" SET NOT NULL; + +-- AlterTable +ALTER TABLE "Deployment" ALTER COLUMN "configId" SET NOT NULL; + +-- AlterTable +ALTER TABLE "Image" ALTER COLUMN "configId" SET NOT NULL; + +-- AlterTable +ALTER TABLE "Instance" ALTER COLUMN "configId" SET NOT NULL; + -- CreateIndex CREATE UNIQUE INDEX "ConfigBundle_configId_key" ON "ConfigBundle"("configId"); diff --git a/web/crux/prisma/schema.prisma b/web/crux/prisma/schema.prisma index ece7a7ba44..57ac00b025 100644 --- a/web/crux/prisma/schema.prisma +++ b/web/crux/prisma/schema.prisma @@ -220,8 +220,8 @@ model Image { version Version @relation(fields: [versionId], references: [id], onDelete: Cascade) versionId String @db.Uuid - config ContainerConfig? @relation(fields: [configId], references: [id], onDelete: SetNull) - configId String? @unique @db.Uuid + config ContainerConfig @relation(fields: [configId], references: [id], onDelete: Cascade) + configId String @unique @db.Uuid instances Instance[] } @@ -319,6 +319,8 @@ model Deployment { createdBy String @db.Uuid updatedAt DateTime @updatedAt @db.Timestamptz(6) updatedBy String? @db.Uuid + deployedAt DateTime? @db.Timestamptz(6) + deployedBy String? @db.Uuid note String? prefix String? @@ -332,8 +334,8 @@ model Deployment { node Node @relation(fields: [nodeId], references: [id], onDelete: Cascade) nodeId String @db.Uuid - config ContainerConfig? @relation(fields: [configId], references: [id], onDelete: SetNull) - configId String? @unique @db.Uuid + config ContainerConfig @relation(fields: [configId], references: [id], onDelete: Cascade) + configId String @unique @db.Uuid instances Instance[] events DeploymentEvent[] @@ -365,8 +367,8 @@ model Instance { image Image @relation(fields: [imageId], references: [id], onDelete: Cascade) imageId String @db.Uuid - config ContainerConfig? @relation(fields: [configId], references: [id], onDelete: SetNull) - configId String? @unique @db.Uuid + config ContainerConfig @relation(fields: [configId], references: [id], onDelete: Cascade) + configId String @unique @db.Uuid } model DeploymentEvent { @@ -650,8 +652,8 @@ model ConfigBundle { team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) teamId String @db.Uuid - config ContainerConfig? @relation(fields: [configId], references: [id], onDelete: SetNull) - configId String? @unique @db.Uuid + config ContainerConfig? @relation(fields: [configId], references: [id], onDelete: Cascade) + configId String @unique @db.Uuid deployments ConfigBundleOnDeployments[] diff --git a/web/crux/proto/agent.proto b/web/crux/proto/agent.proto index 90ae346d5b..8b6e43eb40 100644 --- a/web/crux/proto/agent.proto +++ b/web/crux/proto/agent.proto @@ -36,7 +36,6 @@ service Agent { rpc ContainerLogStream(stream common.ContainerLogMessage) returns (common.Empty); - // one-shot requests rpc SecretList(common.ListSecretsResponse) returns (common.Empty); rpc DeleteContainers(common.Empty) returns (common.Empty); @@ -57,7 +56,7 @@ message AgentInfo { message AgentCommand { oneof command { - VersionDeployRequest deploy = 1; + DeployRequest deploy = 1; ContainerStateRequest containerState = 2; ContainerDeleteRequest containerDelete = 3; DeployRequestLegacy deployLegacy = 4; @@ -92,31 +91,20 @@ message AgentCommandError { */ message DeployResponse { bool started = 1; } -message VersionDeployRequest { +message DeployRequest { string id = 1; string versionName = 2; string releaseNotes = 3; + string prefix = 4; - repeated DeployRequest requests = 4; + map secrets = 5; + repeated DeployWorkloadRequest requests = 6; } /* * Request for a keys of existing secrets in a prefix, eg. namespace */ -message ListSecretsRequest { common.ContainerIdentifier container = 1; } - -/** - * Deploys a single container - * - */ -message InstanceConfig { - /* - prefix mapped into host folder structure, - used as namespace id - */ - string prefix = 1; - optional string mountPath = 2; // mount path of instance (docker only) - map environment = 3; // environment variable map - optional string repositoryPrefix = 4; // registry repo prefix +message ListSecretsRequest { + common.ContainerOrPrefix target = 1; } message RegistryAuth { @@ -241,25 +229,20 @@ message CommonContainerConfig { repeated InitContainer initContainers = 1007; } -message DeployRequest { +message DeployWorkloadRequest { string id = 1; string containerName = 2; - /* InstanceConfig is set for multiple containers */ - InstanceConfig instanceConfig = 3; - /* ContainerConfigs */ - optional CommonContainerConfig common = 4; - optional DagentContainerConfig dagent = 5; - optional CraneContainerConfig crane = 6; + optional CommonContainerConfig common = 3; + optional DagentContainerConfig dagent = 4; + optional CraneContainerConfig crane = 5; - /* Runtime info and requirements of a container */ - optional string runtimeConfig = 7; - optional string registry = 8; - string imageName = 9; - string tag = 10; + optional string registry = 6; + string imageName = 7; + string tag = 8; - optional RegistryAuth registryAuth = 11; + optional RegistryAuth registryAuth = 9; } message ContainerStateRequest { diff --git a/web/crux/proto/common.proto b/web/crux/proto/common.proto index 35c6114c61..98f5aa2c53 100644 --- a/web/crux/proto/common.proto +++ b/web/crux/proto/common.proto @@ -187,12 +187,17 @@ message KeyValue { string value = 101; } +message ContainerOrPrefix { + oneof target { + common.ContainerIdentifier container = 1; + string prefix = 2; + } +} + message ListSecretsResponse { - string prefix = 1; - string name = 2; + ContainerOrPrefix target = 1; string publicKey = 3; - bool hasKeys = 4; - repeated string keys = 5; + repeated string keys = 4; } message UniqueKey { @@ -218,8 +223,5 @@ message ContainerCommandRequest { } message DeleteContainersRequest { - oneof target { - common.ContainerIdentifier container = 201; - string prefix = 202; - } + ContainerOrPrefix target = 1; } diff --git a/web/crux/src/app/agent/agent.connection-strategy.provider.ts b/web/crux/src/app/agent/agent.connection-strategy.provider.ts index 64e79f24df..db469506a7 100644 --- a/web/crux/src/app/agent/agent.connection-strategy.provider.ts +++ b/web/crux/src/app/agent/agent.connection-strategy.provider.ts @@ -1,11 +1,10 @@ import { Inject, Injectable, Logger, forwardRef } from '@nestjs/common' import { JwtService } from '@nestjs/jwt' import { AgentToken } from 'src/domain/agent-token' -import { CruxUnauthorizedException } from 'src/exception/crux-exception' +import { CruxBadRequestException, CruxUnauthorizedException } from 'src/exception/crux-exception' import GrpcNodeConnection from 'src/shared/grpc-node-connection' import AgentService from './agent.service' import AgentConnectionInstallStrategy from './connection-strategies/agent.connection.install.strategy' -import AgentConnectionLegacyStrategy from './connection-strategies/agent.connection.legacy.strategy' import AgentConnectionStrategy from './connection-strategies/agent.connection.strategy' import AgentConnectionUpdateStrategy from './connection-strategies/agent.connection.update.strategy' @@ -18,7 +17,6 @@ export default class AgentConnectionStrategyProvider { private readonly service: AgentService, private readonly jwtService: JwtService, private readonly defaultStrategy: AgentConnectionStrategy, - private readonly legacy: AgentConnectionLegacyStrategy, private readonly install: AgentConnectionInstallStrategy, private readonly update: AgentConnectionUpdateStrategy, ) {} @@ -27,8 +25,10 @@ export default class AgentConnectionStrategyProvider { const token = this.jwtService.decode(connection.jwt) as AgentToken if (!token.version) { - this.logger.verbose(`${connection.nodeId} - No version found in the token. Using legacy strategy.`) - return this.legacy + this.logger.verbose(`${connection.nodeId} - No version found in the token. Declining connection.`) + throw new CruxBadRequestException({ + message: 'Legacy agents are not supported.', + }) } if (token.type === 'install') { @@ -60,7 +60,6 @@ export default class AgentConnectionStrategyProvider { export const AGENT_STRATEGY_TYPES = [ AgentConnectionStrategy, - AgentConnectionLegacyStrategy, AgentConnectionInstallStrategy, AgentConnectionUpdateStrategy, AgentConnectionStrategyProvider, diff --git a/web/crux/src/app/agent/agent.module.ts b/web/crux/src/app/agent/agent.module.ts index dda633a024..a4d0d3fc82 100644 --- a/web/crux/src/app/agent/agent.module.ts +++ b/web/crux/src/app/agent/agent.module.ts @@ -15,7 +15,13 @@ import AgentController from './agent.grpc.controller' import AgentService from './agent.service' @Module({ - imports: [HttpModule, CruxJwtModule, ImageModule, ContainerModule, forwardRef(() => DeployModule)], + imports: [ + HttpModule, + CruxJwtModule, + forwardRef(() => ImageModule), + forwardRef(() => ContainerModule), + forwardRef(() => DeployModule), + ], exports: [AgentService], controllers: [AgentController], providers: [ diff --git a/web/crux/src/app/agent/agent.service.ts b/web/crux/src/app/agent/agent.service.ts index 00f940d47a..63fea60676 100755 --- a/web/crux/src/app/agent/agent.service.ts +++ b/web/crux/src/app/agent/agent.service.ts @@ -55,7 +55,6 @@ import DeployService from '../deploy/deploy.service' import { DagentTraefikOptionsDto, NodeConnectionStatus, NodeScriptTypeDto } from '../node/node.dto' import AgentConnectionStrategyProvider from './agent.connection-strategy.provider' import { AgentKickReason } from './agent.dto' -import AgentConnectionLegacyStrategy from './connection-strategies/agent.connection.legacy.strategy' @Injectable() export default class AgentService { @@ -534,15 +533,6 @@ export default class AgentService { const agent = await strategy.execute(connection, request) this.logger.verbose('Connection strategy completed') - if (agent.id === AgentConnectionLegacyStrategy.LEGACY_NONCE) { - // self destruct message is already in the queue - // we just have to return the command channel - - // command channel is already completed so no need for onDisconnected() call - this.logger.verbose('Crashing legacy agent intercepted.') - return agent.onConnected(AgentConnectionLegacyStrategy.CONNECTION_STATUS_LISTENER) - } - await this.onAgentConnectionStatusChange(agent, agent.outdated ? 'outdated' : 'connected') return agent.onConnected(it => this.onAgentConnectionStatusChange(agent, it)) diff --git a/web/crux/src/app/agent/connection-strategies/agent.connection.legacy.strategy.ts b/web/crux/src/app/agent/connection-strategies/agent.connection.legacy.strategy.ts deleted file mode 100644 index 3a77afb365..0000000000 --- a/web/crux/src/app/agent/connection-strategies/agent.connection.legacy.strategy.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { Injectable } from '@nestjs/common' -import { Subject } from 'rxjs' -import { Agent } from 'src/domain/agent' -import { generateAgentToken } from 'src/domain/agent-token' -import { CruxConflictException } from 'src/exception/crux-exception' -import { AgentInfo, CloseReason } from 'src/grpc/protobuf/proto/agent' -import GrpcNodeConnection from 'src/shared/grpc-node-connection' -import AgentConnectionStrategy from './agent.connection.strategy' - -@Injectable() -export default class AgentConnectionLegacyStrategy extends AgentConnectionStrategy { - override async execute(connection: GrpcNodeConnection, info: AgentInfo): Promise { - const token = this.parseToken(connection, info) - const node = await this.findNodeById(token.sub) - - const connectedAgent = this.service.getById(node.id) - - if (node.token?.nonce === AgentConnectionLegacyStrategy.LEGACY_NONCE) { - if (this.service.agentVersionSupported(info.version)) { - // incoming updated agent with legacy token - const incomingAgent = await this.createAgent({ - connection, - info, - node, - outdated: false, - callbackTimeout: this.callbackTimeout, - }) - - if (connectedAgent) { - if (!connectedAgent.outdated) { - // duplicated connection - throw new CruxConflictException({ - message: 'Agent is already connected.', - property: 'id', - }) - } - - await this.deleteOldAgentContainer(connectedAgent, incomingAgent) - } - // generate new token for the now up to date agent - const replacement = this.service.generateConnectionTokenFor(incomingAgent.id, node.createdBy) - incomingAgent.replaceToken(replacement) - return incomingAgent - } - - this.throwIfConnected(node.id) - - // simple legacy agent - return await this.createAgent({ - connection, - info, - node, - outdated: true, - callbackTimeout: this.callbackTimeout, - }) - } - - // this legacy token is already replaced - // we send a shutdown to the incoming agent - info.id = AgentConnectionLegacyStrategy.LEGACY_NONCE - const legacyToken = generateAgentToken(AgentConnectionLegacyStrategy.LEGACY_NONCE, 'install') - const signedLegacyToken = this.jwtService.sign(legacyToken) - connection.onTokenReplaced(legacyToken, signedLegacyToken) - - const incomingAgent = new Agent({ - connection, - eventChannel: new Subject(), - info, - outdated: true, - callbackTimeout: this.callbackTimeout, - }) - - await this.deleteOldAgentContainer(incomingAgent, connectedAgent) - return incomingAgent - } - - private async deleteOldAgentContainer(oldAgent: Agent, newAgent: Agent): Promise { - this.logger.verbose('Sending shutdown to the outdated agent.') - oldAgent.close(CloseReason.SHUTDOWN) - - const containerName = newAgent?.info?.containerName - if (containerName) { - // remove the old agent's container - - this.logger.verbose("Removing old agent's container.") - await newAgent.deleteContainers({ - container: { - prefix: '', - name: `${containerName}-update`, - }, - }) - } - } - - static LEGACY_NONCE = '00000000-0000-0000-0000-000000000000' - - static CONNECTION_STATUS_LISTENER = () => {} -} diff --git a/web/crux/src/app/config.bundle/config.bundle.dto.ts b/web/crux/src/app/config.bundle/config.bundle.dto.ts index 8b36b4062b..5ba21da51d 100644 --- a/web/crux/src/app/config.bundle/config.bundle.dto.ts +++ b/web/crux/src/app/config.bundle/config.bundle.dto.ts @@ -13,6 +13,9 @@ export class ConfigBundleDto extends BasicConfigBundleDto { @IsString() @IsOptional() description?: string + + @IsUUID() + configId: string } export class ConfigBundleDetailsDto extends ConfigBundleDto { diff --git a/web/crux/src/app/config.bundle/config.bundle.http.controller.ts b/web/crux/src/app/config.bundle/config.bundle.http.controller.ts index 64de578b4e..2b2ec38368 100644 --- a/web/crux/src/app/config.bundle/config.bundle.http.controller.ts +++ b/web/crux/src/app/config.bundle/config.bundle.http.controller.ts @@ -30,7 +30,6 @@ import { IdentityFromRequest } from '../token/jwt-auth.guard' import { ConfigBundleDetailsDto, ConfigBundleDto, - ConfigBundleOptionDto, CreateConfigBundleDto, PatchConfigBundleDto, } from './config.bundle.dto' @@ -69,24 +68,6 @@ export default class ConfigBundlesHttpController { return this.service.getConfigBundles(teamSlug) } - @Get('options') - @HttpCode(HttpStatus.OK) - @ApiOperation({ - description: 'Response should include `id`, and `name`.', - summary: 'Fetch the name and ID of available config bundle options.', - }) - @ApiOkResponse({ - type: ConfigBundleOptionDto, - isArray: true, - description: 'Name and ID of config bundle options listed.', - }) - @ApiBadRequestResponse({ description: 'Bad request for config bundle options.' }) - @ApiForbiddenResponse({ description: 'Unauthorized request for config bundle options.' }) - @ApiNotFoundResponse({ description: 'Config bundle options not found.' }) - async getConfigBundleOptions(@TeamSlug() teamSlug: string): Promise { - return this.service.getConfigBundleOptions(teamSlug) - } - @Get(ROUTE_CONFIG_BUNDLE_ID) @HttpCode(HttpStatus.OK) @ApiOperation({ @@ -131,7 +112,7 @@ export default class ConfigBundlesHttpController { @Patch(ROUTE_CONFIG_BUNDLE_ID) @HttpCode(HttpStatus.NO_CONTENT) @ApiOperation({ - description: 'Updates a config bundle. Request must include `id`, `name`, and `data`', + description: 'Updates a config bundle.', summary: 'Modify a config bundle.', }) @ApiNoContentResponse({ description: 'Config bundle updated.' }) @@ -140,7 +121,7 @@ export default class ConfigBundlesHttpController { @ApiNotFoundResponse({ description: 'Config bundle not found.' }) @ApiConflictResponse({ description: 'Config bundle name taken.' }) @UuidParams(PARAM_CONFIG_BUNDLE_ID) - async updateConfigBundle( + async patchConfigBundle( @TeamSlug() _: string, @ConfigBundleId() id: string, @Body() request: PatchConfigBundleDto, diff --git a/web/crux/src/app/config.bundle/config.bundle.mapper.ts b/web/crux/src/app/config.bundle/config.bundle.mapper.ts index ca33e84f59..5436c5b4c1 100644 --- a/web/crux/src/app/config.bundle/config.bundle.mapper.ts +++ b/web/crux/src/app/config.bundle/config.bundle.mapper.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common' +import { forwardRef, Inject, Injectable } from '@nestjs/common' import { ConfigBundle, ContainerConfig } from '@prisma/client' import { ContainerConfigData } from 'src/domain/container' import ContainerMapper from '../container/container.mapper' @@ -6,21 +6,25 @@ import { ConfigBundleDetailsDto, ConfigBundleDto } from './config.bundle.dto' @Injectable() export default class ConfigBundleMapper { - constructor(private readonly containerMapper: ContainerMapper) {} + constructor( + @Inject(forwardRef(() => ContainerMapper)) + private readonly containerMapper: ContainerMapper, + ) {} - listItemToDto(configBundle: ConfigBundle): ConfigBundleDto { + toDto(it: ConfigBundle): ConfigBundleDto { return { - id: configBundle.id, - name: configBundle.name, - description: configBundle.description, + id: it.id, + name: it.name, + description: it.description, + configId: it.configId, } } detailsToDto(configBundle: ConfigBundleDetails): ConfigBundleDetailsDto { return { - ...this.listItemToDto(configBundle), + ...this.toDto(configBundle), config: this.containerMapper.configDataToDto( - configBundle.id, + configBundle.configId, 'configBundle', configBundle.config as any as ContainerConfigData, ), @@ -29,5 +33,5 @@ export default class ConfigBundleMapper { } type ConfigBundleDetails = ConfigBundle & { - config?: ContainerConfig + config: ContainerConfig } diff --git a/web/crux/src/app/config.bundle/config.bundle.module.ts b/web/crux/src/app/config.bundle/config.bundle.module.ts index ccd9208e3e..e6f070103e 100644 --- a/web/crux/src/app/config.bundle/config.bundle.module.ts +++ b/web/crux/src/app/config.bundle/config.bundle.module.ts @@ -1,5 +1,5 @@ import { HttpModule } from '@nestjs/axios' -import { Module } from '@nestjs/common' +import { forwardRef, Module } from '@nestjs/common' import KratosService from 'src/services/kratos.service' import PrismaService from 'src/services/prisma.service' import AuditLoggerModule from '../audit.logger/audit.logger.module' @@ -12,15 +12,9 @@ import ConfigBundleMapper from './config.bundle.mapper' import ConfigBundleService from './config.bundle.service' @Module({ - imports: [HttpModule, TeamModule, AuditLoggerModule, EditorModule, ContainerModule], + imports: [HttpModule, TeamModule, AuditLoggerModule, EditorModule, forwardRef(() => ContainerModule)], exports: [ConfigBundleMapper, ConfigBundleService], controllers: [ConfigBundlesHttpController], - providers: [ - ConfigBundleService, - PrismaService, - ConfigBundleMapper, - TeamRepository, - KratosService, - ], + providers: [ConfigBundleService, PrismaService, ConfigBundleMapper, TeamRepository, KratosService], }) export default class ConfigBundleModule {} diff --git a/web/crux/src/app/config.bundle/config.bundle.service.ts b/web/crux/src/app/config.bundle/config.bundle.service.ts index 87a08bc8a5..3bbeeb36ec 100644 --- a/web/crux/src/app/config.bundle/config.bundle.service.ts +++ b/web/crux/src/app/config.bundle/config.bundle.service.ts @@ -35,7 +35,7 @@ export default class ConfigBundleService { }, }) - return configBundles.map(it => this.mapper.listItemToDto(it)) + return configBundles.map(it => this.mapper.toDto(it)) } async getConfigBundleDetails(id: string): Promise { @@ -43,6 +43,9 @@ export default class ConfigBundleService { where: { id, }, + include: { + config: true, + }, }) return this.mapper.detailsToDto(configBundle) @@ -59,15 +62,13 @@ export default class ConfigBundleService { data: { name: req.name, description: req.description, - config: { - create: { - type: 'configBundle', - updatedBy: identity.id, - }, - }, + config: { create: { type: 'configBundle' } }, team: { connect: { id: teamId } }, createdBy: identity.id, }, + include: { + config: true, + }, }) return this.mapper.detailsToDto(configBundle) diff --git a/web/crux/src/app/container/container-config.http.service.ts b/web/crux/src/app/container/container-config.http.service.ts new file mode 100644 index 0000000000..d0900a9732 --- /dev/null +++ b/web/crux/src/app/container/container-config.http.service.ts @@ -0,0 +1,84 @@ +import { Body, Controller, Get, HttpCode, HttpStatus, Param, Patch, UseGuards } from '@nestjs/common' +import { + ApiBadRequestResponse, + ApiForbiddenResponse, + ApiNoContentResponse, + ApiNotFoundResponse, + ApiOkResponse, + ApiOperation, + ApiTags, +} from '@nestjs/swagger' +import { Identity } from '@ory/kratos-client' +import UuidParams from 'src/decorators/api-params.decorator' +import { IdentityFromRequest } from '../token/jwt-auth.guard' +import ContainerConfigService from './container-config.service' +import { ContainerConfigDto, ContainerConfigRelationsDto, PatchContainerConfigDto } from './container.dto' +import ContainerConfigTeamAccessGuard from './guards/container-config.team-access.guard' + +const PARAM_TEAM_SLUG = 'teamSlug' +const TeamSlug = () => Param(PARAM_TEAM_SLUG) +const PARAM_CONFIG_ID = 'configId' +const ConfigId = () => Param(PARAM_CONFIG_ID) + +const ROUTE_TEAM_SLUG = ':teamSlug' +const ROUTE_CONTAINER_CONFIGS = 'container-configurations' +const ROUTE_CONFIG_ID = ':configId' + +@Controller(`${ROUTE_TEAM_SLUG}/${ROUTE_CONTAINER_CONFIGS}`) +@ApiTags(ROUTE_CONTAINER_CONFIGS) +@UseGuards(ContainerConfigTeamAccessGuard) +export default class ContainerConfigHttpController { + constructor(private service: ContainerConfigService) {} + + @Get(ROUTE_CONFIG_ID) + @HttpCode(HttpStatus.OK) + @ApiOperation({ + description: 'Get details of a container configuration. Request must include `teamSlug` and `configId` in URL.', + summary: 'Retrieve details of a container configuration.', + }) + @ApiOkResponse({ type: ContainerConfigDto, description: 'Details of a container configuration.' }) + @ApiBadRequestResponse({ description: 'Bad request for container configuration details.' }) + @ApiForbiddenResponse({ description: 'Unauthorized request for container configuration details.' }) + @ApiNotFoundResponse({ description: 'Container configuration not found.' }) + @UuidParams(PARAM_CONFIG_ID) + async getConfigDetails(@TeamSlug() _: string, @ConfigId() configId: string): Promise { + return await this.service.getConfigDetails(configId) + } + + @Get(`${ROUTE_CONFIG_ID}/relations`) + @HttpCode(HttpStatus.OK) + @ApiOperation({ + description: + 'Get the relations of a container configuration. Request must include `teamSlug` and `configId` in URL.', + summary: 'Retrieve the relations of a container configuration.', + }) + @ApiOkResponse({ type: ContainerConfigRelationsDto, description: 'Relations of a container configuration.' }) + @ApiBadRequestResponse({ description: 'Bad request for container configuration relations.' }) + @ApiForbiddenResponse({ description: 'Unauthorized request for container configuration relations.' }) + @ApiNotFoundResponse({ description: 'Container configuration not found.' }) + @UuidParams(PARAM_CONFIG_ID) + async getConfigRelations(@TeamSlug() _: string, @ConfigId() configId: string): Promise { + return await this.service.getConfigRelations(configId) + } + + @Patch(ROUTE_CONFIG_ID) + @HttpCode(HttpStatus.NO_CONTENT) + @ApiOperation({ + description: 'Request must include `configId` and `teamSlug` in URL.', + summary: 'Update container configuration.', + }) + // @UseInterceptors(DeployPatchValidationInterceptor) + @ApiNoContentResponse({ description: 'Container configuration modified.' }) + @ApiBadRequestResponse({ description: 'Bad request for a container configuration.' }) + @ApiForbiddenResponse({ description: 'Unauthorized request for a container configuration.' }) + @ApiNotFoundResponse({ description: 'Container configuration not found.' }) + @UuidParams(PARAM_CONFIG_ID) + async patchDeployment( + @TeamSlug() _: string, + @ConfigId() configId: string, + @Body() request: PatchContainerConfigDto, + @IdentityFromRequest() identity: Identity, + ): Promise { + await this.service.patchConfig(configId, request, identity) + } +} diff --git a/web/crux/src/app/container/container-config.message.ts b/web/crux/src/app/container/container-config.message.ts index 011415187f..584f40fd1d 100644 --- a/web/crux/src/app/container/container-config.message.ts +++ b/web/crux/src/app/container/container-config.message.ts @@ -1,6 +1,13 @@ import { ContainerConfigData } from 'src/domain/container' import { ContainerConfigProperty } from './container.const' +export const WS_TYPE_GET_CONFIG_SECRETS = 'get-config-secrets' +export const WS_TYPE_CONFIG_SECRETS = 'config-secrets' +export type ConfigSecretsMessage = { + keys: string[] + publicKey: string +} + export const WS_TYPE_PATCH_CONFIG = 'patch-config' export type PatchConfigMessage = { config?: ContainerConfigData diff --git a/web/crux/src/app/container/container-config.service.ts b/web/crux/src/app/container/container-config.service.ts index 968f111825..f0c05a9bde 100644 --- a/web/crux/src/app/container/container-config.service.ts +++ b/web/crux/src/app/container/container-config.service.ts @@ -3,18 +3,25 @@ import { EventEmitter2 } from '@nestjs/event-emitter' import { Identity } from '@ory/kratos-client' import { Prisma } from '@prisma/client' import { Observable, filter, map } from 'rxjs' -import { ContainerConfigData } from 'src/domain/container' -import { checkDeploymentMutability } from 'src/domain/deployment' +import { ContainerConfigData, nameOfInstance } from 'src/domain/container' +import { deploymentIsMutable } from 'src/domain/deployment' import { CONTAINER_CONFIG_EVENT_UPDATE, ContainerConfigUpdatedEvent } from 'src/domain/domain-events' -import { checkVersionMutability } from 'src/domain/version' -import { CruxBadRequestException } from 'src/exception/crux-exception' +import { versionIsMutable } from 'src/domain/version' +import { CruxBadRequestException, CruxPreconditionFailedException } from 'src/exception/crux-exception' import PrismaService from 'src/services/prisma.service' import { DomainEvent } from 'src/shared/domain-event' import { WsMessage } from 'src/websockets/common' +import AgentService from '../agent/agent.service' import { EditorLeftMessage, EditorMessage } from '../editor/editor.message' import EditorServiceProvider from '../editor/editor.service.provider' import ContainerConfigDomainEventListener from './container-config.domain-event.listener' -import { ConfigUpdatedMessage, PatchConfigMessage, WS_TYPE_CONFIG_UPDATED } from './container-config.message' +import { ConfigUpdatedMessage, WS_TYPE_CONFIG_UPDATED } from './container-config.message' +import { + ContainerConfigDetailsDto, + ContainerConfigRelationsDto, + ContainerSecretsDto, + PatchContainerConfigDto, +} from './container.dto' import ContainerMapper from './container.mapper' @Injectable() @@ -24,6 +31,7 @@ export default class ContainerConfigService { constructor( private readonly prisma: PrismaService, private readonly mapper: ContainerMapper, + private readonly agentService: AgentService, private readonly editorServices: EditorServiceProvider, private readonly domainEventListener: ContainerConfigDomainEventListener, private readonly events: EventEmitter2, @@ -85,7 +93,163 @@ export default class ContainerConfigService { ) } - async patchConfig(configId: string, message: PatchConfigMessage, identity: Identity): Promise { + async getConfigDetails(configId: string): Promise { + const config = await this.prisma.containerConfig.findUniqueOrThrow({ + where: { + id: configId, + }, + include: { + image: { + include: { + version: { + include: { + children: true, + deployments: { + select: { + status: true, + }, + }, + }, + }, + }, + }, + configBundle: true, + deployment: { + include: { + version: true, + }, + }, + instance: { + include: { + image: true, + deployment: { + include: { + version: true, + }, + }, + }, + }, + }, + }) + + return this.mapper.configDetailsToDto(config) + } + + async getConfigSecrets(configId: string): Promise { + const config = await this.prisma.containerConfig.findUnique({ + where: { + id: configId, + }, + select: { + type: true, + secrets: true, + instance: { + select: { + deployment: true, + config: { + select: { + name: true, + }, + }, + image: { + select: { + name: true, + config: { + select: { + name: true, + }, + }, + }, + }, + }, + }, + deployment: true, + }, + }) + + const deployment = config.type === 'instance' ? config.instance.deployment : config.deployment + if (!deployment) { + return null + } + + const agent = this.agentService.getById(deployment.nodeId) + if (!agent) { + throw new CruxPreconditionFailedException({ + message: 'Node is unreachable', + property: 'nodeId', + value: deployment.nodeId, + }) + } + + const secrets = await agent.listSecrets({ + target: + config.type === 'deployment' + ? { + prefix: deployment.prefix, + } + : { + container: { + prefix: deployment.prefix, + name: nameOfInstance(config.instance), + }, + }, + }) + + return this.mapper.secretsResponseToDto(secrets) + } + + async getConfigRelations(configId: string): Promise { + const versionInclude = { + include: { + project: true, + }, + } + + const deploymentInclude = { + include: { + version: versionInclude, + config: true, + node: true, + configBundles: { + select: { + configBundle: { + include: { + config: true, + }, + }, + }, + }, + }, + } + + const config = await this.prisma.containerConfig.findUnique({ + where: { + id: configId, + }, + select: { + type: true, + configBundle: true, + deployment: deploymentInclude, + image: { + include: { + registry: true, + version: versionInclude, + config: true, + }, + }, + instance: { + include: { + image: { include: { registry: true, config: true } }, + deployment: deploymentInclude, + }, + }, + }, + }) + + return this.mapper.configRelationsToDto(config) + } + + async patchConfig(configId: string, req: PatchContainerConfigDto, identity: Identity): Promise { const mutable = await this.checkMutability(configId) if (!mutable) { throw new CruxBadRequestException({ @@ -95,9 +259,9 @@ export default class ContainerConfigService { }) } - const data: ContainerConfigData = message.config ?? {} - if (message.resetSection) { - data[message.resetSection] = null + const data: ContainerConfigData = req.config ?? {} + if (req.resetSection) { + data[req.resetSection] = null } await this.prisma.containerConfig.update({ @@ -106,7 +270,6 @@ export default class ContainerConfigService { }, data: { ...this.mapper.configDataToDbPatch(data), - updatedAt: new Date(), updatedBy: identity.id, }, }) @@ -190,11 +353,11 @@ export default class ContainerConfigService { switch (config.type) { case 'image': - return checkVersionMutability(config.image.version) + return versionIsMutable(config.image.version) case 'deployment': - return checkDeploymentMutability(config.deployment.status, config.deployment.version.type) + return deploymentIsMutable(config.deployment.status, config.deployment.version.type) case 'instance': - return checkDeploymentMutability(config.instance.deployment.status, config.instance.deployment.version.type) + return deploymentIsMutable(config.instance.deployment.status, config.instance.deployment.version.type) case 'configBundle': return true default: diff --git a/web/crux/src/app/container/container-config.ws.gateway.ts b/web/crux/src/app/container/container-config.ws.gateway.ts index 6865086902..f8e5ac9f56 100644 --- a/web/crux/src/app/container/container-config.ws.gateway.ts +++ b/web/crux/src/app/container/container-config.ws.gateway.ts @@ -27,7 +27,14 @@ import { } from '../editor/editor.message' import EditorServiceProvider from '../editor/editor.service.provider' import { IdentityFromSocket } from '../token/jwt-auth.guard' -import { PatchConfigMessage, WS_TYPE_PATCH_CONFIG, WS_TYPE_PATCH_RECEIVED } from './container-config.message' +import { + ConfigSecretsMessage, + PatchConfigMessage, + WS_TYPE_CONFIG_SECRETS, + WS_TYPE_GET_CONFIG_SECRETS, + WS_TYPE_PATCH_CONFIG, + WS_TYPE_PATCH_RECEIVED, +} from './container-config.message' import ContainerConfigService from './container-config.service' const TeamSlug = () => WsParam('teamSlug') @@ -111,6 +118,19 @@ export default class ContainerConfigWebSocketGateway { } } + @SubscribeMessage(WS_TYPE_GET_CONFIG_SECRETS) + async getConfigSecrets(@ConfigId() configId: string): Promise> { + const secrets = await this.service.getConfigSecrets(configId) + + return { + type: WS_TYPE_CONFIG_SECRETS, + data: secrets ?? { + publicKey: null, + keys: [], + }, + } + } + @AuditLogLevel('disabled') @SubscribeMessage(WS_TYPE_FOCUS_INPUT) async onFocusInput( diff --git a/web/crux/src/app/container/container.dto.ts b/web/crux/src/app/container/container.dto.ts index f8a5b2bf37..1451cdbf45 100644 --- a/web/crux/src/app/container/container.dto.ts +++ b/web/crux/src/app/container/container.dto.ts @@ -1,6 +1,8 @@ -import { ApiProperty, OmitType, PartialType } from '@nestjs/swagger' +import { ApiProperty, OmitType } from '@nestjs/swagger' +import { Type } from 'class-transformer' import { IsBoolean, + IsDate, IsIn, IsInt, IsNumber, @@ -29,7 +31,13 @@ import { PORT_MAX, PORT_MIN, } from 'src/domain/container' -import { UID_MAX } from 'src/shared/const' +import { UID_MAX, UID_MIN } from 'src/shared/const' +import { ConfigBundleDto } from '../config.bundle/config.bundle.dto' +import { DeploymentWithConfigDto } from '../deploy/deploy.dto' +import { ImageDto } from '../image/image.dto' +import { BasicProjectDto } from '../project/project.dto' +import { BasicVersionDto } from '../version/version.dto' +import { ContainerConfigProperty } from './container.const' export const CONTAINER_CONFIG_TYPE_VALUES = ['image', 'instance', 'deployment', 'config-bundle'] as const export type ContainerConfigTypeDto = (typeof CONTAINER_CONFIG_TYPE_VALUES)[number] @@ -339,7 +347,7 @@ export class ContainerConfigDto { @IsOptional() @IsInt() - @Min(-1) + @Min(UID_MIN) @Max(UID_MAX) user?: number @@ -457,12 +465,73 @@ export class ContainerConfigDto { metrics?: MetricsDto } -export class ConcreteContainerConfigDto extends OmitType(PartialType(ContainerConfigDto), ['secrets']) { +export class ContainerConfigRelationsDto { + @ValidateNested() + @IsOptional() + project?: BasicProjectDto + + @ValidateNested() + @IsOptional() + version?: BasicVersionDto + + @ValidateNested() + @IsOptional() + image?: ImageDto + + @ValidateNested() + @IsOptional() + deployment?: DeploymentWithConfigDto + + @ValidateNested() + @IsOptional() + configBundle?: ConfigBundleDto +} + +export class ContainerConfigParentDto { + @IsUUID() + id: string + + @IsString() + name: string + + @IsBoolean() + mutable: boolean +} + +export class ContainerConfigDetailsDto extends ContainerConfigDto { + @ValidateNested() + parent: ContainerConfigParentDto + + @Type(() => Date) + @IsDate() + @IsOptional() + updatedAt?: Date + + @IsString() + @IsOptional() + updatedBy?: string +} + +export class ContainerConfigDataDto extends OmitType(ContainerConfigDto, ['id', 'type']) {} + +export class PatchContainerConfigDto { + @IsOptional() + @ValidateNested() + config?: ContainerConfigDataDto + + @IsOptional() + @IsString() + resetSection?: ContainerConfigProperty +} + +export class ConcreteContainerConfigDto extends OmitType(ContainerConfigDto, ['secrets']) { @IsOptional() @ValidateNested({ each: true }) secrets?: UniqueSecretKeyValueDto[] } +export class ConcreteContainerConfigDataDto extends OmitType(ConcreteContainerConfigDto, ['id', 'type']) {} + export class ContainerIdentifierDto { @IsString() prefix: string @@ -470,3 +539,11 @@ export class ContainerIdentifierDto { @IsString() name: string } + +export class ContainerSecretsDto { + @IsString() + publicKey: string + + @IsString({ each: true }) + keys: string[] +} diff --git a/web/crux/src/app/container/container.mapper.ts b/web/crux/src/app/container/container.mapper.ts index 27ebf5a1ce..af203709ed 100644 --- a/web/crux/src/app/container/container.mapper.ts +++ b/web/crux/src/app/container/container.mapper.ts @@ -1,13 +1,51 @@ -import { Injectable } from '@nestjs/common' -import { ContainerConfig, ContainerConfigType } from '@prisma/client' +import { forwardRef, Inject, Injectable } from '@nestjs/common' +import { + ConfigBundle, + ContainerConfig, + ContainerConfigType, + Deployment, + DeploymentStatusEnum, + Image, + Project, + Version, + VersionsOnParentVersion, +} from '@prisma/client' import { ContainerConfigData } from 'src/domain/container' +import { deploymentIsMutable, DeploymentWithConfigAndBundles } from 'src/domain/deployment' import { ContainerConfigUpdatedEvent } from 'src/domain/domain-events' +import { ImageDetails } from 'src/domain/image' import { toNullableBoolean, toNullableNumber, toPrismaJson } from 'src/domain/utils' +import { versionIsMutable } from 'src/domain/version' +import ConfigBundleMapper from '../config.bundle/config.bundle.mapper' +import DeployMapper from '../deploy/deploy.mapper' +import ImageMapper from '../image/image.mapper' +import ProjectMapper from '../project/project.mapper' +import VersionMapper from '../version/version.mapper' import { ConfigUpdatedMessage } from './container-config.message' -import { ContainerConfigDto, ContainerConfigTypeDto } from './container.dto' +import { + ContainerConfigDataDto, + ContainerConfigDetailsDto, + ContainerConfigDto, + ContainerConfigParentDto, + ContainerConfigRelationsDto, + ContainerConfigTypeDto, + ContainerSecretsDto, +} from './container.dto' +import { ListSecretsResponse } from 'src/grpc/protobuf/proto/common' @Injectable() export default class ContainerMapper { + constructor( + private readonly projectMapper: ProjectMapper, + private readonly versionMapper: VersionMapper, + @Inject(forwardRef(() => ImageMapper)) + private readonly imageMapper: ImageMapper, + @Inject(forwardRef(() => DeployMapper)) + private readonly deployMapper: DeployMapper, + @Inject(forwardRef(() => ConfigBundleMapper)) + private readonly configBundleMapper: ConfigBundleMapper, + ) {} + typeToDto(type: ContainerConfigType): ContainerConfigTypeDto { switch (type) { case 'configBundle': @@ -37,7 +75,64 @@ export default class ContainerMapper { } } - configDtoToConfigData(current: ContainerConfigData, patch: ContainerConfigDto): ContainerConfigData { + configDetailsToDto(config: ContainerConfigDetails): ContainerConfigDetailsDto { + return { + ...this.configDataToDto(config.id, config.type, config as any as ContainerConfigData), + parent: this.configDetailsToParentDto(config), + updatedAt: config.updatedAt, + updatedBy: config.updatedBy, + } + } + + configRelationsToDto(config: ContainerConfigRelations): ContainerConfigRelationsDto { + switch (config.type) { + case 'image': { + const { version } = config.image + + return { + image: this.imageMapper.toDetailsDto(config.image), + project: this.projectMapper.toBasicDto(version.project), + version: this.versionMapper.toBasicDto(version), + } + } + case 'instance': { + const { deployment } = config.instance + const { version } = deployment + + return { + image: this.imageMapper.toDetailsDto(config.instance.image), + project: this.projectMapper.toBasicDto(version.project), + version: this.versionMapper.toBasicDto(version), + deployment: this.deployMapper.toDeploymentWithConfigDto(deployment), + } + } + case 'deployment': { + const { deployment } = config + const { version } = deployment + + return { + project: this.projectMapper.toBasicDto(version.project), + version: this.versionMapper.toBasicDto(version), + deployment: this.deployMapper.toDeploymentWithConfigDto(deployment), + } + } + case 'configBundle': + return { + configBundle: this.configBundleMapper.toDto(config.configBundle), + } + default: + throw new Error(`Unknown ContainerConfigType ${config.type}`) + } + } + + secretsResponseToDto(secrets: ListSecretsResponse): ContainerSecretsDto { + return { + keys: secrets.keys ?? [], + publicKey: secrets.publicKey, + } + } + + configDtoToConfigData(current: ContainerConfigData, patch: ContainerConfigDataDto): ContainerConfigData { let result: ContainerConfigData = { ...current, ...patch, @@ -178,6 +273,86 @@ export default class ContainerMapper { id: event.id, } } + + private configDetailsToParentDto(config: ContainerConfigDetails): ContainerConfigParentDto { + switch (config.type) { + case 'image': { + const { image } = config + + return { + id: image.id, + name: image.name, + mutable: versionIsMutable(image.version), + } + } + case 'instance': { + const { instance } = config + const { image, deployment } = instance + + return { + id: image.id, + name: image.name, + mutable: deploymentIsMutable(deployment.status, deployment.version.type), + } + } + case 'deployment': { + const { deployment } = config + + return { + id: deployment.id, + name: deployment.prefix, + mutable: deploymentIsMutable(deployment.status, deployment.version.type), + } + } + case 'configBundle': { + const { configBundle } = config + + return { + id: configBundle.id, + name: configBundle.name, + mutable: true, + } + } + default: + throw new Error(`Unknown ContainerConfigType ${config.type}`) + } + } +} + +type ContainerConfigRelations = { + type: ContainerConfigType + image: ImageDetails & { + version: Version & { + project: Project + } + } + instance: { + image: ImageDetails + deployment: DeploymentWithConfigAndBundles + } + deployment: DeploymentWithConfigAndBundles + configBundle: ConfigBundle +} + +type ContainerConfigDetails = ContainerConfig & { + image: Image & { + version: Version & { + deployments: { + status: DeploymentStatusEnum + }[] + children: VersionsOnParentVersion[] + } + } + instance: { + image: Image + deployment: Deployment & { + version: Version + } + } + deployment: Deployment & { + version: Version + } + configBundle: ConfigBundle } export type ContainerConfigDbPatch = Omit diff --git a/web/crux/src/app/container/container.module.ts b/web/crux/src/app/container/container.module.ts index e47bb7eb42..b3a9416294 100644 --- a/web/crux/src/app/container/container.module.ts +++ b/web/crux/src/app/container/container.module.ts @@ -1,16 +1,32 @@ -import { Module } from '@nestjs/common' +import { forwardRef, Module } from '@nestjs/common' import PrismaService from 'src/services/prisma.service' import AuditLoggerModule from '../audit.logger/audit.logger.module' +import ConfigBundleModule from '../config.bundle/config.bundle.module' +import DeployModule from '../deploy/deploy.module' import EditorModule from '../editor/editor.module' +import ImageModule from '../image/image.module' +import ProjectModule from '../project/project.module' +import VersionModule from '../version/version.module' import ContainerConfigDomainEventListener from './container-config.domain-event.listener' +import ContainerConfigHttpController from './container-config.http.service' import ContainerConfigService from './container-config.service' import ContainerConfigWebSocketGateway from './container-config.ws.gateway' import ContainerMapper from './container.mapper' +import AgentModule from '../agent/agent.module' @Module({ - imports: [AuditLoggerModule, EditorModule], + imports: [ + AuditLoggerModule, + EditorModule, + forwardRef(() => AgentModule), + forwardRef(() => ProjectModule), + forwardRef(() => VersionModule), + forwardRef(() => ImageModule), + forwardRef(() => DeployModule), + forwardRef(() => ConfigBundleModule), + ], exports: [ContainerMapper, ContainerConfigService], - controllers: [], + controllers: [ContainerConfigHttpController], providers: [ PrismaService, ContainerMapper, diff --git a/web/crux/src/app/container/guards/container-config.team-access.guard.ts b/web/crux/src/app/container/guards/container-config.team-access.guard.ts new file mode 100644 index 0000000000..412122843c --- /dev/null +++ b/web/crux/src/app/container/guards/container-config.team-access.guard.ts @@ -0,0 +1,22 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common' +import { AuthorizedHttpRequest, identityOfRequest } from 'src/app/token/jwt-auth.guard' +import ContainerConfigService from '../container-config.service' + +@Injectable() +export default class ContainerConfigTeamAccessGuard implements CanActivate { + constructor(private readonly service: ContainerConfigService) {} + + async canActivate(context: ExecutionContext): Promise { + const req = context.switchToHttp().getRequest() as AuthorizedHttpRequest + const teamSlug = req.params.teamSlug as string + const configId = req.params.configId as string + + if (!configId) { + return true + } + + const identity = identityOfRequest(context) + + return await this.service.checkConfigIsInTeam(teamSlug, configId, identity) + } +} diff --git a/web/crux/src/app/dashboard/dashboard.mapper.spec.ts b/web/crux/src/app/dashboard/dashboard.mapper.spec.ts index 7cf7fc6bbc..3b86549a7a 100644 --- a/web/crux/src/app/dashboard/dashboard.mapper.spec.ts +++ b/web/crux/src/app/dashboard/dashboard.mapper.spec.ts @@ -21,7 +21,9 @@ describe('DashboardMapper', () => { nodes: [], project: null, version: null, - image: null, + image: { + config: null, + }, deployment: null, } }) @@ -140,9 +142,9 @@ describe('DashboardMapper', () => { }) test('should be done when there is an image', () => { - const IMAGE_ID = 'imageId' + const IMAGE_CONFIG_ID = 'imageConfigId' - const expected: OnboardingItemDto = { done: true, resourceId: IMAGE_ID } + const expected: OnboardingItemDto = { done: true, resourceId: IMAGE_CONFIG_ID } team.project = { id: 'projectId', @@ -151,7 +153,9 @@ describe('DashboardMapper', () => { id: 'versionid', } team.image = { - id: IMAGE_ID, + config: { + id: IMAGE_CONFIG_ID, + }, } const actual = mapper.teamToOnboard(team) @@ -186,7 +190,9 @@ describe('DashboardMapper', () => { id: 'versionid', } team.image = { - id: 'imageId', + config: { + id: 'imageConfigId', + }, } team.deployment = { id: DEPLOYMENT_ID, @@ -226,7 +232,9 @@ describe('DashboardMapper', () => { id: 'versionid', } team.image = { - id: 'imageId', + config: { + id: 'imageConfigId', + }, } team.deployment = { id: DEPLOYMENT_ID, @@ -253,7 +261,9 @@ describe('DashboardMapper', () => { id: 'versionid', } team.image = { - id: 'imageId', + config: { + id: 'imageConfigId', + }, } team.deployment = { id: DEPLOYMENT_ID, diff --git a/web/crux/src/app/dashboard/dashboard.mapper.ts b/web/crux/src/app/dashboard/dashboard.mapper.ts index 5f0165a6bd..2963198c54 100644 --- a/web/crux/src/app/dashboard/dashboard.mapper.ts +++ b/web/crux/src/app/dashboard/dashboard.mapper.ts @@ -18,7 +18,7 @@ export default class DashboardMapper { createNode: this.resourceToOnboardItem(deployment ? deployment.node : team.nodes.find(Boolean)), createProject: this.resourceToOnboardItem(team.project), createVersion: this.resourceToOnboardItem(team.version), - addImages: this.resourceToOnboardItem(team.image), + addImages: this.resourceToOnboardItem(team.image.config), addDeployment: this.resourceToOnboardItem(deployment), deploy: { done: (deployment && deployment.status !== 'preparing') ?? false, @@ -55,6 +55,8 @@ export type DashboardTeam = ResourceWithId & { nodes: ResourceWithId[] project: ResourceWithId version: ResourceWithId - image: ResourceWithId + image: { + config: ResourceWithId + } deployment: DashboardDeployment } diff --git a/web/crux/src/app/dashboard/dashboard.service.ts b/web/crux/src/app/dashboard/dashboard.service.ts index e1a16617ab..e74420f689 100644 --- a/web/crux/src/app/dashboard/dashboard.service.ts +++ b/web/crux/src/app/dashboard/dashboard.service.ts @@ -42,7 +42,9 @@ export default class DashboardService { ...team, project: null, version: null, - image: null, + image: { + config: null, + }, deployment: null, } @@ -131,6 +133,7 @@ export default class DashboardService { images: { select: { id: true, + config: true, }, take: 1, orderBy: { @@ -151,18 +154,13 @@ export default class DashboardService { return null } - const { - version, - version: { - images: [image], - project, - }, - } = deployment + const { version } = deployment + const { project, images } = version return { project, version, - image, + image: images.find(Boolean), deployment, } } @@ -190,7 +188,11 @@ export default class DashboardService { id: true, images: { select: { - id: true, + config: { + select: { + id: true, + }, + }, }, take: 1, orderBy: { diff --git a/web/crux/src/app/deploy/deploy.dto.ts b/web/crux/src/app/deploy/deploy.dto.ts index 14dc982462..f5f039cb07 100644 --- a/web/crux/src/app/deploy/deploy.dto.ts +++ b/web/crux/src/app/deploy/deploy.dto.ts @@ -1,5 +1,4 @@ import { ApiProperty } from '@nestjs/swagger' -import { Deployment, Node, Project, Version } from '@prisma/client' import { Type } from 'class-transformer' import { IsBoolean, @@ -17,11 +16,14 @@ import { } from 'class-validator' import { CONTAINER_STATE_VALUES, ContainerState } from 'src/domain/container' import { PaginatedList, PaginationQuery } from 'src/shared/dtos/paginating' -import { BasicProperties } from '../../shared/dtos/shared.dto' import { AuditDto } from '../audit/audit.dto' import { ConfigBundleDetailsDto } from '../config.bundle/config.bundle.dto' -import { ConcreteContainerConfigDto, ContainerIdentifierDto } from '../container/container.dto' -import { ImageDto } from '../image/image.dto' +import { + ConcreteContainerConfigDataDto, + ConcreteContainerConfigDto, + ContainerIdentifierDto, +} from '../container/container.dto' +import { ImageDetailsDto } from '../image/image.dto' import { BasicNodeDto, BasicNodeWithStatus } from '../node/node.dto' import { BasicProjectDto } from '../project/project.dto' import { BasicVersionDto } from '../version/version.dto' @@ -84,21 +86,18 @@ export class InstanceDto { updatedAt: Date @ValidateNested() - image: ImageDto + image: ImageDetailsDto +} - @IsOptional() +export class InstanceDetailsDto extends InstanceDto { @ValidateNested() - config?: ConcreteContainerConfigDto + config: ConcreteContainerConfigDto } export class PatchInstanceDto { - @IsString() - @IsOptional() - tag?: string | null - @IsOptional() @ValidateNested() - config?: ConcreteContainerConfigDto | null + config?: ConcreteContainerConfigDataDto | null } export class DeploymentTokenDto { @@ -137,28 +136,30 @@ export class DeploymentTokenCreatedDto extends DeploymentTokenDto { curl: string } -export class DeploymentDetailsDto extends DeploymentDto { - @ValidateNested() - @IsOptional() - config?: ConcreteContainerConfigDto - +export class DeploymentWithConfigDto extends DeploymentDto { @IsString() @IsOptional() publicKey?: string | null @ValidateNested() - instances: InstanceDto[] + config: ConcreteContainerConfigDto + + @IsString({ each: true }) + @IsOptional() + configBundles: ConfigBundleDetailsDto[] +} + +export class DeploymentDetailsDto extends DeploymentWithConfigDto { + @ValidateNested({ each: true }) + instances: InstanceDetailsDto[] @IsInt() @Min(0) lastTry: number @ValidateNested() - token: DeploymentTokenDto - - @IsString({ each: true }) @IsOptional() - configBundles: ConfigBundleDetailsDto[] + token?: DeploymentTokenDto } export class CreateDeploymentDto { @@ -190,6 +191,7 @@ export class UpdateDeploymentDto { @IsBoolean() protected: boolean + @IsString({ each: true }) configBundles: string[] } @@ -270,16 +272,17 @@ export class DeploymentEventDto { containerProgress?: DeploymentEventContainerProgressDto | null } -export class InstanceSecretsDto { - @ValidateNested() - container: ContainerIdentifierDto - +export class DeploymentSecretsDto { @IsString() publicKey: string - @IsOptional() @IsString({ each: true }) - keys?: string[] | null + keys: string[] +} + +export class InstanceSecretsDto extends DeploymentSecretsDto { + @ValidateNested() + container: ContainerIdentifierDto } export class DeploymentLogListDto extends PaginatedList { @@ -303,13 +306,3 @@ export class StartDeploymentDto { @IsString({ each: true }) instances?: string[] } - -export type DeploymentWithNode = Deployment & { - node: Pick -} - -export type DeploymentWithNodeVersion = DeploymentWithNode & { - version: Pick & { - project: Pick - } -} diff --git a/web/crux/src/app/deploy/deploy.http.controller.ts b/web/crux/src/app/deploy/deploy.http.controller.ts index e0d4e4f836..392d888f52 100644 --- a/web/crux/src/app/deploy/deploy.http.controller.ts +++ b/web/crux/src/app/deploy/deploy.http.controller.ts @@ -37,8 +37,9 @@ import { DeploymentDto, DeploymentLogListDto, DeploymentLogPaginationQuery, + DeploymentSecretsDto, DeploymentTokenCreatedDto, - InstanceDto, + InstanceDetailsDto, InstanceSecretsDto, PatchInstanceDto, StartDeploymentDto, @@ -65,6 +66,7 @@ const InstanceId = () => Param(PARAM_INSTANCE_ID) const ROUTE_TEAM_SLUG = ':teamSlug' const ROUTE_DEPLOYMENTS = 'deployments' const ROUTE_DEPLOYMENT_ID = ':deploymentId' +const ROUTE_SECRETS = 'secrets' const ROUTE_INSTANCES = 'instances' const ROUTE_INSTANCE_ID = ':instanceId' const ROUTE_TOKEN = 'token' @@ -111,6 +113,25 @@ export default class DeployHttpController { return await this.service.getDeploymentDetails(deploymentId) } + @Get(`${ROUTE_DEPLOYMENT_ID}/${ROUTE_SECRETS}`) + @HttpCode(HttpStatus.OK) + @ApiOperation({ + description: + 'Request must include `teamSlug` and `deploymentId`, which refers to the ID of a deployment, needs to be included in URL. Response should include `publicKey`, `keys`.', + summary: 'Fetch secrets of a deployment.', + }) + @ApiOkResponse({ type: DeploymentSecretsDto, description: 'Secrets of a deployment listed.' }) + @ApiBadRequestResponse({ description: 'Bad request for deployment secrets.' }) + @ApiForbiddenResponse({ description: 'Unauthorized request for deployment secrets.' }) + @ApiNotFoundResponse({ description: 'Deployment secrets not found.' }) + @UuidParams(PARAM_DEPLOYMENT_ID) + async getDeploymentSecrets( + @TeamSlug() _: string, + @DeploymentId() deploymentId: string, + ): Promise { + return await this.service.getDeploymentSecrets(deploymentId) + } + @Get(`${ROUTE_DEPLOYMENT_ID}/${ROUTE_INSTANCES}/${ROUTE_INSTANCE_ID}`) @HttpCode(HttpStatus.OK) @ApiOperation({ @@ -118,7 +139,7 @@ export default class DeployHttpController { 'Request must include `teamSlug`, `deploymentId` and `instanceId`, which refer to the ID of a deployment and the instance, in the URL. Instances are the manifestation of an image in the deployment. Response should include `state`, `id`, `updatedAt`, and `image` details including `id`, `name`, `tag`, `order` and `config` variables.', summary: 'Get details of a soon-to-be container.', }) - @ApiOkResponse({ type: InstanceDto, description: 'Details of an instance.' }) + @ApiOkResponse({ type: InstanceDetailsDto, description: 'Details of an instance.' }) @ApiBadRequestResponse({ description: 'Bad request for instance details.' }) @ApiForbiddenResponse({ description: 'Unauthorized request for an instance.' }) @ApiNotFoundResponse({ description: 'Instance not found.' }) @@ -127,11 +148,11 @@ export default class DeployHttpController { @TeamSlug() _: string, @DeploymentId() _deploymentId: string, @InstanceId() instanceId: string, - ): Promise { + ): Promise { return await this.service.getInstance(instanceId) } - @Get(`${ROUTE_DEPLOYMENT_ID}/${ROUTE_INSTANCES}/${ROUTE_INSTANCE_ID}/secrets`) + @Get(`${ROUTE_DEPLOYMENT_ID}/${ROUTE_INSTANCES}/${ROUTE_INSTANCE_ID}/${ROUTE_SECRETS}`) @HttpCode(HttpStatus.OK) @ApiOperation({ description: @@ -143,7 +164,7 @@ export default class DeployHttpController { @ApiForbiddenResponse({ description: 'Unauthorized request for instance secrets.' }) @ApiNotFoundResponse({ description: 'Instance secrets not found.' }) @UuidParams(PARAM_DEPLOYMENT_ID, PARAM_INSTANCE_ID) - async getDeploymentSecrets( + async getInstanceSecrets( @TeamSlug() _: string, @DeploymentId() _deploymentId: string, @InstanceId() instanceId: string, @@ -179,7 +200,7 @@ export default class DeployHttpController { } } - @Patch(ROUTE_DEPLOYMENT_ID) + @Put(ROUTE_DEPLOYMENT_ID) @HttpCode(HttpStatus.NO_CONTENT) @ApiOperation({ description: 'Request must include `deploymentId` and `teamSlug` in URL.', diff --git a/web/crux/src/app/deploy/deploy.mapper.spec.ts b/web/crux/src/app/deploy/deploy.mapper.spec.ts index bb23bcf771..bdbb915a45 100644 --- a/web/crux/src/app/deploy/deploy.mapper.spec.ts +++ b/web/crux/src/app/deploy/deploy.mapper.spec.ts @@ -1,6 +1,7 @@ import { DeploymentStatusEnum, NodeTypeEnum, ProjectTypeEnum, Storage, VersionTypeEnum } from '.prisma/client' import { Test, TestingModule } from '@nestjs/testing' import { ConcreteContainerConfigData, ContainerConfigData } from 'src/domain/container' +import { DeploymentWithNodeVersion } from 'src/domain/deployment' import { CommonContainerConfig, DagentContainerConfig, ImportContainer } from 'src/grpc/protobuf/proto/agent' import { DriverType, NetworkMode, RestartPolicy } from 'src/grpc/protobuf/proto/common' import EncryptionService from 'src/services/encryption.service' @@ -13,7 +14,7 @@ import NodeMapper from '../node/node.mapper' import ProjectMapper from '../project/project.mapper' import RegistryMapper from '../registry/registry.mapper' import VersionMapper from '../version/version.mapper' -import { DeploymentDto, DeploymentWithNodeVersion, PatchInstanceDto } from './deploy.dto' +import { DeploymentDto, PatchInstanceDto } from './deploy.dto' import DeployMapper from './deploy.mapper' describe('DeployMapper', () => { @@ -527,7 +528,7 @@ describe('DeployMapper', () => { }, ] - const actual = deployMapper.instanceConfigDtoToInstanceContainerConfigData(fullImage, {}, patch.config) + const actual = deployMapper.concreteConfigDtoToConcreteContainerConfigData(fullImage, {}, patch.config) expect(actual).toEqual(expected) }) @@ -560,7 +561,7 @@ describe('DeployMapper', () => { deployment: fullInstance.labels.deployment, } - const actual = deployMapper.instanceConfigDtoToInstanceContainerConfigData(fullImage, {}, patch.config) + const actual = deployMapper.concreteConfigDtoToConcreteContainerConfigData(fullImage, {}, patch.config) expect(actual).toEqual(expected) }) @@ -614,7 +615,7 @@ describe('DeployMapper', () => { ingress: labelIngress, } - const actual = deployMapper.instanceConfigDtoToInstanceContainerConfigData(fullImage, instance, patch.config) + const actual = deployMapper.concreteConfigDtoToConcreteContainerConfigData(fullImage, instance, patch.config) expect(actual).toEqual(expected) }) diff --git a/web/crux/src/app/deploy/deploy.mapper.ts b/web/crux/src/app/deploy/deploy.mapper.ts index f3370bb80e..9e79de2fca 100644 --- a/web/crux/src/app/deploy/deploy.mapper.ts +++ b/web/crux/src/app/deploy/deploy.mapper.ts @@ -1,15 +1,12 @@ import { Inject, Injectable, forwardRef } from '@nestjs/common' import { - ConfigBundle, ContainerConfig, Deployment, DeploymentEvent, DeploymentEventTypeEnum, DeploymentStatusEnum, DeploymentStrategy, - DeploymentToken, ExposeStrategy, - Instance, NetworkMode, RestartPolicy, Storage, @@ -22,11 +19,18 @@ import { ContainerVolumeType, InitContainer, UniqueKey, - UniqueKeyValue, Volume, } from 'src/domain/container' import { mergeMarkers, mergeSecrets } from 'src/domain/container-merge' -import { deploymentLogLevelToDb, deploymentStatusToDb } from 'src/domain/deployment' +import { + DeploymentDetails, + DeploymentWithConfigAndBundles, + DeploymentWithNode, + DeploymentWithNodeVersion, + InstanceDetails, + deploymentLogLevelToDb, + deploymentStatusToDb, +} from 'src/domain/deployment' import { DeploymentConfigBundlesUpdatedEvent, InstanceDeletedEvent, @@ -39,7 +43,6 @@ import { CraneContainerConfig, DagentContainerConfig, ImportContainer, - InstanceConfig, Volume as ProtoVolume, } from 'src/grpc/protobuf/proto/agent' import { @@ -59,11 +62,12 @@ import { volumeTypeFromJSON, } from 'src/grpc/protobuf/proto/common' import EncryptionService from 'src/services/encryption.service' +import AgentService from '../agent/agent.service' import AuditMapper from '../audit/audit.mapper' import ConfigBundleMapper from '../config.bundle/config.bundle.mapper' -import { ConcreteContainerConfigDto, ContainerConfigDto } from '../container/container.dto' +import { ConcreteContainerConfigDataDto, ConcreteContainerConfigDto } from '../container/container.dto' import ContainerMapper from '../container/container.mapper' -import ImageMapper, { ImageDetails } from '../image/image.mapper' +import ImageMapper from '../image/image.mapper' import { NodeConnectionStatus } from '../node/node.dto' import NodeMapper from '../node/node.mapper' import ProjectMapper from '../project/project.mapper' @@ -76,11 +80,11 @@ import { DeploymentEventLogDto, DeploymentEventTypeDto, DeploymentLogLevelDto, + DeploymentSecretsDto, DeploymentStatusDto, DeploymentWithBasicNodeDto, - DeploymentWithNode, - DeploymentWithNodeVersion, - InstanceDto, + DeploymentWithConfigDto, + InstanceDetailsDto, InstanceSecretsDto, } from './deploy.dto' import { @@ -93,16 +97,16 @@ import { @Injectable() export default class DeployMapper { constructor( - @Inject(forwardRef(() => ImageMapper)) + @Inject(forwardRef(() => AgentService)) + private readonly agentService: AgentService, private readonly imageMapper: ImageMapper, + @Inject(forwardRef(() => ContainerMapper)) private readonly containerMapper: ContainerMapper, - @Inject(forwardRef(() => ProjectMapper)) private readonly projectMapper: ProjectMapper, private readonly auditMapper: AuditMapper, - @Inject(forwardRef(() => VersionMapper)) private readonly versionMapper: VersionMapper, - @Inject(forwardRef(() => NodeMapper)) private readonly nodeMapper: NodeMapper, + @Inject(forwardRef(() => ConfigBundleMapper)) private readonly configBundleMapper: ConfigBundleMapper, private readonly encryptionService: EncryptionService, ) {} @@ -152,35 +156,46 @@ export default class DeployMapper { } } - toDetailsDto(deployment: DeploymentDetails, publicKey?: string): DeploymentDetailsDto { + toDeploymentWithConfigDto(deployment: DeploymentWithConfigAndBundles): DeploymentWithConfigDto { + const agent = this.agentService.getById(deployment.nodeId) + return { ...this.toDto(deployment), - token: deployment.token ?? null, - lastTry: deployment.tries, - publicKey, + publicKey: agent?.publicKey ?? null, configBundles: deployment.configBundles.map(it => this.configBundleMapper.detailsToDto(it.configBundle)), config: this.instanceConfigToDto(deployment.config), + } + } + + toDetailsDto(deployment: DeploymentDetails): DeploymentDetailsDto { + return { + ...this.toDeploymentWithConfigDto(deployment), + token: deployment.token ?? null, + lastTry: deployment.tries, instances: deployment.instances.map(it => this.instanceToDto(it)), } } - instanceToDto(it: InstanceDetails): InstanceDto { + instanceToDto(it: InstanceDetails): InstanceDetailsDto { return { id: it.id, - updatedAt: it.config?.updatedAt ?? it.image.config?.updatedAt ?? it.image.updatedAt, - image: this.imageMapper.toDto(it.image), + updatedAt: it.config.updatedAt, + image: this.imageMapper.toDetailsDto(it.image), config: this.instanceConfigToDto(it.config), } } - secretsResponseToInstanceSecretsDto(it: ListSecretsResponse): InstanceSecretsDto { + secretsResponseToDeploymentSecretsDto(it: ListSecretsResponse): DeploymentSecretsDto { return { - container: { - prefix: it.prefix, - name: it.name, - }, publicKey: it.publicKey, - keys: !it.hasKeys ? null : it.keys, + keys: it.keys, + } + } + + secretsResponseToInstanceSecretsDto(it: ListSecretsResponse): InstanceSecretsDto { + return { + ...this.secretsResponseToDeploymentSecretsDto(it), + container: it.target.container, } } @@ -208,23 +223,23 @@ export default class DeployMapper { return result } - instanceConfigDtoToInstanceContainerConfigData( - imageConfig: ContainerConfigData, - instanceConfig: ConcreteContainerConfigData, - patch: ConcreteContainerConfigDto, + concreteConfigDtoToConcreteContainerConfigData( + baseConfig: ContainerConfigData, + concreteConfig: ConcreteContainerConfigData, + patch: ConcreteContainerConfigDataDto, ): ConcreteContainerConfigData { const config = this.containerMapper.configDtoToConfigData( - instanceConfig as ContainerConfigData, - patch as ContainerConfigDto, + concreteConfig as ContainerConfigData, + patch as ConcreteContainerConfigDto, ) if ('labels' in patch) { - const currentLabels = instanceConfig.labels ?? imageConfig.labels ?? {} + const currentLabels = concreteConfig.labels ?? baseConfig.labels ?? {} config.labels = mergeMarkers(config.labels, currentLabels) } if ('annotations' in patch) { - const currentAnnotations = instanceConfig.annotations ?? imageConfig.annotations ?? {} + const currentAnnotations = concreteConfig.annotations ?? baseConfig.annotations ?? {} config.annotations = mergeMarkers(config.annotations, currentAnnotations) } @@ -233,7 +248,7 @@ export default class DeployMapper { // otherwise we need to merge with them with the image secrets return { ...config, - secrets: instanceConfig.secrets ? patch.secrets : mergeSecrets(patch.secrets, imageConfig.secrets), + secrets: concreteConfig.secrets ? patch.secrets : mergeSecrets(patch.secrets, baseConfig.secrets), } } @@ -329,15 +344,6 @@ export default class DeployMapper { return events } - instanceConfigToAgent(deployment: Deployment, mergedEnvironment: UniqueKeyValue[]): InstanceConfig { - const environmentMap = this.mapKeyValueToMap(mergedEnvironment) - - return { - prefix: deployment.prefix, - environment: environmentMap, - } - } - containerStateToDto(state?: ProtoContainerState): ContainerState { return state ? (containerStateToJSON(state).toLowerCase() as ContainerState) : null } @@ -427,7 +433,7 @@ export default class DeployMapper { return event.instances.map(it => ({ id: it.id, configId: it.configId, - image: this.imageMapper.toDto(it.image), + image: this.imageMapper.toDetailsDto(it.image), })) } @@ -440,7 +446,7 @@ export default class DeployMapper { bundlesUpdatedEventToMessage(event: DeploymentConfigBundlesUpdatedEvent): DeploymentBundlesUpdatedMessage { return { - bundles: event.bundles.map(it => this.configBundleMapper.detailsToDto(it)), + bundles: event.bundles.map(it => this.configBundleMapper.toDto(it)), } } @@ -610,21 +616,3 @@ export default class DeployMapper { return networkModeFromJSON(it?.toUpperCase()) } } - -type InstanceDetails = Instance & { - image: ImageDetails - config: ContainerConfig -} - -type ConfigBundleDetails = ConfigBundle & { - config: ContainerConfig -} - -type DeploymentDetails = DeploymentWithNodeVersion & { - token: Pick - instances: InstanceDetails[] - config: ContainerConfig - configBundles: { - configBundle: ConfigBundleDetails - }[] -} diff --git a/web/crux/src/app/deploy/deploy.message.ts b/web/crux/src/app/deploy/deploy.message.ts index a02543c8a2..634d30b27e 100644 --- a/web/crux/src/app/deploy/deploy.message.ts +++ b/web/crux/src/app/deploy/deploy.message.ts @@ -1,6 +1,6 @@ import { ConfigBundleDto } from '../config.bundle/config.bundle.dto' -import { ImageDto } from '../image/image.dto' -import { DeploymentEventDto, InstanceDto } from './deploy.dto' +import { ImageDetailsDto } from '../image/image.dto' +import { DeploymentEventDto } from './deploy.dto' export const WS_TYPE_FETCH_DEPLOYMENT_EVENTS = 'fetch-deployment-events' @@ -15,19 +15,17 @@ export type DeploymentBundlesUpdatedMessage = { bundles: ConfigBundleDto[] } -export const WS_TYPE_GET_INSTANCE = 'get-instance' -export type GetInstanceMessage = { - id: string -} - -export const WS_TYPE_INSTANCE = 'instance' -export type InstanceMessage = InstanceDto - +export const WS_TYPE_GET_DEPLOYMENT_SECRETS = 'get-deployment-secrets' export const WS_TYPE_GET_INSTANCE_SECRETS = 'get-instance-secrets' export type GetInstanceSecretsMessage = { id: string } +export const WS_TYPE_DEPLOYMENT_SECRETS = 'deployment-secrets' +export type DeploymentSecretsMessage = { + keys: string[] +} + export const WS_TYPE_INSTANCE_SECRETS = 'instance-secrets' export type InstanceSecretsMessage = { instanceId: string @@ -37,7 +35,7 @@ export type InstanceSecretsMessage = { type InstanceCreatedMessage = { id: string configId: string - image: ImageDto + image: ImageDetailsDto } export const WS_TYPE_INSTANCES_ADDED = 'instances-added' export type InstancesAddedMessage = InstanceCreatedMessage[] diff --git a/web/crux/src/app/deploy/deploy.module.ts b/web/crux/src/app/deploy/deploy.module.ts index 3565cc9665..bd292ec0d9 100644 --- a/web/crux/src/app/deploy/deploy.module.ts +++ b/web/crux/src/app/deploy/deploy.module.ts @@ -26,10 +26,10 @@ import DeployWebSocketGateway from './deploy.ws.gateway' @Module({ imports: [ forwardRef(() => AgentModule), - ImageModule, + forwardRef(() => ImageModule), EditorModule, RegistryModule, - ContainerModule, + forwardRef(() => ContainerModule), ConfigModule, ConfigBundleModule, AuditLoggerModule, diff --git a/web/crux/src/app/deploy/deploy.service.ts b/web/crux/src/app/deploy/deploy.service.ts index bcf4828da4..f73c6f4be3 100644 --- a/web/crux/src/app/deploy/deploy.service.ts +++ b/web/crux/src/app/deploy/deploy.service.ts @@ -11,7 +11,7 @@ import { UniqueSecretKeyValue, configIsEmpty, } from 'src/domain/container' -import Deployment from 'src/domain/deployment' +import Deployment, { DeploymentWithConfig } from 'src/domain/deployment' import { DeploymentTokenScriptGenerator } from 'src/domain/deployment-token' import { DEPLOYMENT_EVENT_CONFIG_BUNDLES_UPDATE, @@ -26,12 +26,13 @@ import { collectInvalidSecrets, deploymentConfigOf, instanceConfigOf, + mergePrefixNeighborSecrets, } from 'src/domain/start-deployment' import { DeploymentTokenPayload, tokenSignOptionsFor } from 'src/domain/token' import { collectChildVersionIds, collectParentVersionIds, toPrismaJson } from 'src/domain/utils' import { copyDeployment } from 'src/domain/version-increase' import { CruxPreconditionFailedException } from 'src/exception/crux-exception' -import { DeployRequest } from 'src/grpc/protobuf/proto/agent' +import { DeployWorkloadRequest } from 'src/grpc/protobuf/proto/agent' import EncryptionService from 'src/services/encryption.service' import PrismaService from 'src/services/prisma.service' import { DomainEvent } from 'src/shared/domain-event' @@ -52,8 +53,9 @@ import { DeploymentEventDto, DeploymentLogListDto, DeploymentLogPaginationQuery, + DeploymentSecretsDto, DeploymentTokenCreatedDto, - InstanceDto, + InstanceDetailsDto, InstanceSecretsDto, PatchInstanceDto, UpdateDeploymentDto, @@ -73,11 +75,14 @@ export default class DeployService { constructor( private readonly prisma: PrismaService, private readonly jwtService: JwtService, - @Inject(forwardRef(() => AgentService)) private readonly agentService: AgentService, + @Inject(forwardRef(() => AgentService)) + private readonly agentService: AgentService, private readonly domainEvents: DeployDomainEventListener, + @Inject(forwardRef(() => DeployMapper)) private readonly mapper: DeployMapper, private readonly events: EventEmitter2, private readonly registryMapper: RegistryMapper, + @Inject(forwardRef(() => ContainerMapper)) private readonly containerMapper: ContainerMapper, private readonly editorServices: EditorServiceProvider, private readonly configService: ConfigService, @@ -162,9 +167,7 @@ export default class DeployService { }, }) - const publicKey = this.agentService.getById(deployment.nodeId)?.publicKey - - return this.mapper.toDetailsDto(deployment, publicKey) + return this.mapper.toDetailsDto(deployment) } async getDeploymentEvents(deploymentId: string, tryCount?: number): Promise { @@ -181,7 +184,7 @@ export default class DeployService { return events.map(it => this.mapper.eventToDto(it)) } - async getInstance(instanceId: string): Promise { + async getInstance(instanceId: string): Promise { const instance = await this.prisma.instance.findUniqueOrThrow({ where: { id: instanceId, @@ -212,56 +215,11 @@ export default class DeployService { }, }) - const deployment = await this.prisma.deployment.create({ - data: { - versionId: request.versionId, - nodeId: request.nodeId, - status: DeploymentStatusEnum.preparing, - note: request.note, - createdBy: identity.id, - prefix: request.prefix, - protected: request.protected, - instances: { - createMany: { - data: version.images.map(it => ({ - imageId: it.id, - })), - }, - }, - }, - include: { - node: true, - version: { - include: { - project: true, - }, - }, - }, - }) - - const instanceIds = await this.prisma.instance.findMany({ - where: { - deploymentId: deployment.id, - }, - select: { - id: true, - imageId: true, - image: { - select: { - name: true, - }, - }, - }, - }) - const previousDeployment = await this.prisma.deployment.findFirst({ where: { prefix: request.prefix, nodeId: request.nodeId, versionId: request.versionId, - id: { - not: deployment.id, - }, }, include: { instances: { @@ -276,34 +234,97 @@ export default class DeployService { }, }) - if (previousDeployment?.instances) { - instanceIds.forEach(async it => { - const secrets: UniqueSecretKeyValue[] = - (previousDeployment.instances.find(instance => instance.imageId === it.imageId).config - ?.secrets as UniqueSecretKeyValue[]) ?? [] - - if (secrets.length < 1) { - return - } - - await this.prisma.instance.update({ - where: { - id: it.id, + const deploy = await this.prisma.$transaction(async prisma => { + const deployment = await this.prisma.deployment.create({ + data: { + version: { connect: { id: request.versionId } }, + node: { connect: { id: request.nodeId } }, + status: DeploymentStatusEnum.preparing, + note: request.note, + createdBy: identity.id, + prefix: request.prefix, + protected: request.protected, + config: { create: { type: 'deployment' } }, + }, + include: { + node: true, + version: { + include: { + project: true, + }, }, - data: { - config: { - create: { - type: 'instance', - updatedBy: identity.id, - secrets, + instances: { + select: { + id: true, + imageId: true, + image: { + select: { + name: true, + }, }, }, }, - }) + }, }) - } - return this.mapper.toDto(deployment) + const instances = await Promise.all( + version.images.map( + async image => + await prisma.instance.create({ + data: { + deployment: { connect: { id: deployment.id } }, + image: { connect: { id: image.id } }, + config: { create: { type: 'instance' } }, + }, + select: { + id: true, + imageId: true, + image: { + select: { + name: true, + }, + }, + }, + }), + ), + ) + + deployment.instances = instances + + const previousSecrets: Map = new Map( + previousDeployment?.instances + ?.filter(it => { + const secrets = it.config.secrets as UniqueSecretKeyValue[] + return !!it.config.secrets || secrets.length > 0 + }) + ?.map(it => [it.imageId, it.config.secrets as UniqueSecretKeyValue[]]) ?? [], + ) + + await Promise.all( + deployment.instances.map(async it => { + const secrets = previousSecrets[it.imageId] + + await prisma.instance.update({ + where: { + id: it.id, + }, + data: { + config: { + create: { + type: 'instance', + updatedBy: identity.id, + secrets, + }, + }, + }, + }) + }), + ) + + return deployment + }) + + return this.mapper.toDto(deploy) } async updateDeployment(deploymentId: string, req: UpdateDeploymentDto, identity: Identity): Promise { @@ -321,7 +342,6 @@ export default class DeployService { }, create: req.configBundles.map(configBundleId => ({ configBundle: { connect: { id: configBundleId } } })), }, - updatedAt: new Date(), updatedBy: identity.id, }, select: { @@ -365,7 +385,7 @@ export default class DeployService { }, }) - const configData = this.mapper.instanceConfigDtoToInstanceContainerConfigData( + const configData = this.mapper.concreteConfigDtoToConcreteContainerConfigData( instance.image.config as any as ContainerConfigData, (instance.config ?? {}) as any as ConcreteContainerConfigData, req.config, @@ -378,11 +398,6 @@ export default class DeployService { updatedBy: identity.id, } - // // We should overwrite the user in the ConfigData. This is an edge case, which is why we haven't - // // implemented a new mapper for configDataToDb. However, in the long run, if there are more similar - // // situations, we will have to create a different mapper for InstanceConfig. - // config.user = configData.user - await this.prisma.deployment.update({ where: { id: deploymentId, @@ -406,7 +421,7 @@ export default class DeployService { } async deleteDeployment(deploymentId: string): Promise { - const deployment = await this.prisma.deployment.delete({ + await this.prisma.deployment.delete({ where: { id: deploymentId, }, @@ -416,18 +431,6 @@ export default class DeployService { status: true, }, }) - - if (deployment.status === 'successful') { - const agent = this.agentService.getById(deployment.nodeId) - if (!agent) { - return - } - - await agent.deleteContainers({ - prefix: deployment.prefix, - container: null, - }) - } } async startDeployment(deploymentId: string, identity: Identity, instances?: string[]): Promise { @@ -541,6 +544,9 @@ export default class DeployService { await this.updateInvalidSecretsAndThrow(invalidSecrets) } + const prefixNeighbors = await this.collectLatestSuccessfulDeploymentsForPrefix(deployment.nodeId, deployment.prefix) + const sharedSecrets = mergePrefixNeighborSecrets(prefixNeighbors, publicKey) + const tries = deployment.tries + 1 await this.prisma.deployment.update({ where: { @@ -556,6 +562,8 @@ export default class DeployService { id: deployment.id, releaseNotes: deployment.version.changelog, versionName: deployment.version.name, + prefix: deployment.prefix, + secrets: sharedSecrets, requests: await Promise.all( deployment.instances.map(async it => { const { registry } = it.image @@ -587,10 +595,11 @@ export default class DeployService { user: registry.user, password: this.encryptionService.decrypt(registry.token), }, - } as DeployRequest + } as DeployWorkloadRequest }), ), }, + instanceConfigs, notification: { teamId: deployment.version.project.teamId, actor: identity ?? deployment.token?.name ?? null, @@ -598,12 +607,6 @@ export default class DeployService { versionName: deployment.version.name, nodeName: deployment.node.name, }, - instanceConfigs: new Map( - [...instanceConfigs.entries()].filter(entry => { - const [, conf] = entry - return !configIsEmpty(conf) - }), - ), deploymentConfig: !configIsEmpty(deploymentConfig) ? deploymentConfig : null, tries, }) @@ -618,6 +621,8 @@ export default class DeployService { }, data: { status: DeploymentStatusEnum.inProgress, + deployedAt: new Date(), + deployedBy: identity.id, }, }) } @@ -664,6 +669,7 @@ export default class DeployService { }, data: { status: finalStatus, + deployedAt: new Date(), }, }) @@ -763,13 +769,7 @@ export default class DeployService { }, data: { config: { - upsert: { - update: data, - create: { - ...data, - type: 'instance', - }, - }, + update: data, }, }, }) @@ -843,6 +843,31 @@ export default class DeployService { return deployments.map(it => this.mapper.toDto(it)) } + async getDeploymentSecrets(deploymentId: string): Promise { + const deployment = await this.prisma.deployment.findUniqueOrThrow({ + where: { + id: deploymentId, + }, + }) + + const agent = this.agentService.getById(deployment.nodeId) + if (!agent) { + throw new CruxPreconditionFailedException({ + message: 'Node is unreachable', + property: 'nodeId', + value: deployment.nodeId, + }) + } + + const secrets = await agent.listSecrets({ + target: { + prefix: deployment.prefix, + }, + }) + + return this.mapper.secretsResponseToDeploymentSecretsDto(secrets) + } + async getInstanceSecrets(instanceId: string): Promise { const instance = await this.prisma.instance.findUniqueOrThrow({ where: { @@ -871,9 +896,11 @@ export default class DeployService { } const secrets = await agent.listSecrets({ - container: { - prefix: deployment.prefix, - name: containerName, + target: { + container: { + prefix: deployment.prefix, + name: containerName, + }, }, }) @@ -1116,4 +1143,39 @@ export default class DeployService { value: secrets.map(it => ({ ...it, secrets: undefined })), }) } + + private async collectLatestSuccessfulDeploymentsForPrefix( + nodeId: string, + prefix: string, + ): Promise { + const versions = await this.prisma.version.findMany({ + where: { + deployments: { + some: { + prefix, + nodeId, + status: 'successful', + }, + }, + }, + include: { + deployments: { + where: { + prefix, + nodeId, + status: 'successful', + }, + take: 1, + orderBy: { + createdAt: 'desc', + }, + include: { + config: true, + }, + }, + }, + }) + + return versions.flatMap(it => it.deployments) + } } diff --git a/web/crux/src/app/deploy/deploy.ws.gateway.ts b/web/crux/src/app/deploy/deploy.ws.gateway.ts index 204ef09736..3a50f8b3ce 100644 --- a/web/crux/src/app/deploy/deploy.ws.gateway.ts +++ b/web/crux/src/app/deploy/deploy.ws.gateway.ts @@ -30,15 +30,14 @@ import { IdentityFromSocket } from '../token/jwt-auth.guard' import { DeploymentEventListMessage, DeploymentEventMessage, - GetInstanceMessage, + DeploymentSecretsMessage, GetInstanceSecretsMessage, - InstanceMessage, InstanceSecretsMessage, WS_TYPE_DEPLOYMENT_EVENT_LIST, + WS_TYPE_DEPLOYMENT_SECRETS, WS_TYPE_FETCH_DEPLOYMENT_EVENTS, - WS_TYPE_GET_INSTANCE, + WS_TYPE_GET_DEPLOYMENT_SECRETS, WS_TYPE_GET_INSTANCE_SECRETS, - WS_TYPE_INSTANCE, WS_TYPE_INSTANCE_SECRETS, } from './deploy.message' import DeployService from './deploy.service' @@ -142,13 +141,15 @@ export default class DeployWebSocketGateway { } @AuditLogLevel('disabled') - @SubscribeMessage(WS_TYPE_GET_INSTANCE) - async getInstance(@SocketMessage() message: GetInstanceMessage): Promise> { - const instance = await this.service.getInstance(message.id) + @SubscribeMessage(WS_TYPE_GET_DEPLOYMENT_SECRETS) + async getDeploymentSecrets(@DeploymentId() deploymentId: string): Promise> { + const secrets = await this.service.getDeploymentSecrets(deploymentId) return { - type: WS_TYPE_INSTANCE, - data: instance, + type: WS_TYPE_DEPLOYMENT_SECRETS, + data: { + keys: secrets.keys ?? [], + }, } } diff --git a/web/crux/src/app/deploy/interceptors/deploy.delete.interceptor.ts b/web/crux/src/app/deploy/interceptors/deploy.delete.interceptor.ts index 315d04022a..ed21878b18 100644 --- a/web/crux/src/app/deploy/interceptors/deploy.delete.interceptor.ts +++ b/web/crux/src/app/deploy/interceptors/deploy.delete.interceptor.ts @@ -1,6 +1,6 @@ import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common' import { Observable } from 'rxjs' -import { checkDeploymentDeletability } from 'src/domain/deployment' +import { deploymentIsDeletable } from 'src/domain/deployment' import { CruxPreconditionFailedException } from 'src/exception/crux-exception' import PrismaService from 'src/services/prisma.service' @@ -18,7 +18,7 @@ export default class DeleteDeploymentValidationInterceptor implements NestInterc }, }) - if (!checkDeploymentDeletability(deployment.status)) { + if (!deploymentIsDeletable(deployment.status)) { throw new CruxPreconditionFailedException({ message: 'Invalid deployment status.', property: 'status', diff --git a/web/crux/src/app/deploy/interceptors/deploy.patch.interceptor.ts b/web/crux/src/app/deploy/interceptors/deploy.patch.interceptor.ts index 27cdb67f43..e96e894fce 100644 --- a/web/crux/src/app/deploy/interceptors/deploy.patch.interceptor.ts +++ b/web/crux/src/app/deploy/interceptors/deploy.patch.interceptor.ts @@ -1,7 +1,7 @@ import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common' import { VersionTypeEnum } from '@prisma/client' import { Observable } from 'rxjs' -import { checkDeploymentMutability } from 'src/domain/deployment' +import { deploymentIsMutable } from 'src/domain/deployment' import { CruxConflictException, CruxPreconditionFailedException } from 'src/exception/crux-exception' import PrismaService from 'src/services/prisma.service' import { UpdateDeploymentDto } from '../deploy.dto' @@ -24,7 +24,7 @@ export default class DeployPatchValidationInterceptor implements NestInterceptor }, }) - if (!checkDeploymentMutability(deployment.status, deployment.version.type)) { + if (!deploymentIsMutable(deployment.status, deployment.version.type)) { throw new CruxPreconditionFailedException({ message: 'Invalid deployment status.', property: 'status', diff --git a/web/crux/src/app/deploy/interceptors/deploy.start.interceptor.ts b/web/crux/src/app/deploy/interceptors/deploy.start.interceptor.ts index 5f7f0cc2f5..63cb4986d3 100644 --- a/web/crux/src/app/deploy/interceptors/deploy.start.interceptor.ts +++ b/web/crux/src/app/deploy/interceptors/deploy.start.interceptor.ts @@ -1,7 +1,6 @@ import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common' import { Observable } from 'rxjs' import AgentService from 'src/app/agent/agent.service' -import ContainerMapper from 'src/app/container/container.mapper' import { ImageValidation } from 'src/app/image/image.dto' import { ConcreteContainerConfigData, ContainerConfigData, ContainerConfigDataWithId } from 'src/domain/container' import { getConflictsForConcreteConfig } from 'src/domain/container-conflict' @@ -9,7 +8,7 @@ import { mergeConfigsWithConcreteConfig } from 'src/domain/container-merge' import { checkDeploymentDeployability } from 'src/domain/deployment' import { parseDyrectorioEnvRules } from 'src/domain/image' import { missingSecretsOf } from 'src/domain/start-deployment' -import { createStartDeploymentSchema, yupValidate } from 'src/domain/validation' +import { createStartDeploymentSchema, nullifyUndefinedProperties, yupValidate } from 'src/domain/validation' import { CruxPreconditionFailedException } from 'src/exception/crux-exception' import PrismaService from 'src/services/prisma.service' import { StartDeploymentDto } from '../deploy.dto' @@ -19,7 +18,6 @@ export default class DeployStartValidationInterceptor implements NestInterceptor constructor( private prisma: PrismaService, private agentService: AgentService, - private containerMapper: ContainerMapper, ) {} async intercept(context: ExecutionContext, next: CallHandler): Promise> { @@ -104,6 +102,12 @@ export default class DeployStartValidationInterceptor implements NestInterceptor return prev }, {}) + nullifyUndefinedProperties(target.config) + target.configBundles.forEach(it => nullifyUndefinedProperties(it.configBundle.config)) + target.instances.forEach(instance => { + nullifyUndefinedProperties(instance.config) + nullifyUndefinedProperties(instance.image.config) + }) yupValidate(createStartDeploymentSchema(instanceValidations), target) const missingSecrets = deployment.instances @@ -133,7 +137,7 @@ export default class DeployStartValidationInterceptor implements NestInterceptor throw new CruxPreconditionFailedException({ message: 'Unresolved conflicts between config bundles', property: 'configBundles', - value: conflicts, + value: Object.keys(conflicts).join(', '), }) } diff --git a/web/crux/src/app/image/image.dto.ts b/web/crux/src/app/image/image.dto.ts index bb09572d35..970a520350 100644 --- a/web/crux/src/app/image/image.dto.ts +++ b/web/crux/src/app/image/image.dto.ts @@ -46,10 +46,6 @@ export class ImageDto { @IsNumber() order: number - @ValidateNested() - @IsOptional() - config?: ContainerConfigDto - @Type(() => Date) @IsDate() createdAt: Date @@ -61,6 +57,11 @@ export class ImageDto { labels: Record } +export class ImageDetailsDto extends ImageDto { + @ValidateNested() + config: ContainerConfigDto +} + export class AddImagesDto { @IsUUID() registryId: string diff --git a/web/crux/src/app/image/image.http.controller.ts b/web/crux/src/app/image/image.http.controller.ts index 8d0fbaf851..5e9c2dbc3c 100644 --- a/web/crux/src/app/image/image.http.controller.ts +++ b/web/crux/src/app/image/image.http.controller.ts @@ -30,7 +30,7 @@ import { IdentityFromRequest } from '../token/jwt-auth.guard' import ImageAddToVersionTeamAccessGuard from './guards/image.add-to-version.team-access.guard' import ImageOrderImagesTeamAccessGuard from './guards/image.order-images.team-access.guard' import ImageTeamAccessGuard from './guards/image.team-access.guard' -import { AddImagesDto, ImageDto, PatchImageDto } from './image.dto' +import { AddImagesDto, ImageDetailsDto, PatchImageDto } from './image.dto' import ImageService from './image.service' import ImageAddToVersionValidationInterceptor from './interceptors/image.add-images.interceptor' import DeleteImageValidationInterceptor from './interceptors/image.delete.interceptor' @@ -61,7 +61,7 @@ export default class ImageHttpController { summary: 'Fetch data of all images of a version.', }) @ApiOkResponse({ - type: ImageDto, + type: ImageDetailsDto, isArray: true, description: 'Data of images listed.', }) @@ -71,7 +71,7 @@ export default class ImageHttpController { @TeamSlug() _: string, @ProjectId() _projectId: string, @VersionId() versionId: string, - ): Promise { + ): Promise { return await this.service.getImagesByVersionId(versionId) } @@ -82,7 +82,7 @@ export default class ImageHttpController { "Fetch details of an image within a version. `projectId` refers to the project's ID, `versionId` refers to the version's ID, `imageId` refers to the image's ID. All, and `teamSlug` are required in the URL.

Image details consists `name`, `id`, `tag`, `order`, and the config of the image.", summary: 'Fetch data of an image of a version.', }) - @ApiOkResponse({ type: ImageDto, description: 'Data of an image.' }) + @ApiOkResponse({ type: ImageDetailsDto, description: 'Data of an image.' }) @ApiBadRequestResponse({ description: 'Bad request for image details.' }) @ApiForbiddenResponse({ description: 'Unauthorized request for image details.' }) @ApiNotFoundResponse({ description: 'Image not found.' }) @@ -92,7 +92,7 @@ export default class ImageHttpController { @ProjectId() _projectId: string, @VersionId() _versionId: string, @ImageId() imageId: string, - ): Promise { + ): Promise { return await this.service.getImageDetails(imageId) } @@ -105,7 +105,7 @@ export default class ImageHttpController { summary: 'Add images to a version.', }) @ApiBody({ type: AddImagesDto, isArray: true }) - @ApiCreatedResponse({ type: ImageDto, isArray: true, description: 'New image added.' }) + @ApiCreatedResponse({ type: ImageDetailsDto, isArray: true, description: 'New image added.' }) @ApiBadRequestResponse({ description: 'Bad request for images.' }) @ApiForbiddenResponse({ description: 'Unauthorized request for images.' }) @UseGuards(ImageAddToVersionTeamAccessGuard) @@ -117,7 +117,7 @@ export default class ImageHttpController { @VersionId() versionId: string, @Body() request: AddImagesDto[], @IdentityFromRequest() identity: Identity, - ): Promise> { + ): Promise> { const images = await this.service.addImagesToVersion(teamSlug, versionId, request, identity) return { diff --git a/web/crux/src/app/image/image.mapper.ts b/web/crux/src/app/image/image.mapper.ts index c2013a7b08..9f41c5d1db 100644 --- a/web/crux/src/app/image/image.mapper.ts +++ b/web/crux/src/app/image/image.mapper.ts @@ -1,33 +1,39 @@ -import { Injectable } from '@nestjs/common' -import { - ContainerConfig, - DeploymentStrategy, - ExposeStrategy, - Image, - NetworkMode, - Registry, - RestartPolicy, -} from '@prisma/client' +import { forwardRef, Inject, Injectable } from '@nestjs/common' +import { DeploymentStrategy, ExposeStrategy, Image, NetworkMode, RestartPolicy } from '@prisma/client' import { ContainerConfigData } from 'src/domain/container' +import { ImageDetails, ImageWithRegistry } from 'src/domain/image' import { + networkModeToJSON, DeploymentStrategy as ProtoDeploymentStrategy, ExposeStrategy as ProtoExposeStrategy, NetworkMode as ProtoNetworkMode, RestartPolicy as ProtoRestartPolicy, - networkModeToJSON, } from 'src/grpc/protobuf/proto/common' import ContainerMapper from '../container/container.mapper' import RegistryMapper from '../registry/registry.mapper' -import { ImageDto } from './image.dto' +import { ImageDetailsDto, ImageDto } from './image.dto' @Injectable() export default class ImageMapper { constructor( private registryMapper: RegistryMapper, + @Inject(forwardRef(() => ContainerMapper)) private readonly containerMapper: ContainerMapper, ) {} - toDto(it: ImageDetails): ImageDto { + toDto(it: ImageWithRegistry): ImageDto { + return { + id: it.id, + name: it.name, + tag: it.tag, + order: it.order, + registry: this.registryMapper.toDto(it.registry), + createdAt: it.createdAt, + labels: it.labels as Record, + } + } + + toDetailsDto(it: ImageDetails): ImageDetailsDto { return { id: it.id, name: it.name, @@ -114,8 +120,3 @@ export default class ImageMapper { return networkModeToJSON(it).toLowerCase() as NetworkMode } } - -export type ImageDetails = Image & { - config: ContainerConfig - registry: Registry -} diff --git a/web/crux/src/app/image/image.module.ts b/web/crux/src/app/image/image.module.ts index 2db99507fe..209f911ff8 100644 --- a/web/crux/src/app/image/image.module.ts +++ b/web/crux/src/app/image/image.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common' +import { forwardRef, Module } from '@nestjs/common' import EncryptionService from 'src/services/encryption.service' import KratosService from 'src/services/kratos.service' import PrismaService from 'src/services/prisma.service' @@ -14,7 +14,7 @@ import ImageMapper from './image.mapper' import ImageService from './image.service' @Module({ - imports: [RegistryModule, EditorModule, ContainerModule, AuditLoggerModule], + imports: [RegistryModule, EditorModule, forwardRef(() => ContainerModule), AuditLoggerModule], exports: [ImageService, ImageMapper], providers: [ PrismaService, diff --git a/web/crux/src/app/image/image.service.ts b/web/crux/src/app/image/image.service.ts index 72f0e44104..99203a5e84 100644 --- a/web/crux/src/app/image/image.service.ts +++ b/web/crux/src/app/image/image.service.ts @@ -9,7 +9,7 @@ import { v4 as uuid } from 'uuid' import ContainerConfigService from '../container/container-config.service' import RegistryClientProvider from '../registry/registry-client.provider' import TeamRepository from '../team/team.repository' -import { AddImagesDto, ImageDto, PatchImageDto } from './image.dto' +import { AddImagesDto, ImageDetailsDto, PatchImageDto } from './image.dto' import ImageMapper from './image.mapper' type LabelMap = Record @@ -27,7 +27,7 @@ export default class ImageService { private readonly registryClients: RegistryClientProvider, ) {} - async getImagesByVersionId(versionId: string): Promise { + async getImagesByVersionId(versionId: string): Promise { const images = await this.prisma.image.findMany({ where: { versionId, @@ -38,10 +38,10 @@ export default class ImageService { }, }) - return images.map(it => this.mapper.toDto(it)) + return images.map(it => this.mapper.toDetailsDto(it)) } - async getImageDetails(imageId: string): Promise { + async getImageDetails(imageId: string): Promise { const image = await this.prisma.image.findUniqueOrThrow({ where: { id: imageId, @@ -51,7 +51,7 @@ export default class ImageService { registry: true, }, }) - return this.mapper.toDto(image) + return this.mapper.toDetailsDto(image) } async addImagesToVersion( @@ -59,7 +59,7 @@ export default class ImageService { versionId: string, request: AddImagesDto[], identity: Identity, - ): Promise { + ): Promise { const teamId = await this.teamRepository.getTeamIdBySlug(teamSlug) const labelLookupPromises = request.map(async it => { @@ -169,7 +169,7 @@ export default class ImageService { ), ) - const dtos = images.map(it => this.mapper.toDto(it)) + const dtos = images.map(it => this.mapper.toDetailsDto(it)) const event: ImagesAddedEvent = { versionId, @@ -235,7 +235,6 @@ export default class ImageService { data: { labels: labels ?? undefined, tag: request.tag ?? undefined, - updatedAt: new Date(), updatedBy: identity.id, }, }) diff --git a/web/crux/src/app/node/node.service.ts b/web/crux/src/app/node/node.service.ts index 606679b371..5dcb432afe 100644 --- a/web/crux/src/app/node/node.service.ts +++ b/web/crux/src/app/node/node.service.ts @@ -301,15 +301,19 @@ export default class NodeService { async deleteAllContainers(nodeId: string, prefix: string): Promise { await this.sendDeleteContainerCommand(nodeId, 'deleteContainers', { - prefix, + target: { + prefix, + }, }) } async deleteContainer(nodeId: string, prefix: string, name: string): Promise { await this.sendDeleteContainerCommand(nodeId, 'deleteContainer', { - container: { - prefix: prefix ?? '', - name, + target: { + container: { + prefix: prefix ?? '', + name, + }, }, }) } diff --git a/web/crux/src/app/node/pipes/node.get-script.pipe.ts b/web/crux/src/app/node/pipes/node.get-script.pipe.ts index a59dde79fd..e9f669fda4 100644 --- a/web/crux/src/app/node/pipes/node.get-script.pipe.ts +++ b/web/crux/src/app/node/pipes/node.get-script.pipe.ts @@ -27,7 +27,7 @@ export default class NodeGetScriptValidationPipe implements PipeTransform { } } - // throwing intentionally ambigous exceptions, so an attacker can not guess node ids + // throwing intentionally ambiguous exceptions, so an attacker can not guess node ids throw new CruxUnauthorizedException() } } diff --git a/web/crux/src/app/package/package.service.ts b/web/crux/src/app/package/package.service.ts index 89cd34c791..0118728b71 100644 --- a/web/crux/src/app/package/package.service.ts +++ b/web/crux/src/app/package/package.service.ts @@ -129,7 +129,6 @@ class PackageService { })), }, updatedBy: identity.id, - updatedAt: new Date(), }, }) } @@ -213,7 +212,6 @@ class PackageService { id: packageId, }, data: { - updatedAt: new Date(), updatedBy: identity.id, }, }) @@ -232,7 +230,6 @@ class PackageService { id: packageId, }, data: { - updatedAt: new Date(), updatedBy: identity.id, environments: { update: { @@ -256,7 +253,6 @@ class PackageService { id: packageId, }, data: { - updatedAt: new Date(), updatedBy: identity.id, environments: { delete: { @@ -381,32 +377,42 @@ class PackageService { if (sourceVersion.deployments.length < 1) { // create a new empty deployment - const deployment = await this.prisma.deployment.create({ - data: { - nodeId: env.nodeId, - prefix: env.prefix, - versionId: target.id, - status: DeploymentStatusEnum.preparing, - createdBy: identity.id, - instances: { - createMany: { - data: sourceVersion.images.map(it => ({ - imageId: it.id, - })), - }, + const deploy = await this.prisma.$transaction(async prisma => { + const deployment = await prisma.deployment.create({ + data: { + version: { connect: { id: target.id } }, + node: { connect: { id: env.nodeId } }, + config: { create: { type: 'deployment' } }, + prefix: env.prefix, + status: DeploymentStatusEnum.preparing, + createdBy: identity.id, }, - }, - include: { - node: true, - version: { - include: { - project: true, + include: { + node: true, + version: { + include: { + project: true, + }, }, }, - }, + }) + + await Promise.all( + sourceVersion.images.map(async image => { + await prisma.instance.create({ + data: { + deployment: { connect: { id: deployment.id } }, + image: { connect: { id: image.id } }, + config: { create: { type: 'instance' } }, + }, + }) + }), + ) + + return deployment }) - return this.deployMapper.toDto(deployment) + return this.deployMapper.toDto(deploy) } // copy deployment from target @@ -495,8 +501,9 @@ class PackageService { if (!instance) { await this.prisma.instance.create({ data: { - deploymentId: newDeployment.id, - imageId: image.id, + deployment: { connect: { id: newDeployment.id } }, + image: { connect: { id: image.id } }, + config: { create: { type: 'instance' } }, }, }) } diff --git a/web/crux/src/app/pipeline/pipeline.service.ts b/web/crux/src/app/pipeline/pipeline.service.ts index e3486306cc..a6a166c15a 100644 --- a/web/crux/src/app/pipeline/pipeline.service.ts +++ b/web/crux/src/app/pipeline/pipeline.service.ts @@ -210,7 +210,6 @@ export default class PipelineService { ...this.mapper.detailsToDb(req, repo.projectId), hooks, token: !req.token ? undefined : this.encryptionService.encrypt(req.token), - updatedAt: new Date(), updatedBy: identity.id, }, }) @@ -316,7 +315,6 @@ export default class PipelineService { }, data: { ...this.mapper.eventWatcherToDb(req), - updatedAt: new Date(), updatedBy: identity.id, }, }) diff --git a/web/crux/src/app/project/project.mapper.ts b/web/crux/src/app/project/project.mapper.ts index 0995828941..d0fbc50816 100644 --- a/web/crux/src/app/project/project.mapper.ts +++ b/web/crux/src/app/project/project.mapper.ts @@ -1,8 +1,9 @@ import { Project } from '.prisma/client' import { Inject, Injectable, forwardRef } from '@nestjs/common' +import { VersionWithChildren } from 'src/domain/version' import { BasicProperties } from 'src/shared/dtos/shared.dto' import AuditMapper from '../audit/audit.mapper' -import VersionMapper, { VersionWithChildren } from '../version/version.mapper' +import VersionMapper from '../version/version.mapper' import { BasicProjectDto, ProjectDetailsDto, ProjectDto, ProjectListItemDto } from './project.dto' @Injectable() @@ -52,7 +53,7 @@ type ProjectWithVersions = Project & { deletable: boolean } -export type ProjectWithCount = Project & { +type ProjectWithCount = Project & { _count: { versions: number } diff --git a/web/crux/src/app/project/project.module.ts b/web/crux/src/app/project/project.module.ts index d81e37854d..0a149c6ba6 100644 --- a/web/crux/src/app/project/project.module.ts +++ b/web/crux/src/app/project/project.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common' +import { forwardRef, Module } from '@nestjs/common' import KratosService from 'src/services/kratos.service' import PrismaService from 'src/services/prisma.service' import AuditLoggerModule from '../audit.logger/audit.logger.module' @@ -12,7 +12,7 @@ import ProjectMapper from './project.mapper' import ProjectService from './project.service' @Module({ - imports: [VersionModule, TeamModule, TokenModule, AuditLoggerModule], + imports: [forwardRef(() => VersionModule), TeamModule, TokenModule, AuditLoggerModule], exports: [ProjectMapper, ProjectService], controllers: [ProjectHttpController], providers: [PrismaService, ProjectService, ProjectMapper, TeamRepository, KratosService, AuditMapper], diff --git a/web/crux/src/app/version/version.dto.ts b/web/crux/src/app/version/version.dto.ts index d2aed6915b..06d4d8c5f3 100644 --- a/web/crux/src/app/version/version.dto.ts +++ b/web/crux/src/app/version/version.dto.ts @@ -3,7 +3,7 @@ import { Type } from 'class-transformer' import { IsBoolean, IsIn, IsNotEmpty, IsOptional, IsString, IsUUID, ValidateNested } from 'class-validator' import { AuditDto } from '../audit/audit.dto' import { DeploymentWithBasicNodeDto } from '../deploy/deploy.dto' -import { ImageDto } from '../image/image.dto' +import { ImageDetailsDto } from '../image/image.dto' export const VERSION_TYPE_VALUES = ['incremental', 'rolling'] as const export type VersionTypeDto = (typeof VERSION_TYPE_VALUES)[number] @@ -88,8 +88,8 @@ export class VersionDetailsDto extends VersionDto { @IsOptional() autoCopyDeployments?: boolean - @Type(() => ImageDto) - images: ImageDto[] + @Type(() => ImageDetailsDto) + images: ImageDetailsDto[] deployments: DeploymentWithBasicNodeDto[] } diff --git a/web/crux/src/app/version/version.mapper.ts b/web/crux/src/app/version/version.mapper.ts index 63e3024a8c..dd214b5a1b 100644 --- a/web/crux/src/app/version/version.mapper.ts +++ b/web/crux/src/app/version/version.mapper.ts @@ -1,14 +1,18 @@ import { Version } from '.prisma/client' -import { Inject, Injectable, forwardRef } from '@nestjs/common' -import { ProjectTypeEnum } from '@prisma/client' +import { forwardRef, Inject, Injectable } from '@nestjs/common' import { ImageDeletedEvent, ImagesAddedEvent } from 'src/domain/domain-events' -import { versionIsDeletable, versionIsIncreasable, versionIsMutable } from 'src/domain/version' +import { + VersionDetails, + versionIsDeletable, + versionIsIncreasable, + versionIsMutable, + VersionWithChildren, +} from 'src/domain/version' import { VersionChainWithEdges } from 'src/domain/version-chain' import { BasicProperties } from '../../shared/dtos/shared.dto' import AuditMapper from '../audit/audit.mapper' -import { DeploymentWithNode } from '../deploy/deploy.dto' import DeployMapper from '../deploy/deploy.mapper' -import ImageMapper, { ImageDetails } from '../image/image.mapper' +import ImageMapper from '../image/image.mapper' import { NodeConnectionStatus } from '../node/node.dto' import { BasicVersionDto, VersionChainDto, VersionDetailsDto, VersionDto } from './version.dto' import { ImageDeletedMessage, ImagesAddedMessage } from './version.message' @@ -18,6 +22,7 @@ export default class VersionMapper { constructor( @Inject(forwardRef(() => DeployMapper)) private deployMapper: DeployMapper, + @Inject(forwardRef(() => ImageMapper)) private imageMapper: ImageMapper, private auditMapper: AuditMapper, ) {} @@ -54,7 +59,7 @@ export default class VersionMapper { deletable: versionIsDeletable(version), increasable: versionIsIncreasable(version), autoCopyDeployments: version.autoCopyDeployments, - images: version.images.map(it => this.imageMapper.toDto(it)), + images: version.images.map(it => this.imageMapper.toDetailsDto(it)), deployments: version.deployments.map(it => this.deployMapper.toDeploymentWithBasicNodeDto(it, nodeStatusLookup.get(it.nodeId)), ), @@ -77,7 +82,7 @@ export default class VersionMapper { imagesAddedEventToMessage(event: ImagesAddedEvent): ImagesAddedMessage { return { - images: event.images.map(it => this.imageMapper.toDto(it)), + images: event.images.map(it => this.imageMapper.toDetailsDto(it)), } } @@ -87,15 +92,3 @@ export default class VersionMapper { } } } - -export type VersionWithChildren = Version & { - children: { versionId: string }[] -} - -export type VersionDetails = VersionWithChildren & { - project: { - type: ProjectTypeEnum - } - images: ImageDetails[] - deployments: DeploymentWithNode[] -} diff --git a/web/crux/src/app/version/version.message.ts b/web/crux/src/app/version/version.message.ts index 096464cbe5..f66390e72c 100644 --- a/web/crux/src/app/version/version.message.ts +++ b/web/crux/src/app/version/version.message.ts @@ -1,17 +1,17 @@ -import { AddImagesDto, ImageDto } from '../image/image.dto' +import { AddImagesDto, ImageDetailsDto } from '../image/image.dto' export type GetImageMessage = { id: string } export const WS_TYPE_IMAGE = 'image' -export type ImageMessage = ImageDto +export type ImageMessage = ImageDetailsDto export type AddImagesMessage = { registryImages: AddImagesDto[] } -export const WS_TYPE_IMAGE_SET_TAG = 'image-set-tag' +export const WS_TYPE_SET_IMAGE_TAG = 'set-image-tag' export const WS_TYPE_IMAGE_TAG_UPDATED = 'image-tag-updated' export type ImageTagMessage = { imageId: string @@ -20,7 +20,7 @@ export type ImageTagMessage = { export const WS_TYPE_IMAGES_ADDED = 'images-added' export type ImagesAddedMessage = { - images: ImageDto[] + images: ImageDetailsDto[] } export type DeleteImageMessage = { diff --git a/web/crux/src/app/version/version.module.ts b/web/crux/src/app/version/version.module.ts index c0342c3a6a..8f1b814908 100644 --- a/web/crux/src/app/version/version.module.ts +++ b/web/crux/src/app/version/version.module.ts @@ -1,5 +1,5 @@ import { HttpModule } from '@nestjs/axios' -import { Module } from '@nestjs/common' +import { forwardRef, Module } from '@nestjs/common' import NotificationTemplateBuilder from 'src/builders/notification.template.builder' import DomainNotificationService from 'src/services/domain.notification.service' import KratosService from 'src/services/kratos.service' @@ -20,7 +20,15 @@ import VersionService from './version.service' import VersionWebSocketGateway from './version.ws.gateway' @Module({ - imports: [ImageModule, ContainerModule, HttpModule, DeployModule, AgentModule, EditorModule, AuditLoggerModule], + imports: [ + forwardRef(() => ImageModule), + forwardRef(() => ContainerModule), + HttpModule, + forwardRef(() => DeployModule), + forwardRef(() => AgentModule), + EditorModule, + AuditLoggerModule, + ], exports: [VersionService, VersionMapper], controllers: [VersionHttpController, VersionChainHttpController], providers: [ diff --git a/web/crux/src/app/version/version.service.ts b/web/crux/src/app/version/version.service.ts index de35ee10de..b1efb77a14 100644 --- a/web/crux/src/app/version/version.service.ts +++ b/web/crux/src/app/version/version.service.ts @@ -1,7 +1,7 @@ -import { Injectable, Logger } from '@nestjs/common' +import { forwardRef, Inject, Injectable, Logger } from '@nestjs/common' import { Identity } from '@ory/kratos-client' import { DeploymentStatusEnum, Prisma } from '@prisma/client' -import { Observable, filter, map } from 'rxjs' +import { filter, map, Observable } from 'rxjs' import { IMAGE_EVENT_ADD, IMAGE_EVENT_DELETE, ImageDeletedEvent, ImagesAddedEvent } from 'src/domain/domain-events' import { VersionMessage } from 'src/domain/notification-templates' import { versionChainMembersOf } from 'src/domain/version-chain' @@ -27,7 +27,7 @@ import { VersionListQuery, } from './version.dto' import VersionMapper from './version.mapper' -import { WS_TYPE_IMAGES_ADDED, WS_TYPE_IMAGE_DELETED } from './version.message' +import { WS_TYPE_IMAGE_DELETED, WS_TYPE_IMAGES_ADDED } from './version.message' @Injectable() export default class VersionService { @@ -38,6 +38,7 @@ export default class VersionService { private readonly mapper: VersionMapper, private readonly imageMapper: ImageMapper, private readonly deployMapper: DeployMapper, + @Inject(forwardRef(() => ContainerMapper)) private readonly containerMapper: ContainerMapper, private readonly domainEvents: VersionDomainEventListener, private readonly notificationService: DomainNotificationService, @@ -584,6 +585,11 @@ export default class VersionService { }, }, instances: undefined, + config: { + create: { + type: 'deployment', + }, + }, }, }) diff --git a/web/crux/src/app/version/version.ws.gateway.ts b/web/crux/src/app/version/version.ws.gateway.ts index 79621bf52f..8cd1c31765 100644 --- a/web/crux/src/app/version/version.ws.gateway.ts +++ b/web/crux/src/app/version/version.ws.gateway.ts @@ -42,17 +42,14 @@ import { WS_TYPE_IMAGES_ADDED, WS_TYPE_IMAGES_WERE_REORDERED, WS_TYPE_IMAGE_DELETED, - WS_TYPE_IMAGE_SET_TAG, WS_TYPE_IMAGE_TAG_UPDATED, + WS_TYPE_SET_IMAGE_TAG, } from './version.message' import VersionService from './version.service' const VersionId = () => WsParam('versionId') const TeamSlug = () => WsParam('teamSlug') -// TODO(@m8vago): make an event aggregator for image updates patches etc -// so subscribers will be notified of the changes regardless of the transport platform - @WebSocketGateway({ namespace: ':teamSlug/projects/:projectId/versions/:versionId', }) @@ -162,7 +159,7 @@ export default class VersionWebSocketGateway { subscription.sendToAll(res) } - @SubscribeMessage(WS_TYPE_IMAGE_SET_TAG) + @SubscribeMessage(WS_TYPE_SET_IMAGE_TAG) async setImageTag( @TeamSlug() teamSlug, @SocketClient() client: WsClient, diff --git a/web/crux/src/domain/agent-callback.ts b/web/crux/src/domain/agent-callback.ts index 0763b87871..b8b3b00df7 100644 --- a/web/crux/src/domain/agent-callback.ts +++ b/web/crux/src/domain/agent-callback.ts @@ -75,6 +75,7 @@ export default class AgentCallback { } onError(key: string, error: AgentError) { + console.log('error', key) const result = this.requests.get(key) if (!result) { return diff --git a/web/crux/src/domain/agent.ts b/web/crux/src/domain/agent.ts index 76acb1883f..2792a1d694 100755 --- a/web/crux/src/domain/agent.ts +++ b/web/crux/src/domain/agent.ts @@ -22,6 +22,7 @@ import { ContainerIdentifier, ContainerInspectResponse, ContainerLogListResponse, + ContainerOrPrefix, DeleteContainersRequest, DeploymentStatusMessage, Empty, @@ -106,7 +107,7 @@ export class Agent { this.outdated = options.outdated const callbacks: Record> = { - listSecrets: (req: ListSecretsRequest) => [Agent.containerPrefixNameOf(req.container), { listSecrets: req }], + listSecrets: (req: ListSecretsRequest) => [Agent.containerPrefixNameOrPrefixOf(req.target), { listSecrets: req }], containerLog: (req: ContainerLogRequest) => [ Agent.containerPrefixNameOf(req.container), { @@ -121,12 +122,7 @@ export class Agent { { containerInspect: req }, ], deleteContainers: (req: DeleteContainersRequest) => [ - Agent.containerPrefixNameOf( - req?.container ?? { - prefix: req.prefix, - name: null, - }, - ), + Agent.containerPrefixNameOrPrefixOf(req.target), { deleteContainers: req }, ], } @@ -494,6 +490,9 @@ export class Agent { public static containerPrefixNameOf = (id: ContainerIdentifier): string => !id.prefix ? id.name : `${id.prefix}-${id.name ?? ''}` + + public static containerPrefixNameOrPrefixOf = (target: ContainerOrPrefix) => + this.containerPrefixNameOf(target.container ?? { prefix: target.prefix, name: '' }) } export type AgentConnectionMessage = { diff --git a/web/crux/src/domain/container-conflict.ts b/web/crux/src/domain/container-conflict.ts index 6f81dfb59d..039d232d06 100644 --- a/web/crux/src/domain/container-conflict.ts +++ b/web/crux/src/domain/container-conflict.ts @@ -8,7 +8,6 @@ import { Port, PortRange, ResourceConfig, - Storage, UniqueKeyValue, Volume, } from './container' @@ -44,12 +43,6 @@ export type ConflictedMarker = { ingress?: ConflictedUniqueItem[] } -type StorageData = { - storageSet?: boolean - storageId?: string - storageConfig?: Storage -} - type ConflictedLogKeys = { driver?: boolean options?: string[] @@ -69,7 +62,7 @@ export type ConflictedContainerConfigData = { workingDirectory?: string[] tty?: string[] configContainer?: string[] - ports?: number[] + ports?: ConflictedPort[] portRanges?: ConflictedPortRange[] volumes?: ConflictedUniqueItem[] initContainers?: string[] @@ -272,14 +265,22 @@ const appendMarkerConflict = ( return conflicts } -const stringsConflict = (one: string, other: string): boolean => one && other && one !== other +const stringsConflict = (one: string, other: string): boolean => { + if (typeof one !== 'string' || typeof other !== 'string') { + // one of them are null or uninterpretable + return false + } + + return one !== other +} + const booleansConflict = (one: boolean, other: boolean): boolean => { if (typeof one !== 'boolean' || typeof other !== 'boolean') { - // some of them are null or uninterpretable + // one of them are null or uninterpretable return false } - return one === other + return one !== other } const numbersConflict = (one: number, other: number): boolean => { @@ -288,7 +289,7 @@ const numbersConflict = (one: number, other: number): boolean => { return false } - return one === other + return one !== other } const objectsConflict = (one: object, other: object): boolean => { @@ -297,7 +298,7 @@ const objectsConflict = (one: object, other: object): boolean => { return false } - return JSON.stringify(one) === JSON.stringify(other) + return JSON.stringify(one) !== JSON.stringify(other) } // returns the conflicting keys @@ -309,6 +310,9 @@ const uniqueKeyValuesConflict = (one: UniqueKeyValue[], other: UniqueKeyValue[]) const conflicts = one .filter(item => { const otherItem = other.find(it => it.key === item.key) + if (!otherItem) { + return false + } return item.value !== otherItem.value }) @@ -376,18 +380,6 @@ const volumesConflict = (one: Volume[], other: Volume[]): string[] | null => { return conflicts } -const storagesConflict = (one: StorageData, other: StorageData): boolean => { - if (!one || !other) { - return false - } - - if (!one.storageSet || other.storageSet) { - return false - } - - return stringsConflict(one.storageId, other.storageId) || objectsConflict(one.storageConfig, other.storageConfig) -} - const logsConflict = (one: Log, other: Log): ConflictedLogKeys | null => { if (!one || !other) { return null @@ -543,7 +535,7 @@ const collectConflicts = ( } const checkStorageConflict = () => { - if (storagesConflict(one, other)) { + if (objectsConflict(one, other)) { conflicts.storage = appendConflict(conflicts.storage, one.id, other.id) } } @@ -627,7 +619,7 @@ const collectConflicts = ( type ContainerConfigDataProperty = keyof ContainerConfigData export const checkForConflicts = ( configs: ContainerConfigDataWithId[], - interestedProperties: ContainerConfigDataProperty[] = [], + definedKeys: ContainerConfigDataProperty[] = [], ): ConflictedContainerConfigData | null => { configs = configs.map(conf => { const newConf: ContainerConfigDataWithId = { @@ -636,12 +628,7 @@ export const checkForConflicts = ( Object.keys(conf).forEach(it => { const prop = it as ContainerConfigDataProperty - if (prop === 'secrets') { - // secrets can not be overriden - return - } - - if (interestedProperties.includes(prop)) { + if (!definedKeys.includes(prop)) { return } @@ -659,15 +646,31 @@ export const checkForConflicts = ( others.forEach(other => collectConflicts(conflicts, one, other)) }) - if (Object.keys(configs).length < 1) { + if (Object.keys(conflicts).length < 1) { return null } return conflicts } +const UNINTERESTED_KEYS = ['id', 'type', 'updatedAt', 'updatedBy', 'secrets'] export const getConflictsForConcreteConfig = ( configs: ContainerConfigDataWithId[], concreteConfig: ConcreteContainerConfigData, ): ConflictedContainerConfigData | null => - checkForConflicts(configs, Object.keys(concreteConfig) as ContainerConfigDataProperty[]) + checkForConflicts( + configs, + Object.entries(concreteConfig) + .filter(entry => { + const [key, value] = entry + if (UNINTERESTED_KEYS.includes(key)) { + return false + } + + return typeof value !== 'undefined' && value !== null + }) + .map(entry => { + const [key] = entry + return key + }) as ContainerConfigDataProperty[], + ) diff --git a/web/crux/src/domain/container-merge.ts b/web/crux/src/domain/container-merge.ts index 5fe1510dbb..378952445c 100644 --- a/web/crux/src/domain/container-merge.ts +++ b/web/crux/src/domain/container-merge.ts @@ -81,7 +81,7 @@ export const mergeMarkers = (strong: Marker, weak: Marker): Marker => { } } -const squashSecrets = (one: UniqueSecretKey[], other: UniqueSecretKey[]): UniqueSecretKey[] => { +const mergeSecretKeys = (one: UniqueSecretKey[], other: UniqueSecretKey[]): UniqueSecretKey[] => { if (!one) { return other } @@ -93,18 +93,6 @@ const squashSecrets = (one: UniqueSecretKey[], other: UniqueSecretKey[]): Unique return [...one, ...other.filter(it => !one.includes(it))] } -const squashConfigs = (configs: ContainerConfigData[]): ContainerConfigData => - configs.reduce((result, conf) => { - if ('secrets' in conf) { - conf.secrets = squashSecrets(result.secrets, conf.secrets) - } - - return { - ...result, - ...conf, - } - }, {} as ContainerConfigData) - export const mergeSecrets = (strong: UniqueSecretKeyValue[], weak: UniqueSecretKey[]): UniqueSecretKeyValue[] => { weak = weak ?? [] strong = strong ?? [] @@ -123,6 +111,50 @@ export const mergeSecrets = (strong: UniqueSecretKeyValue[], weak: UniqueSecretK return [...missing, ...strong] } +export const mergeConfigs = (strong: ContainerConfigData, weak: ContainerConfigData): ContainerConfigData => ({ + // common + name: strong.name ?? weak.name, + environment: strong.environment ?? weak.environment, + secrets: mergeSecretKeys(strong.secrets, weak.secrets), + user: mergeNumber(strong.user, weak.user), + workingDirectory: strong.workingDirectory ?? weak.workingDirectory, + tty: mergeBoolean(strong.tty, weak.tty), + portRanges: strong.portRanges ?? weak.portRanges, + args: strong.args ?? weak.args, + commands: strong.commands ?? weak.commands, + expose: strong.expose ?? weak.expose, + configContainer: strong.configContainer ?? weak.configContainer, + routing: strong.routing ?? weak.routing, + volumes: strong.volumes ?? weak.volumes, + initContainers: strong.initContainers ?? weak.initContainers, + capabilities: [], // TODO (@m8vago, @nandor-magyar): capabilities feature is still missing + ports: strong.ports ?? weak.ports, + ...mergeStorage(strong, weak), + + // crane + customHeaders: strong.customHeaders ?? weak.customHeaders, + proxyHeaders: mergeBoolean(strong.proxyHeaders, weak.proxyHeaders), + extraLBAnnotations: strong.extraLBAnnotations ?? weak.extraLBAnnotations, + healthCheckConfig: strong.healthCheckConfig ?? weak.healthCheckConfig, + resourceConfig: strong.resourceConfig ?? weak.resourceConfig, + useLoadBalancer: mergeBoolean(strong.useLoadBalancer, weak.useLoadBalancer), + deploymentStrategy: strong.deploymentStrategy ?? weak.deploymentStrategy, + labels: mergeMarkers(strong.labels, weak.labels), + annotations: mergeMarkers(strong.annotations, weak.annotations), + metrics: strong.metrics ?? weak.metrics, + + // dagent + logConfig: strong.logConfig ?? weak.logConfig, + networkMode: strong.networkMode ?? weak.networkMode, + restartPolicy: strong.restartPolicy ?? weak.restartPolicy, + networks: strong.networks ?? weak.networks, + dockerLabels: strong.dockerLabels ?? weak.dockerLabels, + expectedState: strong.expectedState ?? weak.expectedState, +}) + +const squashConfigs = (configs: ContainerConfigData[]): ContainerConfigData => + configs.reduce((result, conf) => mergeConfigs(conf, result), {} as ContainerConfigData) + // this assumes that the concrete config takes care of any conflict between the other configs export const mergeConfigsWithConcreteConfig = ( configs: ContainerConfigData[], @@ -131,45 +163,11 @@ export const mergeConfigsWithConcreteConfig = ( const squashed = squashConfigs(configs.filter(it => !!it)) concrete = concrete ?? {} + const baseConfig = mergeConfigs(concrete, squashed) + return { - // common - name: concrete.name ?? squashed.name, - environment: concrete.environment ?? squashed.environment, + ...baseConfig, secrets: mergeSecrets(concrete.secrets, squashed.secrets), - user: mergeNumber(concrete.user, squashed.user), - workingDirectory: concrete.workingDirectory ?? squashed.workingDirectory, - tty: mergeBoolean(concrete.tty, squashed.tty), - portRanges: concrete.portRanges ?? squashed.portRanges, - args: concrete.args ?? squashed.args, - commands: concrete.commands ?? squashed.commands, - expose: concrete.expose ?? squashed.expose, - configContainer: concrete.configContainer ?? squashed.configContainer, - routing: concrete.routing ?? squashed.routing, - volumes: concrete.volumes ?? squashed.volumes, - initContainers: concrete.initContainers ?? squashed.initContainers, - capabilities: [], // TODO (@m8vago, @nandor-magyar): capabilities feature is still missing - ports: concrete.ports ?? squashed.ports, - ...mergeStorage(concrete, squashed), - - // crane - customHeaders: concrete.customHeaders ?? squashed.customHeaders, - proxyHeaders: mergeBoolean(concrete.proxyHeaders, squashed.proxyHeaders), - extraLBAnnotations: concrete.extraLBAnnotations ?? squashed.extraLBAnnotations, - healthCheckConfig: concrete.healthCheckConfig ?? squashed.healthCheckConfig, - resourceConfig: concrete.resourceConfig ?? squashed.resourceConfig, - useLoadBalancer: mergeBoolean(concrete.useLoadBalancer, squashed.useLoadBalancer), - deploymentStrategy: concrete.deploymentStrategy ?? squashed.deploymentStrategy, - labels: mergeMarkers(concrete.labels, squashed.labels), - annotations: mergeMarkers(concrete.annotations, squashed.annotations), - metrics: concrete.metrics ?? squashed.metrics, - - // dagent - logConfig: concrete.logConfig ?? squashed.logConfig, - networkMode: concrete.networkMode ?? squashed.networkMode, - restartPolicy: concrete.restartPolicy ?? squashed.restartPolicy, - networks: concrete.networks ?? squashed.networks, - dockerLabels: concrete.dockerLabels ?? squashed.dockerLabels, - expectedState: concrete.expectedState ?? squashed.expectedState, } } diff --git a/web/crux/src/domain/container.ts b/web/crux/src/domain/container.ts index e5ffe35a68..599cc7f63b 100644 --- a/web/crux/src/domain/container.ts +++ b/web/crux/src/domain/container.ts @@ -268,3 +268,14 @@ export const CONTAINER_CONFIG_COMPOSITE_FIELDS = { } export const configIsEmpty = (config: T): boolean => Object.keys(config).length < 1 + +type InstanceWithConfigAndImageConfig = { + config: { name: string } + image: { + name: string + config: { name: string } + } +} + +export const nameOfInstance = (instance: InstanceWithConfigAndImageConfig) => + instance.config.name ?? instance.image.config.name ?? instance.image.name diff --git a/web/crux/src/domain/deployment.spec.ts b/web/crux/src/domain/deployment.spec.ts index 67d38cff6e..1a8a8898fb 100644 --- a/web/crux/src/domain/deployment.spec.ts +++ b/web/crux/src/domain/deployment.spec.ts @@ -2,11 +2,11 @@ import { DeploymentStatusEnum, VersionTypeEnum } from '@prisma/client' import { ContainerState, DeploymentStatus as ProtoDeploymentStatus } from 'src/grpc/protobuf/proto/common' import { checkDeploymentCopiability, - checkDeploymentDeletability, checkDeploymentDeployability, - checkDeploymentMutability, containerNameFromImageName, containerStateToDto, + deploymentIsDeletable, + deploymentIsMutable, deploymentStatusToDb, } from './deployment' @@ -63,7 +63,7 @@ describe('DomainDeployment', () => { ) it.each(DEPLOYMENT_STATUSES)('%p and %p', (status: DeploymentStatusEnum) => { - expect(checkDeploymentDeletability(status)).toEqual(status !== 'inProgress') + expect(deploymentIsDeletable(status)).toEqual(status !== 'inProgress') }) }) @@ -71,7 +71,7 @@ describe('DomainDeployment', () => { it.each(DEPLOYMENT_STATUSES_VERSION_TYPES)( 'should return true if status is deploying or if the status is successful or failed and the version is rolling (%p and %p)', (status: DeploymentStatusEnum, type: VersionTypeEnum) => { - expect(checkDeploymentMutability(status, type)).toEqual( + expect(deploymentIsMutable(status, type)).toEqual( status === 'preparing' || status === 'failed' || (status === 'successful' && type === 'rolling'), ) }, diff --git a/web/crux/src/domain/deployment.ts b/web/crux/src/domain/deployment.ts index 5df892bcd3..faaab47333 100644 --- a/web/crux/src/domain/deployment.ts +++ b/web/crux/src/domain/deployment.ts @@ -1,13 +1,20 @@ import { + ConfigBundle, + ContainerConfig, Deployment as DbDeployment, DeploymentEventTypeEnum, DeploymentStatusEnum, + DeploymentToken, + Instance, + Node, + Project, + Version, VersionTypeEnum, } from '.prisma/client' import { Logger } from '@nestjs/common' import { Identity } from '@ory/kratos-client' import { Observable, Subject } from 'rxjs' -import { AgentCommand, VersionDeployRequest } from 'src/grpc/protobuf/proto/agent' +import { AgentCommand, DeployRequest } from 'src/grpc/protobuf/proto/agent' import { DeploymentMessageLevel, DeploymentStatusMessage, @@ -16,7 +23,44 @@ import { containerStateToJSON, deploymentStatusToJSON, } from 'src/grpc/protobuf/proto/common' +import { BasicProperties } from 'src/shared/dtos/shared.dto' import { ConcreteContainerConfigData, ContainerState } from './container' +import { ImageDetails } from './image' + +export type DeploymentWithNode = DbDeployment & { + node: Pick +} + +export type DeploymentWithNodeVersion = DeploymentWithNode & { + version: Pick & { + project: Pick + } +} + +export type InstanceDetails = Instance & { + image: ImageDetails + config: ContainerConfig +} + +type ConfigBundleDetails = ConfigBundle & { + config: ContainerConfig +} + +export type DeploymentWithConfig = DbDeployment & { + config: ContainerConfig +} + +export type DeploymentWithConfigAndBundles = DeploymentWithNodeVersion & { + config: ContainerConfig + configBundles: { + configBundle: ConfigBundleDetails + }[] +} + +export type DeploymentDetails = DeploymentWithConfigAndBundles & { + token: Pick + instances: InstanceDetails[] +} export type DeploymentLogLevel = 'info' | 'warn' | 'error' @@ -99,9 +143,9 @@ export const checkPrefixAvailability = ( return !relevantDeployments.find(it => it.status === 'preparing' || it.status === 'inProgress') } -export const checkDeploymentDeletability = (status: DeploymentStatusEnum): boolean => status !== 'inProgress' +export const deploymentIsDeletable = (status: DeploymentStatusEnum): boolean => status !== 'inProgress' -export const checkDeploymentMutability = (status: DeploymentStatusEnum, type: VersionTypeEnum): boolean => { +export const deploymentIsMutable = (status: DeploymentStatusEnum, type: VersionTypeEnum): boolean => { switch (status) { case 'preparing': case 'failed': @@ -135,7 +179,7 @@ export type DeploymentNotification = { } export type DeploymentOptions = { - request: VersionDeployRequest + request: DeployRequest notification: DeploymentNotification instanceConfigs: Map deploymentConfig: ConcreteContainerConfigData @@ -147,8 +191,6 @@ export default class Deployment { private status: DeploymentStatusEnum = 'preparing' - private readonly request: VersionDeployRequest - get id(): string { return this.options.request.id } @@ -184,7 +226,7 @@ export default class Deployment { }) commandChannel.next({ - deploy: this.request, + deploy: this.options.request, } as AgentCommand) return this.statusChannel.asObservable() diff --git a/web/crux/src/domain/domain-events.ts b/web/crux/src/domain/domain-events.ts index f7779f8fa3..59f15e7c75 100644 --- a/web/crux/src/domain/domain-events.ts +++ b/web/crux/src/domain/domain-events.ts @@ -1,6 +1,6 @@ import { ConfigBundle } from '@prisma/client' -import { ImageDetails } from 'src/app/image/image.mapper' import { ContainerConfigData } from './container' +import { ImageDetails } from './image' // container export const CONTAINER_CONFIG_EVENT_UPDATE = 'container-config.update' diff --git a/web/crux/src/domain/image.ts b/web/crux/src/domain/image.ts index 063beb2db6..bd0253f61b 100644 --- a/web/crux/src/domain/image.ts +++ b/web/crux/src/domain/image.ts @@ -1,3 +1,4 @@ +import { ContainerConfig, Image, Registry } from '@prisma/client' import { CruxInternalServerErrorException } from 'src/exception/crux-exception' export const ENVIRONMENT_VALUE_TYPES = ['string', 'boolean', 'int'] as const @@ -9,6 +10,14 @@ export type EnvironmentRule = { default?: string } +export type ImageWithRegistry = Image & { + registry: Registry +} + +export type ImageDetails = ImageWithRegistry & { + config: ContainerConfig +} + /** * Parse dyrector.io specific image labels which contain environment validation rules. * diff --git a/web/crux/src/domain/start-deployment.ts b/web/crux/src/domain/start-deployment.ts index 28e4c07621..259df810ef 100644 --- a/web/crux/src/domain/start-deployment.ts +++ b/web/crux/src/domain/start-deployment.ts @@ -1,6 +1,7 @@ import { ContainerConfig, DeploymentStatusEnum, VersionTypeEnum } from '@prisma/client' import { ConcreteContainerConfigData, ContainerConfigData, UniqueSecretKeyValue } from './container' import { mergeConfigsWithConcreteConfig, mergeInstanceConfigWithDeploymentConfig } from './container-merge' +import { DeploymentWithConfig } from './deployment' export type InvalidSecrets = { configId: string @@ -18,14 +19,16 @@ export const missingSecretsOf = (configId: string, config: ConcreteContainerConf return null } - const requiredAndMissingSecrets = config.secrets.filter(it => it.required && it.encrypted && it.value.length > 0) - if (requiredAndMissingSecrets.length < 1) { + const requiredSecrets = config.secrets.filter(it => it.required || (it.value && it.value.length > 0)) + const missingSecrets = requiredSecrets.filter(it => !it.encrypted) + + if (missingSecrets.length < 1) { return null } return { configId, - secretKeys: requiredAndMissingSecrets.map(it => it.key), + secretKeys: missingSecrets.map(it => it.key), } } @@ -120,3 +123,43 @@ export const instanceConfigOf = ( const instanceConfig = instance.config as any as ConcreteContainerConfigData return mergeInstanceConfigWithDeploymentConfig(mergedDeploymentConfig, instanceConfig) } + +type SecretCandidate = { + deployedAt: Date + value: string +} +export const mergePrefixNeighborSecrets = ( + deployments: DeploymentWithConfig[], + publicKey: string, +): Record => { + const result = new Map() + + deployments + .sort((one, other) => other.createdAt.getTime() - one.createdAt.getTime()) + .forEach(depl => { + const secrets = depl.config.secrets as UniqueSecretKeyValue[] + secrets.forEach(it => { + if (it.publicKey !== publicKey) { + return + } + + const candidate = result.get(it.key) + if (candidate && candidate.deployedAt.getTime() > depl.deployedAt.getTime()) { + // when there is already a deployment for the key, and it's the more recent one + return + } + + result.set(it.key, { + deployedAt: depl.deployedAt, + value: it.value, + }) + }) + }) + + const entries = [...result.entries()].map(entry => { + const [key, candidate] = entry + return [key, candidate.value] + }) + + return Object.fromEntries(entries) +} diff --git a/web/crux/src/domain/validation.ts b/web/crux/src/domain/validation.ts index ff0efdf5b1..2dc9c7789c 100644 --- a/web/crux/src/domain/validation.ts +++ b/web/crux/src/domain/validation.ts @@ -2,7 +2,7 @@ import { ContainerConfigPortRangeDto } from 'src/app/container/container.dto' import { EnvironmentRule, ImageValidation } from 'src/app/image/image.dto' import { ContainerPort } from 'src/app/node/node.dto' import { CruxBadRequestException } from 'src/exception/crux-exception' -import { UID_MAX } from 'src/shared/const' +import { UID_MAX, UID_MIN } from 'src/shared/const' import * as yup from 'yup' import { CONTAINER_DEPLOYMENT_STRATEGY_VALUES, @@ -66,127 +66,62 @@ const portNumberBaseRule = yup const portNumberOptionalRule = portNumberBaseRule.nullable() const portNumberRule = portNumberBaseRule.required() -const routingRule = yup - .object() - .shape({ - domain: yup.string().nullable(), - path: yup.string().nullable(), - stripPath: yup.bool().nullable(), - uploadLimit: yup.string().nullable(), - }) - .default({}) - .nullable() - -const exposeRule = yup - .mixed() - .oneOf([...CONTAINER_EXPOSE_STRATEGY_VALUES]) - .default('none') - .required() - -const instanceExposeRule = yup - .mixed() - .oneOf([...CONTAINER_EXPOSE_STRATEGY_VALUES, null]) - .nullable() - -const restartPolicyRule = yup - .mixed() - .oneOf([...CONTAINER_RESTART_POLICY_TYPE_VALUES]) - .default('no') - -const instanceRestartPolicyRule = yup - .mixed() - .oneOf([...CONTAINER_RESTART_POLICY_TYPE_VALUES, null]) - .nullable() - -const networkModeRule = yup - .mixed() - .oneOf([...CONTAINER_NETWORK_MODE_VALUES]) - .default('bridge') - .required() - -const instanceNetworkModeRule = yup - .mixed() - .oneOf([...CONTAINER_NETWORK_MODE_VALUES, null]) - .nullable() +const routingRule = yup.object().shape({ + domain: yup.string().nullable(), + path: yup.string().nullable(), + stripPath: yup.bool().nullable(), + uploadLimit: yup.string().nullable(), +}) +const exposeRule = yup.mixed().oneOf([...CONTAINER_EXPOSE_STRATEGY_VALUES]) +const restartPolicyRule = yup.mixed().oneOf([...CONTAINER_RESTART_POLICY_TYPE_VALUES]) +const networkModeRule = yup.mixed().oneOf([...CONTAINER_NETWORK_MODE_VALUES]) const deploymentStrategyRule = yup .mixed() .oneOf([...CONTAINER_DEPLOYMENT_STRATEGY_VALUES]) - .required() -const instanceDeploymentStrategyRule = yup - .mixed() - .oneOf([...CONTAINER_DEPLOYMENT_STRATEGY_VALUES, null]) - .nullable() - -const logDriverRule = yup - .mixed() - .oneOf([...CONTAINER_LOG_DRIVER_VALUES]) - .default('nodeDefault') - -const volumeTypeRule = yup - .mixed() - .oneOf([...CONTAINER_VOLUME_TYPE_VALUES]) - .default('rwo') - -const configContainerRule = yup - .object() - .shape({ - image: yup.string().required(), - volume: yup.string().required(), - path: yup.string().required(), - keepFiles: yup.boolean().default(false).required(), - }) - .default({}) - .nullable() - .optional() - -const healthCheckConfigRule = yup - .object() - .shape({ - port: portNumberRule.nullable().optional(), - livenessProbe: yup.string().nullable().optional(), - readinessProbe: yup.string().nullable().optional(), - startupProbe: yup.string().nullable().optional(), - }) - .default({}) - .optional() - .nullable() - -const resourceConfigRule = yup - .object() - .shape({ - limits: yup - .object() - .shape({ - cpu: yup.string().nullable(), - memory: yup.string().nullable(), - }) - .nullable() - .optional(), - memory: yup - .object() - .shape({ - cpu: yup.string().nullable(), - memory: yup.string().nullable(), - }) - .nullable() - .optional(), - livenessProbe: yup.string().nullable(), - }) - .default({}) - .nullable() - .optional() - -const storageRule = yup - .object() - .shape({ - bucket: yup.string().required(), - path: yup.string().required(), - }) - .default({}) - .nullable() - .optional() +const logDriverRule = yup.mixed().oneOf([...CONTAINER_LOG_DRIVER_VALUES]) + +const volumeTypeRule = yup.mixed().oneOf([...CONTAINER_VOLUME_TYPE_VALUES]) + +const configContainerRule = yup.object().shape({ + image: yup.string().required(), + volume: yup.string().required(), + path: yup.string().required(), + keepFiles: yup.boolean().default(false).required(), +}) + +const healthCheckConfigRule = yup.object().shape({ + port: portNumberRule.nullable().optional(), + livenessProbe: yup.string().nullable().optional(), + readinessProbe: yup.string().nullable().optional(), + startupProbe: yup.string().nullable().optional(), +}) + +const resourceConfigRule = yup.object().shape({ + limits: yup + .object() + .shape({ + cpu: yup.string().nullable(), + memory: yup.string().nullable(), + }) + .nullable() + .optional(), + memory: yup + .object() + .shape({ + cpu: yup.string().nullable(), + memory: yup.string().nullable(), + }) + .nullable() + .optional(), + livenessProbe: yup.string().nullable(), +}) + +const storageRule = yup.object().shape({ + bucket: yup.string().required(), + path: yup.string().required(), +}) const createOverlapTest = ( schema: yup.NumberSchema, @@ -206,16 +141,12 @@ const createOverlapTest = ( // note: here yup passes reference as array const portConfigRule = yup.mixed().when('portRanges', ([portRanges]) => { if (!portRanges?.length) { - return yup - .array( - yup.object().shape({ - internal: portNumberRule, - external: portNumberOptionalRule, - }), - ) - .default([]) - .nullable() - .optional() + return yup.array( + yup.object().shape({ + internal: portNumberRule, + external: portNumberOptionalRule, + }), + ).nullable().optional() } return yup @@ -224,50 +155,39 @@ const portConfigRule = yup.mixed().when('portRanges', ([portRanges]) => { internal: createOverlapTest(portNumberRule, portRanges, 'internal'), external: createOverlapTest(portNumberOptionalRule, portRanges, 'external'), }), - ) - .default([]) - .nullable() - .optional() + ).nullable().optional() }) -const portRangeConfigRule = yup - .array( - yup.object().shape({ - internal: yup - .object() - .shape({ - from: portNumberRule, - to: portNumberRule, - }) - .default({}) - .required(), - external: yup - .object() - .shape({ - from: portNumberRule, - to: portNumberRule, - }) - .default({}) - .required(), - }), - ) - .default([]) - .nullable() - .optional() +const portRangeConfigRule = yup.array( + yup.object().shape({ + internal: yup + .object() + .shape({ + from: portNumberRule, + to: portNumberRule, + }) + .default({}) + .required(), + external: yup + .object() + .shape({ + from: portNumberRule, + to: portNumberRule, + }) + .default({}) + .required(), + }), +) -const volumeConfigRule = yup - .array( - yup.object().shape({ - name: yup.string().required(), - path: yup.string().required(), - size: yup.string().nullable(), - class: yup.string().nullable(), - type: volumeTypeRule, - }), - ) - .default([]) - .nullable() - .optional() +const volumeConfigRule = yup.array( + yup.object().shape({ + name: yup.string().required(), + path: yup.string().required(), + size: yup.string().nullable(), + class: yup.string().nullable(), + type: volumeTypeRule, + }), +) const initContainerVolumeLinkRule = yup.array( yup.object().shape({ @@ -276,42 +196,37 @@ const initContainerVolumeLinkRule = yup.array( }), ) -const initContainerRule = yup +const initContainerRule = yup.array( + yup.object().shape({ + name: yup.string().required().matches(/^\S+$/g), + image: yup.string().required(), + command: shellCommandSchema.default([]).nullable(), + args: shellCommandSchema.default([]).nullable(), + environment: uniqueKeyValuesSchema.default([]).nullable(), + useParentConfig: yup.boolean().default(false).required(), + volumes: initContainerVolumeLinkRule.default([]).nullable(), + }), +) + +const logConfigRule = yup.object().shape({ + driver: logDriverRule, + options: uniqueKeyValuesSchema.default([]).nullable(), +}) + +const markerRule = yup.object().shape({ + deployment: uniqueKeyValuesSchema.default([]).nullable(), + service: uniqueKeyValuesSchema.default([]).nullable(), + ingress: uniqueKeyValuesSchema.default([]).nullable(), +}) + +const uniqueSecretKeySchema = yup .array( yup.object().shape({ - name: yup.string().required().matches(/^\S+$/g), - image: yup.string().required(), - command: shellCommandSchema.default([]).nullable(), - args: shellCommandSchema.default([]).nullable(), - environment: uniqueKeyValuesSchema.default([]).nullable(), - useParentConfig: yup.boolean().default(false).required(), - volumes: initContainerVolumeLinkRule.default([]).nullable(), + key: yup.string().required().ensure().matches(/^\S+$/g), // all characters are non-whitespaces }), ) - .default([]) - .nullable() - .optional() - -const logConfigRule = yup - .object() - .shape({ - driver: logDriverRule, - options: uniqueKeyValuesSchema.default([]).nullable(), - }) - .default({}) - .nullable() - .optional() - -const markerRule = yup - .object() - .shape({ - deployment: uniqueKeyValuesSchema.default([]).nullable(), - service: uniqueKeyValuesSchema.default([]).nullable(), - ingress: uniqueKeyValuesSchema.default([]).nullable(), - }) - .default({}) - .nullable() - .optional() + .ensure() + .test('keysAreUnique', 'Keys must be unique', arr => new Set(arr.map(it => it.key)).size === arr.length) const uniqueSecretKeyValuesSchema = yup .array( @@ -341,112 +256,102 @@ const createMetricsPortRule = (ports: ContainerPort[]) => { const metricsRule = yup.mixed().when(['ports'], ([ports]) => { const portRule = createMetricsPortRule(ports) - return yup - .object() - .when({ - is: (it: Metrics) => it?.enabled, - then: schema => - schema.shape({ - enabled: yup.boolean(), - path: yup.string().nullable(), - port: portRule, - }), - }) - .nullable() - .optional() - .default(null) + return yup.object().when({ + is: (it: Metrics) => it?.enabled, + then: schema => + schema.shape({ + enabled: yup.boolean(), + path: yup.string().nullable(), + port: portRule, + }), + }).nullable().optional() }) -const expectedContainerStateRule = yup - .object() - .shape({ - state: yup.string().default(null).nullable().oneOf(CONTAINER_STATE_VALUES), - timeout: yup.number().default(null).nullable().min(0), - exitCode: yup.number().default(0).nullable().min(-127).max(128), - }) - .default({}) - .nullable() - .optional() - -export const containerConfigSchema = yup.object().shape({ - name: yup.string().required().matches(/^\S+$/g), - environment: uniqueKeyValuesSchema.default([]).nullable(), - secrets: uniqueSecretKeyValuesSchema.default([]).nullable(), - routing: routingRule, - expose: exposeRule, - user: yup.number().default(null).min(-1).max(UID_MAX).nullable(), - workingDirectory: yup.string().nullable().optional().matches(/^\S+$/g), - tty: yup.boolean().default(false).required(), - configContainer: configContainerRule, - ports: portConfigRule, - portRanges: portRangeConfigRule, - volumes: volumeConfigRule, - commands: shellCommandSchema.default([]).nullable(), - args: shellCommandSchema.default([]).nullable(), - initContainers: initContainerRule, - capabilities: uniqueKeyValuesSchema.default([]).nullable(), - storageId: yup.string().default(null).nullable(), - storageConfig: storageRule, +const expectedContainerStateRule = yup.object().shape({ + state: yup.string().default(null).nullable().oneOf(CONTAINER_STATE_VALUES), + timeout: yup.number().default(null).nullable().min(0), + exitCode: yup.number().default(0).nullable().min(-127).max(128), +}) + +const containerConfigSchema = yup.object().shape({ + name: yup.string().optional().nullable().matches(/^\S+$/g), + environment: uniqueKeyValuesSchema.optional().nullable(), + secrets: uniqueSecretKeySchema.optional().nullable(), + routing: routingRule.optional().nullable(), + expose: exposeRule.optional().nullable(), + user: yup.number().default(null).min(UID_MIN).max(UID_MAX).optional().nullable(), + workingDirectory: yup.string().optional().nullable().matches(/^\S+$/g), + tty: yup.boolean().optional().nullable(), + configContainer: configContainerRule.optional().nullable(), + ports: portConfigRule.optional().nullable(), + portRanges: portRangeConfigRule.optional().nullable(), + volumes: volumeConfigRule.optional().nullable(), + commands: shellCommandSchema.optional().nullable(), + args: shellCommandSchema.optional().nullable(), + initContainers: initContainerRule.optional().nullable(), + capabilities: uniqueKeyValuesSchema.optional().nullable(), + storageId: yup.string().optional().nullable(), + storageConfig: storageRule.optional().nullable(), // dagent: - logConfig: logConfigRule, - restartPolicy: restartPolicyRule, - networkMode: networkModeRule, - networks: uniqueKeysOnlySchema.default([]).nullable(), - dockerLabels: uniqueKeyValuesSchema.default([]).nullable(), - expectedState: expectedContainerStateRule, + logConfig: logConfigRule.optional().nullable(), + restartPolicy: restartPolicyRule.optional().nullable(), + networkMode: networkModeRule.optional().nullable(), + networks: uniqueKeysOnlySchema.optional().nullable(), + dockerLabels: uniqueKeyValuesSchema.optional().nullable(), + expectedState: expectedContainerStateRule.optional().nullable(), // crane - deploymentStrategy: deploymentStrategyRule, - customHeaders: uniqueKeysOnlySchema.default([]).nullable(), - proxyHeaders: yup.boolean().default(false).required(), - useLoadBalancer: yup.boolean().default(false).required(), - extraLBAnnotations: uniqueKeyValuesSchema.default([]).nullable(), - healthCheckConfig: healthCheckConfigRule, - resourceConfig: resourceConfigRule, - annotations: markerRule, - labels: markerRule, - metrics: metricsRule, + deploymentStrategy: deploymentStrategyRule.optional().nullable(), + customHeaders: uniqueKeysOnlySchema.optional().nullable(), + proxyHeaders: yup.boolean().optional().nullable(), + useLoadBalancer: yup.boolean().optional().nullable(), + extraLBAnnotations: uniqueKeyValuesSchema.optional().nullable(), + healthCheckConfig: healthCheckConfigRule.optional().nullable(), + resourceConfig: resourceConfigRule.optional().nullable(), + annotations: markerRule.optional().nullable(), + labels: markerRule.optional().nullable(), + metrics: metricsRule.optional().nullable(), }) -export const instanceContainerConfigSchema = yup.object().shape({ - name: yup.string().nullable(), - environment: uniqueKeyValuesSchema.default([]).nullable(), - secrets: uniqueKeyValuesSchema.default([]).nullable(), - routing: routingRule.nullable(), - expose: instanceExposeRule, - user: yup.number().default(null).min(-1).max(UID_MAX).nullable(), - tty: yup.boolean().default(false).nullable(), - configContainer: configContainerRule.nullable(), - ports: portConfigRule.nullable(), - portRanges: portRangeConfigRule.nullable(), - volumes: volumeConfigRule.nullable(), - commands: shellCommandSchema.default([]).nullable(), - args: shellCommandSchema.default([]).nullable(), - initContainers: initContainerRule.nullable(), - capabilities: uniqueKeyValuesSchema.default([]).nullable(), - storageId: yup.string().default(null).nullable(), - storageConfig: storageRule, +export const concreteContainerConfigSchema = yup.object().shape({ + name: yup.string().optional().nullable(), + environment: uniqueKeyValuesSchema.optional().nullable(), + secrets: uniqueSecretKeyValuesSchema.optional().nullable(), + routing: routingRule.optional().nullable(), + expose: exposeRule.optional().nullable(), + user: yup.number().optional().nullable().min(-1).max(UID_MAX), + tty: yup.boolean().optional().nullable(), + configContainer: configContainerRule.optional().nullable(), + ports: portConfigRule.optional().nullable(), + portRanges: portRangeConfigRule.optional().nullable(), + volumes: volumeConfigRule.optional().nullable(), + commands: shellCommandSchema.optional().nullable(), + args: shellCommandSchema.optional().nullable(), + initContainers: initContainerRule.optional().nullable(), + capabilities: uniqueKeyValuesSchema.optional().nullable(), + storageId: yup.string().optional().nullable(), + storageConfig: storageRule.optional().nullable(), // dagent: - logConfig: logConfigRule.nullable(), - restartPolicy: instanceRestartPolicyRule, - networkMode: instanceNetworkModeRule, - networks: uniqueKeysOnlySchema.default([]).nullable(), - dockerLabels: uniqueKeyValuesSchema.default([]).nullable(), - expectedState: expectedContainerStateRule, + logConfig: logConfigRule.optional().nullable(), + restartPolicy: restartPolicyRule.optional().nullable(), + networkMode: networkModeRule.optional().nullable(), + networks: uniqueKeysOnlySchema.optional().nullable(), + dockerLabels: uniqueKeyValuesSchema.optional().nullable(), + expectedState: expectedContainerStateRule.optional().nullable(), // crane - deploymentStrategy: instanceDeploymentStrategyRule, - customHeaders: uniqueKeysOnlySchema.default([]).nullable(), - proxyHeaders: yup.boolean().default(false).nullable(), - useLoadBalancer: yup.boolean().default(false).nullable(), - extraLBAnnotations: uniqueKeyValuesSchema.default([]).nullable(), - healthCheckConfig: healthCheckConfigRule.nullable(), - resourceConfig: resourceConfigRule.nullable(), - annotations: markerRule.nullable(), - labels: markerRule.nullable(), - metrics: metricsRule, + deploymentStrategy: deploymentStrategyRule.optional().nullable(), + customHeaders: uniqueKeysOnlySchema.optional().nullable(), + proxyHeaders: yup.boolean().optional().nullable(), + useLoadBalancer: yup.boolean().optional().nullable(), + extraLBAnnotations: uniqueKeyValuesSchema.optional().nullable(), + healthCheckConfig: healthCheckConfigRule.optional().nullable(), + resourceConfig: resourceConfigRule.optional().nullable(), + annotations: markerRule.optional().nullable(), + labels: markerRule.optional().nullable(), + metrics: metricsRule.optional().nullable(), }) const validateEnvironmentRule = (rule: EnvironmentRule, index: number, env: UniqueKeyValue) => { @@ -519,12 +424,19 @@ const testEnvironment = (validation: ImageValidation, arr: UniqueKeyValue[]) => export const createStartDeploymentSchema = (instanceValidation: Record) => yup.object({ - environment: uniqueKeyValuesSchema, + config: containerConfigSchema, + configBundles: yup.array( + yup.object({ + configBundle: yup.object({ + config: containerConfigSchema, + }), + }), + ), instances: yup .array( yup.object({ id: yup.string(), - config: instanceContainerConfigSchema.nullable(), + config: concreteContainerConfigSchema, }), ) .test( @@ -630,6 +542,17 @@ export const templateSchema = yup.object({ .required(), }) +export const nullifyUndefinedProperties = (candidate: object) => { + if (candidate) { + Object.entries(candidate).forEach(entry => { + const [key, value] = entry + if (typeof value === 'undefined') { + candidate[key] = null + } + }) + } +} + export const yupValidate = (schema: yup.AnySchema, candidate: any) => { try { schema.validateSync(candidate) diff --git a/web/crux/src/domain/version.ts b/web/crux/src/domain/version.ts index d02d6a4430..cb23c45fb7 100644 --- a/web/crux/src/domain/version.ts +++ b/web/crux/src/domain/version.ts @@ -1,6 +1,7 @@ import { Deployment, DeploymentStatusEnum, ProjectTypeEnum, Version, VersionTypeEnum } from '@prisma/client' import { CruxPreconditionFailedException } from 'src/exception/crux-exception' -import { checkDeploymentMutability } from './deployment' +import { DeploymentWithNode, deploymentIsMutable } from './deployment' +import { ImageDetails } from './image' export type VersionWithName = Pick @@ -8,6 +9,18 @@ export type VersionWithDeployments = Version & { deployments: Deployment[] } +export type VersionWithChildren = Version & { + children: { versionId: string }[] +} + +export type VersionDetails = VersionWithChildren & { + project: { + type: ProjectTypeEnum + } + images: ImageDetails[] + deployments: DeploymentWithNode[] +} + export type VersionIncreasabilityCheckDao = { type: VersionTypeEnum children: { versionId: string }[] @@ -25,7 +38,7 @@ export type VersionDeletabilityCheckDao = VersionMutabilityCheckDao & { } export const versionHasImmutableDeployments = (version: VersionMutabilityCheckDao): boolean => - version.deployments.filter(it => !checkDeploymentMutability(it.status, version.type)).length > 0 + version.deployments.filter(it => !deploymentIsMutable(it.status, version.type)).length > 0 // - 'rolling' versions are not increasable // - an 'incremental' version is increasable, when it does not have any child yet @@ -71,6 +84,4 @@ export const checkVersionMutability = (version: VersionMutabilityCheckDao) => { value: version.id, }) } - - return false } diff --git a/web/crux/src/grpc/protobuf/proto/agent.ts b/web/crux/src/grpc/protobuf/proto/agent.ts index d41320e393..b3ce694c6a 100644 --- a/web/crux/src/grpc/protobuf/proto/agent.ts +++ b/web/crux/src/grpc/protobuf/proto/agent.ts @@ -9,6 +9,7 @@ import { ContainerInspectResponse, ContainerLogListResponse, ContainerLogMessage, + ContainerOrPrefix, ContainerState, ContainerStateListMessage, DeleteContainersRequest, @@ -108,7 +109,7 @@ export interface AgentInfo { } export interface AgentCommand { - deploy?: VersionDeployRequest | undefined + deploy?: DeployRequest | undefined containerState?: ContainerStateRequest | undefined containerDelete?: ContainerDeleteRequest | undefined deployLegacy?: DeployRequestLegacy | undefined @@ -142,38 +143,25 @@ export interface DeployResponse { started: boolean } -export interface VersionDeployRequest { +export interface DeployRequest { id: string versionName: string releaseNotes: string - requests: DeployRequest[] -} - -/** Request for a keys of existing secrets in a prefix, eg. namespace */ -export interface ListSecretsRequest { - container: ContainerIdentifier | undefined -} - -/** Deploys a single container */ -export interface InstanceConfig { - /** - * prefix mapped into host folder structure, - * used as namespace id - */ prefix: string - /** mount path of instance (docker only) */ - mountPath?: string | undefined - /** environment variable map */ - environment: { [key: string]: string } - /** registry repo prefix */ - repositoryPrefix?: string | undefined + secrets: { [key: string]: string } + requests: DeployWorkloadRequest[] } -export interface InstanceConfig_EnvironmentEntry { +export interface DeployRequest_SecretsEntry { key: string value: string } +/** Request for a keys of existing secrets in a prefix, eg. namespace */ +export interface ListSecretsRequest { + target: ContainerOrPrefix | undefined +} + export interface RegistryAuth { name: string url: string @@ -338,17 +326,13 @@ export interface CommonContainerConfig_SecretsEntry { value: string } -export interface DeployRequest { +export interface DeployWorkloadRequest { id: string containerName: string - /** InstanceConfig is set for multiple containers */ - instanceConfig: InstanceConfig | undefined /** ContainerConfigs */ common?: CommonContainerConfig | undefined dagent?: DagentContainerConfig | undefined crane?: CraneContainerConfig | undefined - /** Runtime info and requirements of a container */ - runtimeConfig?: string | undefined registry?: string | undefined imageName: string tag: string @@ -435,7 +419,7 @@ function createBaseAgentCommand(): AgentCommand { export const AgentCommand = { fromJSON(object: any): AgentCommand { return { - deploy: isSet(object.deploy) ? VersionDeployRequest.fromJSON(object.deploy) : undefined, + deploy: isSet(object.deploy) ? DeployRequest.fromJSON(object.deploy) : undefined, containerState: isSet(object.containerState) ? ContainerStateRequest.fromJSON(object.containerState) : undefined, containerDelete: isSet(object.containerDelete) ? ContainerDeleteRequest.fromJSON(object.containerDelete) @@ -460,8 +444,7 @@ export const AgentCommand = { toJSON(message: AgentCommand): unknown { const obj: any = {} - message.deploy !== undefined && - (obj.deploy = message.deploy ? VersionDeployRequest.toJSON(message.deploy) : undefined) + message.deploy !== undefined && (obj.deploy = message.deploy ? DeployRequest.toJSON(message.deploy) : undefined) message.containerState !== undefined && (obj.containerState = message.containerState ? ContainerStateRequest.toJSON(message.containerState) : undefined) message.containerDelete !== undefined && @@ -560,27 +543,43 @@ export const DeployResponse = { }, } -function createBaseVersionDeployRequest(): VersionDeployRequest { - return { id: '', versionName: '', releaseNotes: '', requests: [] } +function createBaseDeployRequest(): DeployRequest { + return { id: '', versionName: '', releaseNotes: '', prefix: '', secrets: {}, requests: [] } } -export const VersionDeployRequest = { - fromJSON(object: any): VersionDeployRequest { +export const DeployRequest = { + fromJSON(object: any): DeployRequest { return { id: isSet(object.id) ? String(object.id) : '', versionName: isSet(object.versionName) ? String(object.versionName) : '', releaseNotes: isSet(object.releaseNotes) ? String(object.releaseNotes) : '', - requests: Array.isArray(object?.requests) ? object.requests.map((e: any) => DeployRequest.fromJSON(e)) : [], + prefix: isSet(object.prefix) ? String(object.prefix) : '', + secrets: isObject(object.secrets) + ? Object.entries(object.secrets).reduce<{ [key: string]: string }>((acc, [key, value]) => { + acc[key] = String(value) + return acc + }, {}) + : {}, + requests: Array.isArray(object?.requests) + ? object.requests.map((e: any) => DeployWorkloadRequest.fromJSON(e)) + : [], } }, - toJSON(message: VersionDeployRequest): unknown { + toJSON(message: DeployRequest): unknown { const obj: any = {} message.id !== undefined && (obj.id = message.id) message.versionName !== undefined && (obj.versionName = message.versionName) message.releaseNotes !== undefined && (obj.releaseNotes = message.releaseNotes) + message.prefix !== undefined && (obj.prefix = message.prefix) + obj.secrets = {} + if (message.secrets) { + Object.entries(message.secrets).forEach(([k, v]) => { + obj.secrets[k] = v + }) + } if (message.requests) { - obj.requests = message.requests.map(e => (e ? DeployRequest.toJSON(e) : undefined)) + obj.requests = message.requests.map(e => (e ? DeployWorkloadRequest.toJSON(e) : undefined)) } else { obj.requests = [] } @@ -588,70 +587,35 @@ export const VersionDeployRequest = { }, } -function createBaseListSecretsRequest(): ListSecretsRequest { - return { container: undefined } -} - -export const ListSecretsRequest = { - fromJSON(object: any): ListSecretsRequest { - return { container: isSet(object.container) ? ContainerIdentifier.fromJSON(object.container) : undefined } - }, - - toJSON(message: ListSecretsRequest): unknown { - const obj: any = {} - message.container !== undefined && - (obj.container = message.container ? ContainerIdentifier.toJSON(message.container) : undefined) - return obj - }, -} - -function createBaseInstanceConfig(): InstanceConfig { - return { prefix: '', environment: {} } +function createBaseDeployRequest_SecretsEntry(): DeployRequest_SecretsEntry { + return { key: '', value: '' } } -export const InstanceConfig = { - fromJSON(object: any): InstanceConfig { - return { - prefix: isSet(object.prefix) ? String(object.prefix) : '', - mountPath: isSet(object.mountPath) ? String(object.mountPath) : undefined, - environment: isObject(object.environment) - ? Object.entries(object.environment).reduce<{ [key: string]: string }>((acc, [key, value]) => { - acc[key] = String(value) - return acc - }, {}) - : {}, - repositoryPrefix: isSet(object.repositoryPrefix) ? String(object.repositoryPrefix) : undefined, - } +export const DeployRequest_SecretsEntry = { + fromJSON(object: any): DeployRequest_SecretsEntry { + return { key: isSet(object.key) ? String(object.key) : '', value: isSet(object.value) ? String(object.value) : '' } }, - toJSON(message: InstanceConfig): unknown { + toJSON(message: DeployRequest_SecretsEntry): unknown { const obj: any = {} - message.prefix !== undefined && (obj.prefix = message.prefix) - message.mountPath !== undefined && (obj.mountPath = message.mountPath) - obj.environment = {} - if (message.environment) { - Object.entries(message.environment).forEach(([k, v]) => { - obj.environment[k] = v - }) - } - message.repositoryPrefix !== undefined && (obj.repositoryPrefix = message.repositoryPrefix) + message.key !== undefined && (obj.key = message.key) + message.value !== undefined && (obj.value = message.value) return obj }, } -function createBaseInstanceConfig_EnvironmentEntry(): InstanceConfig_EnvironmentEntry { - return { key: '', value: '' } +function createBaseListSecretsRequest(): ListSecretsRequest { + return { target: undefined } } -export const InstanceConfig_EnvironmentEntry = { - fromJSON(object: any): InstanceConfig_EnvironmentEntry { - return { key: isSet(object.key) ? String(object.key) : '', value: isSet(object.value) ? String(object.value) : '' } +export const ListSecretsRequest = { + fromJSON(object: any): ListSecretsRequest { + return { target: isSet(object.target) ? ContainerOrPrefix.fromJSON(object.target) : undefined } }, - toJSON(message: InstanceConfig_EnvironmentEntry): unknown { + toJSON(message: ListSecretsRequest): unknown { const obj: any = {} - message.key !== undefined && (obj.key = message.key) - message.value !== undefined && (obj.value = message.value) + message.target !== undefined && (obj.target = message.target ? ContainerOrPrefix.toJSON(message.target) : undefined) return obj }, } @@ -1371,20 +1335,18 @@ export const CommonContainerConfig_SecretsEntry = { }, } -function createBaseDeployRequest(): DeployRequest { - return { id: '', containerName: '', instanceConfig: undefined, imageName: '', tag: '' } +function createBaseDeployWorkloadRequest(): DeployWorkloadRequest { + return { id: '', containerName: '', imageName: '', tag: '' } } -export const DeployRequest = { - fromJSON(object: any): DeployRequest { +export const DeployWorkloadRequest = { + fromJSON(object: any): DeployWorkloadRequest { return { id: isSet(object.id) ? String(object.id) : '', containerName: isSet(object.containerName) ? String(object.containerName) : '', - instanceConfig: isSet(object.instanceConfig) ? InstanceConfig.fromJSON(object.instanceConfig) : undefined, common: isSet(object.common) ? CommonContainerConfig.fromJSON(object.common) : undefined, dagent: isSet(object.dagent) ? DagentContainerConfig.fromJSON(object.dagent) : undefined, crane: isSet(object.crane) ? CraneContainerConfig.fromJSON(object.crane) : undefined, - runtimeConfig: isSet(object.runtimeConfig) ? String(object.runtimeConfig) : undefined, registry: isSet(object.registry) ? String(object.registry) : undefined, imageName: isSet(object.imageName) ? String(object.imageName) : '', tag: isSet(object.tag) ? String(object.tag) : '', @@ -1392,18 +1354,15 @@ export const DeployRequest = { } }, - toJSON(message: DeployRequest): unknown { + toJSON(message: DeployWorkloadRequest): unknown { const obj: any = {} message.id !== undefined && (obj.id = message.id) message.containerName !== undefined && (obj.containerName = message.containerName) - message.instanceConfig !== undefined && - (obj.instanceConfig = message.instanceConfig ? InstanceConfig.toJSON(message.instanceConfig) : undefined) message.common !== undefined && (obj.common = message.common ? CommonContainerConfig.toJSON(message.common) : undefined) message.dagent !== undefined && (obj.dagent = message.dagent ? DagentContainerConfig.toJSON(message.dagent) : undefined) message.crane !== undefined && (obj.crane = message.crane ? CraneContainerConfig.toJSON(message.crane) : undefined) - message.runtimeConfig !== undefined && (obj.runtimeConfig = message.runtimeConfig) message.registry !== undefined && (obj.registry = message.registry) message.imageName !== undefined && (obj.imageName = message.imageName) message.tag !== undefined && (obj.tag = message.tag) diff --git a/web/crux/src/grpc/protobuf/proto/common.ts b/web/crux/src/grpc/protobuf/proto/common.ts index 3638025543..14e47a0410 100644 --- a/web/crux/src/grpc/protobuf/proto/common.ts +++ b/web/crux/src/grpc/protobuf/proto/common.ts @@ -668,11 +668,14 @@ export interface KeyValue { value: string } +export interface ContainerOrPrefix { + container?: ContainerIdentifier | undefined + prefix?: string | undefined +} + export interface ListSecretsResponse { - prefix: string - name: string + target: ContainerOrPrefix | undefined publicKey: string - hasKeys: boolean keys: string[] } @@ -692,8 +695,7 @@ export interface ContainerCommandRequest { } export interface DeleteContainersRequest { - container?: ContainerIdentifier | undefined - prefix?: string | undefined + target: ContainerOrPrefix | undefined } export const COMMON_PACKAGE_NAME = 'common' @@ -1101,27 +1103,44 @@ export const KeyValue = { }, } +function createBaseContainerOrPrefix(): ContainerOrPrefix { + return {} +} + +export const ContainerOrPrefix = { + fromJSON(object: any): ContainerOrPrefix { + return { + container: isSet(object.container) ? ContainerIdentifier.fromJSON(object.container) : undefined, + prefix: isSet(object.prefix) ? String(object.prefix) : undefined, + } + }, + + toJSON(message: ContainerOrPrefix): unknown { + const obj: any = {} + message.container !== undefined && + (obj.container = message.container ? ContainerIdentifier.toJSON(message.container) : undefined) + message.prefix !== undefined && (obj.prefix = message.prefix) + return obj + }, +} + function createBaseListSecretsResponse(): ListSecretsResponse { - return { prefix: '', name: '', publicKey: '', hasKeys: false, keys: [] } + return { target: undefined, publicKey: '', keys: [] } } export const ListSecretsResponse = { fromJSON(object: any): ListSecretsResponse { return { - prefix: isSet(object.prefix) ? String(object.prefix) : '', - name: isSet(object.name) ? String(object.name) : '', + target: isSet(object.target) ? ContainerOrPrefix.fromJSON(object.target) : undefined, publicKey: isSet(object.publicKey) ? String(object.publicKey) : '', - hasKeys: isSet(object.hasKeys) ? Boolean(object.hasKeys) : false, keys: Array.isArray(object?.keys) ? object.keys.map((e: any) => String(e)) : [], } }, toJSON(message: ListSecretsResponse): unknown { const obj: any = {} - message.prefix !== undefined && (obj.prefix = message.prefix) - message.name !== undefined && (obj.name = message.name) + message.target !== undefined && (obj.target = message.target ? ContainerOrPrefix.toJSON(message.target) : undefined) message.publicKey !== undefined && (obj.publicKey = message.publicKey) - message.hasKeys !== undefined && (obj.hasKeys = message.hasKeys) if (message.keys) { obj.keys = message.keys.map(e => e) } else { @@ -1190,22 +1209,17 @@ export const ContainerCommandRequest = { } function createBaseDeleteContainersRequest(): DeleteContainersRequest { - return {} + return { target: undefined } } export const DeleteContainersRequest = { fromJSON(object: any): DeleteContainersRequest { - return { - container: isSet(object.container) ? ContainerIdentifier.fromJSON(object.container) : undefined, - prefix: isSet(object.prefix) ? String(object.prefix) : undefined, - } + return { target: isSet(object.target) ? ContainerOrPrefix.fromJSON(object.target) : undefined } }, toJSON(message: DeleteContainersRequest): unknown { const obj: any = {} - message.container !== undefined && - (obj.container = message.container ? ContainerIdentifier.toJSON(message.container) : undefined) - message.prefix !== undefined && (obj.prefix = message.prefix) + message.target !== undefined && (obj.target = message.target ? ContainerOrPrefix.toJSON(message.target) : undefined) return obj }, } diff --git a/web/crux/src/shared/const.ts b/web/crux/src/shared/const.ts index 6c3f793729..10f7a1e68b 100644 --- a/web/crux/src/shared/const.ts +++ b/web/crux/src/shared/const.ts @@ -33,6 +33,7 @@ export const API_CREATED_LOCATION_HEADERS = { }, } +export const UID_MIN = -1 export const UID_MAX = 2147483647 export const KRATOS_LIST_PAGE_SIZE = 128 From 76bc1de608541dc67e985ed7159fb3e1bbdaccf1 Mon Sep 17 00:00:00 2001 From: Mate Vago Date: Mon, 25 Nov 2024 15:21:39 +0100 Subject: [PATCH 03/32] fix: build --- golang/pkg/dagent/utils/docker.go | 4 +- golang/pkg/dagent/utils/prefix_file.go | 2 +- .../common-editor.spec.ts | 0 .../common-json.spec.ts | 0 .../container-config-filters.spec.ts} | 0 .../docker-editor.spec.ts | 0 .../docker-json.spec.ts | 0 .../image-config-view-state.spec.ts | 0 .../kubernetes-editor.spec.ts | 0 .../kubernetes-json.spec.ts | 0 web/crux-ui/i18n.json | 2 - web/crux-ui/playwright.config.ts | 4 +- .../config-bundles/config-bundle-card.tsx | 33 +++++++-------- .../common-config-section.tsx | 4 +- .../extendable-item-list.tsx | 4 +- .../deployments/deployment-view-list.tsx | 2 +- .../projects/versions/version-view-list.tsx | 2 + .../shared/secret-key-value-input.tsx | 1 - web/crux-ui/src/models/container.ts | 13 ------ .../src/pages/[teamSlug]/config-bundles.tsx | 1 - .../config-bundles/[configBundleId].tsx | 2 +- .../src/app/container/container.mapper.ts | 2 +- .../src/app/container/container.module.ts | 2 +- web/crux/src/app/deploy/deploy.mapper.spec.ts | 2 + web/crux/src/app/node/node.service.spec.ts | 12 ++++-- web/crux/src/domain/agent-callback.ts | 1 - web/crux/src/domain/agent.spec.ts | 37 +++++++++++------ web/crux/src/domain/container-merge.spec.ts | 10 +++-- web/crux/src/domain/validation.ts | 41 +++++++++++-------- 29 files changed, 94 insertions(+), 87 deletions(-) rename web/crux-ui/e2e/with-login/{image-config => container-config}/common-editor.spec.ts (100%) rename web/crux-ui/e2e/with-login/{image-config => container-config}/common-json.spec.ts (100%) rename web/crux-ui/e2e/with-login/{image-config/image-config-filters.spec.ts => container-config/container-config-filters.spec.ts} (100%) rename web/crux-ui/e2e/with-login/{image-config => container-config}/docker-editor.spec.ts (100%) rename web/crux-ui/e2e/with-login/{image-config => container-config}/docker-json.spec.ts (100%) rename web/crux-ui/e2e/with-login/{image-config => container-config}/image-config-view-state.spec.ts (100%) rename web/crux-ui/e2e/with-login/{image-config => container-config}/kubernetes-editor.spec.ts (100%) rename web/crux-ui/e2e/with-login/{image-config => container-config}/kubernetes-json.spec.ts (100%) diff --git a/golang/pkg/dagent/utils/docker.go b/golang/pkg/dagent/utils/docker.go index a43220ef0e..f1bd2859cb 100644 --- a/golang/pkg/dagent/utils/docker.go +++ b/golang/pkg/dagent/utils/docker.go @@ -323,7 +323,7 @@ func DeploySharedSecrets(ctx context.Context, ) error { cfg := grpc.GetConfigFromContext(ctx).(*config.Configuration) - pf := NewSecretsFile(cfg.InternalMountPath, prefix) + pf := NewSecretsPrefixFile(cfg.InternalMountPath, prefix) err := pf.WriteVariables(secrets) if err != nil { return fmt.Errorf("could not write secrets, aborting: %w", err) @@ -728,7 +728,7 @@ func SecretList(ctx context.Context, prefix string, name string) ([]string, erro if name == "" { cfg := grpc.GetConfigFromContext(ctx).(*config.Configuration) - pf := NewSecretsFile(cfg.InternalMountPath, prefix) + pf := NewSecretsPrefixFile(cfg.InternalMountPath, prefix) secrets, err := pf.ReadVariables() if err != nil { return []string{}, fmt.Errorf("could not read secrets, aborting: %w", err) diff --git a/golang/pkg/dagent/utils/prefix_file.go b/golang/pkg/dagent/utils/prefix_file.go index f23aa11ef0..1f5a1587fc 100644 --- a/golang/pkg/dagent/utils/prefix_file.go +++ b/golang/pkg/dagent/utils/prefix_file.go @@ -38,7 +38,7 @@ func NewSharedEnvPrefixFile(dataRoot, prefix string) prefixFile { } } -func NewSecretsFile(dataRoot, prefix string) prefixFile { +func NewSecretsPrefixFile(dataRoot, prefix string) prefixFile { return prefixFile{ DataRoot: dataRoot, Prefix: prefix, diff --git a/web/crux-ui/e2e/with-login/image-config/common-editor.spec.ts b/web/crux-ui/e2e/with-login/container-config/common-editor.spec.ts similarity index 100% rename from web/crux-ui/e2e/with-login/image-config/common-editor.spec.ts rename to web/crux-ui/e2e/with-login/container-config/common-editor.spec.ts diff --git a/web/crux-ui/e2e/with-login/image-config/common-json.spec.ts b/web/crux-ui/e2e/with-login/container-config/common-json.spec.ts similarity index 100% rename from web/crux-ui/e2e/with-login/image-config/common-json.spec.ts rename to web/crux-ui/e2e/with-login/container-config/common-json.spec.ts diff --git a/web/crux-ui/e2e/with-login/image-config/image-config-filters.spec.ts b/web/crux-ui/e2e/with-login/container-config/container-config-filters.spec.ts similarity index 100% rename from web/crux-ui/e2e/with-login/image-config/image-config-filters.spec.ts rename to web/crux-ui/e2e/with-login/container-config/container-config-filters.spec.ts diff --git a/web/crux-ui/e2e/with-login/image-config/docker-editor.spec.ts b/web/crux-ui/e2e/with-login/container-config/docker-editor.spec.ts similarity index 100% rename from web/crux-ui/e2e/with-login/image-config/docker-editor.spec.ts rename to web/crux-ui/e2e/with-login/container-config/docker-editor.spec.ts diff --git a/web/crux-ui/e2e/with-login/image-config/docker-json.spec.ts b/web/crux-ui/e2e/with-login/container-config/docker-json.spec.ts similarity index 100% rename from web/crux-ui/e2e/with-login/image-config/docker-json.spec.ts rename to web/crux-ui/e2e/with-login/container-config/docker-json.spec.ts diff --git a/web/crux-ui/e2e/with-login/image-config/image-config-view-state.spec.ts b/web/crux-ui/e2e/with-login/container-config/image-config-view-state.spec.ts similarity index 100% rename from web/crux-ui/e2e/with-login/image-config/image-config-view-state.spec.ts rename to web/crux-ui/e2e/with-login/container-config/image-config-view-state.spec.ts diff --git a/web/crux-ui/e2e/with-login/image-config/kubernetes-editor.spec.ts b/web/crux-ui/e2e/with-login/container-config/kubernetes-editor.spec.ts similarity index 100% rename from web/crux-ui/e2e/with-login/image-config/kubernetes-editor.spec.ts rename to web/crux-ui/e2e/with-login/container-config/kubernetes-editor.spec.ts diff --git a/web/crux-ui/e2e/with-login/image-config/kubernetes-json.spec.ts b/web/crux-ui/e2e/with-login/container-config/kubernetes-json.spec.ts similarity index 100% rename from web/crux-ui/e2e/with-login/image-config/kubernetes-json.spec.ts rename to web/crux-ui/e2e/with-login/container-config/kubernetes-json.spec.ts diff --git a/web/crux-ui/i18n.json b/web/crux-ui/i18n.json index 93449db909..6bb23b694e 100644 --- a/web/crux-ui/i18n.json +++ b/web/crux-ui/i18n.json @@ -20,7 +20,6 @@ "/[teamSlug]/projects": ["projects"], "/[teamSlug]/projects/[projectId]": ["projects", "versions", "images", "deployments"], "/[teamSlug]/projects/[projectId]/versions/[versionId]": ["versions", "images", "deployments"], - "/[teamSlug]/projects/[projectId]/versions/[versionId]/images/[imageId]": ["images", "container"], "/[teamSlug]/nodes": ["nodes", "tokens"], "/[teamSlug]/nodes/[nodeId]": ["nodes", "images", "tokens", "deployments"], "/[teamSlug]/nodes/[nodeId]/log": [], @@ -38,7 +37,6 @@ "/[teamSlug]/deployments/[deploymentId]": ["images", "deployments", "nodes", "tokens", "container"], "/[teamSlug]/deployments/[deploymentId]/deploy": ["deployments"], "/[teamSlug]/deployments/[deploymentId]/log": [], - "/[teamSlug]/deployments/[deploymentId]/instances/[instanceId]": ["images", "deployments", "container"], "/status": ["status"], "/templates": ["templates", "projects"], "/composer": ["compose", "versions", "container"], diff --git a/web/crux-ui/playwright.config.ts b/web/crux-ui/playwright.config.ts index 3a26f30d37..136121d0f7 100644 --- a/web/crux-ui/playwright.config.ts +++ b/web/crux-ui/playwright.config.ts @@ -93,8 +93,8 @@ const config: PlaywrightTestConfig = { createProject('template', 'with-login/template.spec.ts'), createProject('project', 'with-login/project.spec.ts'), createProject('version', 'with-login/version.spec.ts'), - createProject('image-config', /with-login\/image-config\/(.*)/, ['registry', 'template', 'version']), - createProject('deployment', /with-login\/deployment(.*)\.spec\.ts/, ['image-config', 'nodes']), + createProject('container-config', /with-login\/container-config\/(.*)/, ['registry', 'template', 'version']), + createProject('deployment', /with-login\/deployment(.*)\.spec\.ts/, ['container-config', 'nodes']), createProject('dagent-deploy', 'with-login/nodes-deploy.spec.ts', ['deployment']), createProject('resource-copy', 'with-login/resource-copy.spec.ts', ['template', 'version', 'deployment', 'nodes']), createProject('dashboard', 'with-login/dashboard.spec.ts'), diff --git a/web/crux-ui/src/components/config-bundles/config-bundle-card.tsx b/web/crux-ui/src/components/config-bundles/config-bundle-card.tsx index 61b567fa42..ef67f8048c 100644 --- a/web/crux-ui/src/components/config-bundles/config-bundle-card.tsx +++ b/web/crux-ui/src/components/config-bundles/config-bundle-card.tsx @@ -12,11 +12,10 @@ import Image from 'next/image' type ConfigBundleCardProps = { className?: string configBundle: ConfigBundle - showConfigIcon?: boolean } const ConfigBundleCard = (props: ConfigBundleCardProps) => { - const { configBundle, className, showConfigIcon } = props + const { configBundle, className } = props const { t } = useTranslation('config-bundles') const routes = useTeamRoutes() @@ -41,22 +40,20 @@ const ConfigBundleCard = (props: ConfigBundleCardProps) => { modalTitle={configBundle.name} /> - {showConfigIcon && ( -
- -
- {t('common:config')} - {t('common:config')} -
-
-
- )} +
+ +
+ {t('common:config')} + {t('common:config')} +
+
+
) } diff --git a/web/crux-ui/src/components/container-configs/common-config-section.tsx b/web/crux-ui/src/components/container-configs/common-config-section.tsx index 3d3e0ea6e3..77bc0c7ae3 100644 --- a/web/crux-ui/src/components/container-configs/common-config-section.tsx +++ b/web/crux-ui/src/components/container-configs/common-config-section.tsx @@ -15,9 +15,7 @@ import { CONTAINER_EXPOSE_STRATEGY_VALUES, CONTAINER_VOLUME_TYPE_VALUES, CommonConfigKey, - ConcreteCommonConfigData, ConcreteContainerConfigData, - ConcreteCraneConfigData, ContainerConfigData, ContainerConfigErrors, ContainerConfigExposeStrategy, @@ -105,7 +103,7 @@ const CommonConfigSection = (props: CommonConfigSectionProps) => { }) const onPortsChanged = (ports: Port[]) => { - let patch: Partial> = { + let patch: ConcreteContainerConfigData = { ports, } diff --git a/web/crux-ui/src/components/container-configs/extendable-item-list.tsx b/web/crux-ui/src/components/container-configs/extendable-item-list.tsx index d39ddb10ca..be707c6e37 100644 --- a/web/crux-ui/src/components/container-configs/extendable-item-list.tsx +++ b/web/crux-ui/src/components/container-configs/extendable-item-list.tsx @@ -122,7 +122,7 @@ const ExtendableItemList = (props: ExtendableItemListProps) = onResetSection, emptyItemFactory, itemClassName, - error, + error: sectionLabelError, } = props const [state, dispatch] = useRepatch>({ @@ -153,7 +153,7 @@ const ExtendableItemList = (props: ExtendableItemListProps) = labelClassName="text-bright font-semibold tracking-wide" disabled={!hasValue || disabled || !onResetSection} onResetSection={onResetSection} - error={error} + error={sectionLabelError} > {label.toUpperCase()} diff --git a/web/crux-ui/src/components/deployments/deployment-view-list.tsx b/web/crux-ui/src/components/deployments/deployment-view-list.tsx index 1efc23fd5c..9bf91fe5b2 100644 --- a/web/crux-ui/src/components/deployments/deployment-view-list.tsx +++ b/web/crux-ui/src/components/deployments/deployment-view-list.tsx @@ -71,7 +71,7 @@ const DeploymentViewList = (props: DeploymentViewListProps) => {
diff --git a/web/crux-ui/src/components/projects/versions/version-view-list.tsx b/web/crux-ui/src/components/projects/versions/version-view-list.tsx index a2a789df3d..d7dd768e9b 100644 --- a/web/crux-ui/src/components/projects/versions/version-view-list.tsx +++ b/web/crux-ui/src/components/projects/versions/version-view-list.tsx @@ -104,6 +104,7 @@ const VersionViewList = (props: VersionViewListProps) => { onClick={() => onOpenTagsDialog(it)} />
+
{ onClick={() => onDelete(it)} />
+ diff --git a/web/crux-ui/src/components/shared/secret-key-value-input.tsx b/web/crux-ui/src/components/shared/secret-key-value-input.tsx index 98995d5f97..d758e0c7d5 100644 --- a/web/crux-ui/src/components/shared/secret-key-value-input.tsx +++ b/web/crux-ui/src/components/shared/secret-key-value-input.tsx @@ -127,7 +127,6 @@ const SecretKeyValueInput = (props: SecretKeyValueInputProps) => { secretKeys.forEach(item => { const repeating = result.find(it => it.key === item.key) - console.log('status', item.key, secrets) result.push({ ...item, encrypted: item.encrypted ?? false, diff --git a/web/crux-ui/src/models/container.ts b/web/crux-ui/src/models/container.ts index f9546ba069..7042ee66f8 100644 --- a/web/crux-ui/src/models/container.ts +++ b/web/crux-ui/src/models/container.ts @@ -295,19 +295,6 @@ export type CraneConfigKey = (typeof CRANE_CONFIG_KEYS)[number] export type DagentConfigKey = (typeof DAGENT_CONFIG_KEYS)[number] export type ContainerConfigKey = (typeof CONTAINER_CONFIG_KEYS)[number] -export type DagentConfigData = Pick -export type CraneConfigData = Pick -export type CommonConfigData = Omit - -export type ConcreteDagentConfigData = Pick -export type ConcreteCraneConfigData = Pick -export type ConcreteCommonConfigData = Omit< - ConcreteContainerConfigData, - DagentConfigKey | CraneConfigKey | 'secrets' -> & { - secrets?: UniqueSecretKeyValue[] -} - export type ConcreteContainerConfigData = Omit & { secrets?: UniqueSecretKeyValue[] } diff --git a/web/crux-ui/src/pages/[teamSlug]/config-bundles.tsx b/web/crux-ui/src/pages/[teamSlug]/config-bundles.tsx index 113742a58d..8b0228a7d1 100644 --- a/web/crux-ui/src/pages/[teamSlug]/config-bundles.tsx +++ b/web/crux-ui/src/pages/[teamSlug]/config-bundles.tsx @@ -76,7 +76,6 @@ const ConfigBundles = (props: ConfigBundlesPageProps) => { className={clsx('max-h-72 w-full p-8 my-2', modulo3Class, modulo2Class)} key={`bundle-${index}`} configBundle={it} - showConfigIcon /> ) })} diff --git a/web/crux-ui/src/pages/[teamSlug]/config-bundles/[configBundleId].tsx b/web/crux-ui/src/pages/[teamSlug]/config-bundles/[configBundleId].tsx index 20838d0d70..134273bb00 100644 --- a/web/crux-ui/src/pages/[teamSlug]/config-bundles/[configBundleId].tsx +++ b/web/crux-ui/src/pages/[teamSlug]/config-bundles/[configBundleId].tsx @@ -76,7 +76,7 @@ const ConfigBundleDetailsPage = (props: ConfigBundleDetailsPageProps) => { {editing ? ( ) : ( - + )} TODO deployment list and config diff --git a/web/crux/src/app/container/container.mapper.ts b/web/crux/src/app/container/container.mapper.ts index af203709ed..20fbe453ff 100644 --- a/web/crux/src/app/container/container.mapper.ts +++ b/web/crux/src/app/container/container.mapper.ts @@ -16,6 +16,7 @@ import { ContainerConfigUpdatedEvent } from 'src/domain/domain-events' import { ImageDetails } from 'src/domain/image' import { toNullableBoolean, toNullableNumber, toPrismaJson } from 'src/domain/utils' import { versionIsMutable } from 'src/domain/version' +import { ListSecretsResponse } from 'src/grpc/protobuf/proto/common' import ConfigBundleMapper from '../config.bundle/config.bundle.mapper' import DeployMapper from '../deploy/deploy.mapper' import ImageMapper from '../image/image.mapper' @@ -31,7 +32,6 @@ import { ContainerConfigTypeDto, ContainerSecretsDto, } from './container.dto' -import { ListSecretsResponse } from 'src/grpc/protobuf/proto/common' @Injectable() export default class ContainerMapper { diff --git a/web/crux/src/app/container/container.module.ts b/web/crux/src/app/container/container.module.ts index b3a9416294..a98e17da1e 100644 --- a/web/crux/src/app/container/container.module.ts +++ b/web/crux/src/app/container/container.module.ts @@ -1,5 +1,6 @@ import { forwardRef, Module } from '@nestjs/common' import PrismaService from 'src/services/prisma.service' +import AgentModule from '../agent/agent.module' import AuditLoggerModule from '../audit.logger/audit.logger.module' import ConfigBundleModule from '../config.bundle/config.bundle.module' import DeployModule from '../deploy/deploy.module' @@ -12,7 +13,6 @@ import ContainerConfigHttpController from './container-config.http.service' import ContainerConfigService from './container-config.service' import ContainerConfigWebSocketGateway from './container-config.ws.gateway' import ContainerMapper from './container.mapper' -import AgentModule from '../agent/agent.module' @Module({ imports: [ diff --git a/web/crux/src/app/deploy/deploy.mapper.spec.ts b/web/crux/src/app/deploy/deploy.mapper.spec.ts index bdbb915a45..6fc9e00c38 100644 --- a/web/crux/src/app/deploy/deploy.mapper.spec.ts +++ b/web/crux/src/app/deploy/deploy.mapper.spec.ts @@ -632,6 +632,8 @@ describe('DeployMapper', () => { updatedAt: new Date(), createdBy: 'created-by', updatedBy: 'updated-by', + deployedAt: null, + deployedBy: null, node: { id: 'deployment-node-id', name: 'deployment node', diff --git a/web/crux/src/app/node/node.service.spec.ts b/web/crux/src/app/node/node.service.spec.ts index dc1473d73b..ab4e15f6e1 100644 --- a/web/crux/src/app/node/node.service.spec.ts +++ b/web/crux/src/app/node/node.service.spec.ts @@ -96,9 +96,11 @@ describe('NodeService', () => { await nodeService.deleteContainer('test-node-id', 'test-prefix', 'test-name') expect(createAgentEventMock).toHaveBeenCalledWith('test-node-id', 'containerCommand', { - container: { - prefix: 'test-prefix', - name: 'test-name', + target: { + container: { + prefix: 'test-prefix', + name: 'test-name', + }, }, operation: 'deleteContainer', }) @@ -108,7 +110,9 @@ describe('NodeService', () => { await nodeService.deleteAllContainers('test-node-id', 'test-prefix') expect(createAgentEventMock).toHaveBeenCalledWith('test-node-id', 'containerCommand', { - prefix: 'test-prefix', + target: { + prefix: 'test-prefix', + }, operation: 'deleteContainers', }) }) diff --git a/web/crux/src/domain/agent-callback.ts b/web/crux/src/domain/agent-callback.ts index b8b3b00df7..0763b87871 100644 --- a/web/crux/src/domain/agent-callback.ts +++ b/web/crux/src/domain/agent-callback.ts @@ -75,7 +75,6 @@ export default class AgentCallback { } onError(key: string, error: AgentError) { - console.log('error', key) const result = this.requests.get(key) if (!result) { return diff --git a/web/crux/src/domain/agent.spec.ts b/web/crux/src/domain/agent.spec.ts index ce9b7783f8..0dbe01be0d 100644 --- a/web/crux/src/domain/agent.spec.ts +++ b/web/crux/src/domain/agent.spec.ts @@ -200,7 +200,9 @@ describe('agent', () => { const commandChannel = firstValueFrom(agent.onConnected(jest.fn())) const deleteRequest: DeleteContainersRequest = { - prefix: 'prefix', + target: { + prefix: 'prefix', + }, } const deleteRes = agent.deleteContainers(deleteRequest) @@ -213,7 +215,7 @@ describe('agent', () => { agent.onCallback( 'deleteContainers', Agent.containerPrefixNameOf({ - prefix: deleteRequest.prefix, + prefix: deleteRequest.target.prefix, name: '', }), Empty, @@ -230,7 +232,9 @@ describe('agent', () => { const commandChannel = firstValueFrom(agent.onConnected(jest.fn())) const deleteRequest: DeleteContainersRequest = { - prefix: 'prefix', + target: { + prefix: 'prefix', + }, } const deleteRes = agent.deleteContainers(deleteRequest) @@ -306,9 +310,11 @@ describe('agent', () => { const commandChannel = firstValueFrom(agent.onConnected(jest.fn())) const req: ListSecretsRequest = { - container: { - prefix: 'prefix', - name: 'name', + target: { + container: { + prefix: 'prefix', + name: 'name', + }, }, } @@ -320,14 +326,17 @@ describe('agent', () => { }) const message: ListSecretsResponse = { - prefix: 'prefix', - name: 'name', + target: { + container: { + prefix: 'prefix', + name: 'name', + }, + }, publicKey: 'key', - hasKeys: true, keys: ['k1', 'k2', 'k3'], } - agent.onCallback('listSecrets', Agent.containerPrefixNameOf(req.container), message) + agent.onCallback('listSecrets', Agent.containerPrefixNameOf(req.target.container), message) const secretsActual = await secrets expect(secretsActual).toEqual(message) @@ -339,9 +348,11 @@ describe('agent', () => { const commandChannel = firstValueFrom(agent.onConnected(jest.fn())) const req: ListSecretsRequest = { - container: { - prefix: 'prefix', - name: 'name', + target: { + container: { + prefix: 'prefix', + name: 'name', + }, }, } diff --git a/web/crux/src/domain/container-merge.spec.ts b/web/crux/src/domain/container-merge.spec.ts index 7ab1249ef9..a85e7ad440 100644 --- a/web/crux/src/domain/container-merge.spec.ts +++ b/web/crux/src/domain/container-merge.spec.ts @@ -6,6 +6,7 @@ describe('container-merge', () => { name: 'img', capabilities: [], deploymentStrategy: 'recreate', + workingDirectory: '/app', expose: 'expose', networkMode: 'bridge', proxyHeaders: false, @@ -219,14 +220,15 @@ describe('container-merge', () => { type: 'mem', }, ], - metrics: null, - expectedState: null, + metrics: undefined, + expectedState: undefined, } const fullConcreteConfig: ConcreteContainerConfigData = { name: 'instance.img', capabilities: [], deploymentStrategy: 'recreate', + workingDirectory: '/app', expose: 'exposeWithTls', networkMode: 'host', proxyHeaders: true, @@ -443,8 +445,8 @@ describe('container-merge', () => { type: 'rwo', }, ], - metrics: null, - expectedState: null, + metrics: undefined, + expectedState: undefined, } describe('mergeConfigsWithConcreteConfig', () => { diff --git a/web/crux/src/domain/validation.ts b/web/crux/src/domain/validation.ts index 2dc9c7789c..5ae47a66b2 100644 --- a/web/crux/src/domain/validation.ts +++ b/web/crux/src/domain/validation.ts @@ -141,12 +141,15 @@ const createOverlapTest = ( // note: here yup passes reference as array const portConfigRule = yup.mixed().when('portRanges', ([portRanges]) => { if (!portRanges?.length) { - return yup.array( - yup.object().shape({ - internal: portNumberRule, - external: portNumberOptionalRule, - }), - ).nullable().optional() + return yup + .array( + yup.object().shape({ + internal: portNumberRule, + external: portNumberOptionalRule, + }), + ) + .nullable() + .optional() } return yup @@ -155,7 +158,9 @@ const portConfigRule = yup.mixed().when('portRanges', ([portRanges]) => { internal: createOverlapTest(portNumberRule, portRanges, 'internal'), external: createOverlapTest(portNumberOptionalRule, portRanges, 'external'), }), - ).nullable().optional() + ) + .nullable() + .optional() }) const portRangeConfigRule = yup.array( @@ -256,15 +261,19 @@ const createMetricsPortRule = (ports: ContainerPort[]) => { const metricsRule = yup.mixed().when(['ports'], ([ports]) => { const portRule = createMetricsPortRule(ports) - return yup.object().when({ - is: (it: Metrics) => it?.enabled, - then: schema => - schema.shape({ - enabled: yup.boolean(), - path: yup.string().nullable(), - port: portRule, - }), - }).nullable().optional() + return yup + .object() + .when({ + is: (it: Metrics) => it?.enabled, + then: schema => + schema.shape({ + enabled: yup.boolean(), + path: yup.string().nullable(), + port: portRule, + }), + }) + .nullable() + .optional() }) const expectedContainerStateRule = yup.object().shape({ From 2a6dc53a9d85645e02ae71ab41d41a0d87307e36 Mon Sep 17 00:00:00 2001 From: Mate Vago Date: Tue, 26 Nov 2024 10:31:20 +0100 Subject: [PATCH 04/32] refactor: remove unused ws messages --- .../deployment-container-status-list.tsx | 10 +- .../instances/use-instance-state.ts | 100 ----------- .../deployments/use-deployment-state.tsx | 160 +----------------- .../components/nodes/node-containers-list.tsx | 5 +- .../src/components/shared/key-only-input.tsx | 2 - web/crux-ui/src/models/deployment.ts | 24 +-- web/crux/src/app/deploy/deploy.message.ts | 18 +- web/crux/src/app/deploy/deploy.ws.gateway.ts | 36 ---- 8 files changed, 14 insertions(+), 341 deletions(-) delete mode 100644 web/crux-ui/src/components/deployments/instances/use-instance-state.ts diff --git a/web/crux-ui/src/components/deployments/deployment-container-status-list.tsx b/web/crux-ui/src/components/deployments/deployment-container-status-list.tsx index 391ce58ca8..6121ec5b9f 100644 --- a/web/crux-ui/src/components/deployments/deployment-container-status-list.tsx +++ b/web/crux-ui/src/components/deployments/deployment-container-status-list.tsx @@ -32,7 +32,7 @@ interface DeploymentContainerStatusListProps { progress: Record } -type ContainerWithInstance = Container & { +type ContainerWithConfigId = Container & { configId: string } @@ -44,7 +44,7 @@ const DeploymentContainerStatusList = (props: DeploymentContainerStatusListProps const now = utcNow() - const [containers, setContainers] = useState(() => + const [containers, setContainers] = useState(() => deployment.instances.map(it => ({ configId: it.config.id, id: { @@ -73,7 +73,7 @@ const DeploymentContainerStatusList = (props: DeploymentContainerStatusListProps } as WatchContainerStatusMessage), }) - const merge = (weak: ContainerWithInstance[], strong: Container[]): ContainerWithInstance[] => { + const merge = (weak: ContainerWithConfigId[], strong: Container[]): ContainerWithConfigId[] => { if (!strong || strong.length === 0) { return weak } @@ -121,7 +121,7 @@ const DeploymentContainerStatusList = (props: DeploymentContainerStatusListProps /> + body={(it: ContainerWithConfigId) => progress[it.configId]?.progress < 1 ? ( ) : ( @@ -136,7 +136,7 @@ const DeploymentContainerStatusList = (props: DeploymentContainerStatusListProps /> ( + body={(it: ContainerWithConfigId) => ( <> {it.state && (
diff --git a/web/crux-ui/src/components/deployments/instances/use-instance-state.ts b/web/crux-ui/src/components/deployments/instances/use-instance-state.ts deleted file mode 100644 index 8298ad9661..0000000000 --- a/web/crux-ui/src/components/deployments/instances/use-instance-state.ts +++ /dev/null @@ -1,100 +0,0 @@ -// import { -// ContainerConfigData, -// GetInstanceSecretsMessage, -// ContainerConfigProperty, -// Instance, -// InstanceSecretsMessage, -// mergeConfigs, -// ConcreteContainerConfigData, -// WS_TYPE_GET_INSTANCE_SECRETS, -// WS_TYPE_INSTANCE_SECRETS, -// } from '@app/models' -// import { createContainerConfigSchema, getValidationError } from '@app/validations' -// import { useEffect, useState } from 'react' -// import { DeploymentActions, DeploymentState } from '../use-deployment-state' -// import useTranslation from 'next-translate/useTranslation' - -// export type InstanceStateOptions = { -// deploymentState: DeploymentState -// deploymentActions: DeploymentActions -// instance: Instance -// } - -// export type InstanceState = { -// config: ConcreteContainerConfigData -// resetableConfig: ContainerConfigData -// definedSecrets: string[] -// errorMessage: string -// } - -// export type InstanceActions = { -// resetSection: (section: ContainerConfigProperty) => void -// onPatch: (newConfig: Partial) => void -// onParseError: (error: Error) => void -// } - -// const useInstanceState = (options: InstanceStateOptions) => { -// const { t } = useTranslation('container') - -// const { instance, deploymentState, deploymentActions } = options -// const { sock } = deploymentState - -// const [parseError, setParseError] = useState(null) -// const [definedSecrets, setDefinedSecrets] = useState([]) - -// sock.on(WS_TYPE_INSTANCE_SECRETS, (message: InstanceSecretsMessage) => { -// if (message.instanceId !== instance.id) { -// return -// } - -// setDefinedSecrets(message.keys) -// }) - -// useEffect(() => { -// sock.send(WS_TYPE_GET_INSTANCE_SECRETS, { -// id: instance.id, -// } as GetInstanceSecretsMessage) -// }, [instance.id, sock]) - -// const mergedConfig = mergeConfigs(instance.image.config, instance.config) - -// const errorMessage = -// parseError ?? getValidationError(createContainerConfigSchema(instance.image.labels), mergedConfig, null, t)?.message - -// const resetSection = (section: ContainerConfigProperty): ConcreteContainerConfigData => { -// const newConfig = { ...instance.config } as any -// newConfig[section] = null - -// deploymentActions.updateInstanceConfig(instance.id, newConfig) - -// sock.send(WS_TYPE_PATCH_INSTANCE, { -// instanceId: instance.id, -// resetSection: section, -// } as PatchInstanceMessage) - -// return newConfig -// } - -// const onPatch = (id: string, newConfig: ConcreteContainerConfigData) => { -// deploymentActions.onPatchInstance(id, newConfig) -// setParseError(null) -// } - -// const onParseError = (err: Error) => setParseError(err.message) - -// return [ -// { -// config: mergedConfig, -// resetableConfig: instance.config, -// definedSecrets, -// errorMessage, -// }, -// { -// onPatch, -// resetSection, -// onParseError, -// }, -// ] -// } - -// export default useInstanceState diff --git a/web/crux-ui/src/components/deployments/use-deployment-state.tsx b/web/crux-ui/src/components/deployments/use-deployment-state.tsx index 6d04bb1db3..e1e4885cf4 100644 --- a/web/crux-ui/src/components/deployments/use-deployment-state.tsx +++ b/web/crux-ui/src/components/deployments/use-deployment-state.tsx @@ -7,9 +7,7 @@ import usePersistedViewMode from '@app/hooks/use-persisted-view-mode' import useTeamRoutes from '@app/hooks/use-team-routes' import useWebSocket from '@app/hooks/use-websocket' import { - ConcreteContainerConfigData, DeploymentDetails, - DeploymentInvalidatedSecrets, deploymentIsCopiable, deploymentIsDeletable, deploymentIsDeployable, @@ -21,14 +19,12 @@ import { ImageDeletedMessage, Instance, instanceCreatedMessageToInstance, - InstanceMessage, InstancesAddedMessage, NodeEventMessage, ProjectDetails, VersionDetails, WebSocketSaveState, WS_TYPE_IMAGE_DELETED, - WS_TYPE_INSTANCE, WS_TYPE_INSTANCES_ADDED, WS_TYPE_NODE_EVENT, } from '@app/models' @@ -68,24 +64,13 @@ export type DeploymentState = { export type DeploymentActions = { setEditState: (state: DeploymentEditState) => void onDeploymentEdited: (editedDeployment: DeploymentDetails) => void - onPatchInstance: (id: string, newConfig: ConcreteContainerConfigData) => void - updateInstanceConfig: (id: string, newConfig: ConcreteContainerConfigData) => void setViewMode: (viewMode: ViewMode) => void - onInvalidateSecrets: (secrets: DeploymentInvalidatedSecrets[]) => void onDeploymentTokenCreated: (token: DeploymentToken) => void onRevokeDeploymentToken: VoidFunction onInstanceSelected: (id: string, deploy: boolean) => void onAllInstancesToggled: (deploy: boolean) => void } -// const mergeInstancePatch = (instance: Instance, message: InstanceUpdatedMessage): Instance => ({ -// ...instance, -// config: { -// ...instance.config, -// ...message, -// }, -// }) - const useDeploymentState = (options: DeploymentStateOptions): [DeploymentState, DeploymentActions] => { const { t } = useTranslation('deployments') const routes = useTeamRoutes() @@ -93,13 +78,9 @@ const useDeploymentState = (options: DeploymentStateOptions): [DeploymentState, const { deployment: optionDeploy, onWsError, onApiError } = options const { project, version } = optionDeploy - // const throttle = useThrottling(DEPLOYMENT_EDIT_WS_REQUEST_DELAY) - - // const patch = useRef>({}) - const [deployment, setDeployment] = useState(optionDeploy) const [node, setNode] = useNodeState(optionDeploy.node) - const [saveState] = useState(null) + const [saveState, setSaveState] = useState('disconnected') const [editState, setEditState] = useState('details') const [instances, setInstances] = useState(deployment.instances ?? []) const [viewMode, setViewMode] = usePersistedViewMode({ initialViewMode: 'list', pageName: 'deployments' }) @@ -126,50 +107,13 @@ const useDeploymentState = (options: DeploymentStateOptions): [DeploymentState, }) const sock = useWebSocket(routes.deployment.detailsSocket(deployment.id), { - // onOpen: () => setSaveState('connected'), - // onClose: () => setSaveState('disconnected'), - // onSend: message => { - // if ([WS_TYPE_PATCH_INSTANCE, WS_TYPE_PATCH_DEPLOYMENT_ENV].includes(message.type)) { - // setSaveState('saving') - // } - // }, - // onReceive: message => { - // if (WS_TYPE_PATCH_RECEIVED === message.type) { - // setSaveState('saved') - // } - // }, + onOpen: () => setSaveState('connected'), + onClose: () => setSaveState('disconnected'), onError: onWsError, }) const editor = useEditorState(sock) - // sock.on(WS_TYPE_DEPLOYMENT_ENV_UPDATED, (message: DeploymentEnvUpdatedMessage) => { - // setDeployment({ - // ...deployment, - // ...message, - // }) - // }) - - // sock.on(WS_TYPE_INSTANCE_UPDATED, (message: InstanceUpdatedMessage) => { - // const index = instances.findIndex(it => it.id === message.instanceId) - // if (index < 0) { - // sock.send(WS_TYPE_GET_INSTANCE, { - // id: message.instanceId, - // } as GetInstanceMessage) - // return - // } - - // const oldOne = instances[index] - // const instance = mergeInstancePatch(oldOne, message) - - // const newInstances = [...instances] - // newInstances[index] = instance - - // setInstances(newInstances) - // }) - - sock.on(WS_TYPE_INSTANCE, (message: InstanceMessage) => setInstances([...instances, message])) - sock.on(WS_TYPE_INSTANCES_ADDED, (message: InstancesAddedMessage) => setInstances([...instances, ...message.map(it => instanceCreatedMessageToInstance(it))]), ) @@ -183,101 +127,6 @@ const useDeploymentState = (options: DeploymentStateOptions): [DeploymentState, setEditState('details') } - // const onEnvironmentEdited = environment => { - // setSaveState('saving') - // setDeployment({ - // ...deployment, - // environment, - // }) - // throttle(() => { - // sock.send(WS_TYPE_PATCH_DEPLOYMENT_ENV, { - // environment, - // }) - // }) - // } - - const onInvalidateSecrets = (secrets: DeploymentInvalidatedSecrets[]) => { - const newInstances = instances.map(it => { - const invalidated = secrets.find(sec => sec.instanceId === it.id) - if (!invalidated) { - return it - } - - return { - ...it, - config: { - ...it.config, - secrets: (it.config.secrets ?? []).map(secret => { - if (invalidated.invalid.includes(secret.id)) { - return { - ...secret, - encrypted: false, - publicKey: '', - value: '', - } - } - - return secret - }), - }, - } - }) - - setInstances(newInstances) - } - - const onPatchInstance = (_: string, __: ConcreteContainerConfigData) => { - // const onPatchInstance = (id: string, newConfig: ConcreteContainerConfigData) => { - // const index = instances.findIndex(it => it.id === id) - // if (index < 0) { - // return - // } - // setSaveState('saving') - // const newPatch = { - // ...patch.current, - // ...newConfig, - // } - // patch.current = newPatch - // const newInstances = [...instances] - // const instance = newInstances[index] - // newInstances[index] = { - // ...instance, - // config: { - // ...instance.config, - // ...newConfig, - // }, - // } - // setInstances(newInstances) - // throttle(() => { - // sock.send(WS_TYPE_PATCH_INSTANCE, { - // instanceId: id, - // config: patch.current, - // } as PatchInstanceMessage) - // patch.current = {} - // }) - } - - const updateInstanceConfig = (_: string, __: ConcreteContainerConfigData) => { - // const updateInstanceConfig = (id: string, newConfig: ConcreteContainerConfigData) => { - // const index = instances.findIndex(it => it.id === id) - // if (index < 0) { - // return - // } - // setSaveState('saving') - // const newInstances = [...instances] - // const instance = newInstances[index] - // newInstances[index] = { - // ...instance, - // config: instance.config - // ? { - // ...instance.config, - // ...newConfig, - // } - // : newConfig, - // } - // setInstances(newInstances) - } - const onDeploymentTokenCreated = (token: DeploymentToken) => { setDeployment({ ...deployment, @@ -348,13 +197,10 @@ const useDeploymentState = (options: DeploymentStateOptions): [DeploymentState, setEditState, onDeploymentEdited, setViewMode, - onInvalidateSecrets, - onPatchInstance, onDeploymentTokenCreated, onRevokeDeploymentToken, onInstanceSelected, onAllInstancesToggled, - updateInstanceConfig, }, ] } diff --git a/web/crux-ui/src/components/nodes/node-containers-list.tsx b/web/crux-ui/src/components/nodes/node-containers-list.tsx index 73af31fe54..606dab5dcf 100644 --- a/web/crux-ui/src/components/nodes/node-containers-list.tsx +++ b/web/crux-ui/src/components/nodes/node-containers-list.tsx @@ -60,7 +60,10 @@ const NodeContainersList = (props: NodeContainersListProps) => { sortField={imageNameOfContainer} sort={sortString} bodyClassName="truncate" - body={(it: Container) => {imageNameOfContainer(it)}} + body={(it: Container) => { + const name = imageNameOfContainer(it) + return {name} + }} /> { } const onResetSection = () => { - // dispatch(mergeItems(items)) - propsOnResetSection() } diff --git a/web/crux-ui/src/models/deployment.ts b/web/crux-ui/src/models/deployment.ts index 3f90d526c2..181c888a21 100644 --- a/web/crux-ui/src/models/deployment.ts +++ b/web/crux-ui/src/models/deployment.ts @@ -1,7 +1,7 @@ import { Audit } from './audit' import { DeploymentStatus, DyoApiError, slugify } from './common' import { ConfigBundleDetails } from './config-bundle' -import { ConcreteContainerConfig, ContainerIdentifier, ContainerState } from './container' +import { ConcreteContainerConfig, ContainerState } from './container' import { ImageDeletedMessage, VersionImage } from './image' import { Instance } from './instance' import { DyoNode } from './node' @@ -150,9 +150,6 @@ export type GetInstanceMessage = { id: string } -export const WS_TYPE_INSTANCE = 'instance' -export type InstanceMessage = Instance & {} - export const WS_TYPE_INSTANCES_ADDED = 'instances-added' type InstanceCreatedMessage = { id: string @@ -180,25 +177,6 @@ export type DeploymentEventMessage = DeploymentEvent export const WS_TYPE_DEPLOYMENT_FINISHED = 'deployment-finished' -export const WS_TYPE_GET_INSTANCE_SECRETS = 'get-instance-secrets' -export type GetInstanceSecretsMessage = { - id: string -} - -export type InstanceSecrets = { - container: ContainerIdentifier - - publicKey: string - - keys?: string[] -} - -export const WS_TYPE_INSTANCE_SECRETS = 'instance-secrets' -export type InstanceSecretsMessage = { - instanceId: string - keys: string[] -} - export const deploymentIsMutable = (status: DeploymentStatus, type: VersionType): boolean => { switch (status) { case 'preparing': diff --git a/web/crux/src/app/deploy/deploy.message.ts b/web/crux/src/app/deploy/deploy.message.ts index 634d30b27e..9575ce4b5f 100644 --- a/web/crux/src/app/deploy/deploy.message.ts +++ b/web/crux/src/app/deploy/deploy.message.ts @@ -15,28 +15,12 @@ export type DeploymentBundlesUpdatedMessage = { bundles: ConfigBundleDto[] } -export const WS_TYPE_GET_DEPLOYMENT_SECRETS = 'get-deployment-secrets' -export const WS_TYPE_GET_INSTANCE_SECRETS = 'get-instance-secrets' -export type GetInstanceSecretsMessage = { - id: string -} - -export const WS_TYPE_DEPLOYMENT_SECRETS = 'deployment-secrets' -export type DeploymentSecretsMessage = { - keys: string[] -} - -export const WS_TYPE_INSTANCE_SECRETS = 'instance-secrets' -export type InstanceSecretsMessage = { - instanceId: string - keys: string[] -} - type InstanceCreatedMessage = { id: string configId: string image: ImageDetailsDto } + export const WS_TYPE_INSTANCES_ADDED = 'instances-added' export type InstancesAddedMessage = InstanceCreatedMessage[] diff --git a/web/crux/src/app/deploy/deploy.ws.gateway.ts b/web/crux/src/app/deploy/deploy.ws.gateway.ts index 3a50f8b3ce..1296f4e3b7 100644 --- a/web/crux/src/app/deploy/deploy.ws.gateway.ts +++ b/web/crux/src/app/deploy/deploy.ws.gateway.ts @@ -30,15 +30,8 @@ import { IdentityFromSocket } from '../token/jwt-auth.guard' import { DeploymentEventListMessage, DeploymentEventMessage, - DeploymentSecretsMessage, - GetInstanceSecretsMessage, - InstanceSecretsMessage, WS_TYPE_DEPLOYMENT_EVENT_LIST, - WS_TYPE_DEPLOYMENT_SECRETS, WS_TYPE_FETCH_DEPLOYMENT_EVENTS, - WS_TYPE_GET_DEPLOYMENT_SECRETS, - WS_TYPE_GET_INSTANCE_SECRETS, - WS_TYPE_INSTANCE_SECRETS, } from './deploy.message' import DeployService from './deploy.service' @@ -140,35 +133,6 @@ export default class DeployWebSocketGateway { ) } - @AuditLogLevel('disabled') - @SubscribeMessage(WS_TYPE_GET_DEPLOYMENT_SECRETS) - async getDeploymentSecrets(@DeploymentId() deploymentId: string): Promise> { - const secrets = await this.service.getDeploymentSecrets(deploymentId) - - return { - type: WS_TYPE_DEPLOYMENT_SECRETS, - data: { - keys: secrets.keys ?? [], - }, - } - } - - @AuditLogLevel('disabled') - @SubscribeMessage(WS_TYPE_GET_INSTANCE_SECRETS) - async getInstanceSecrets( - @SocketMessage() message: GetInstanceSecretsMessage, - ): Promise> { - const secrets = await this.service.getInstanceSecrets(message.id) - - return { - type: WS_TYPE_INSTANCE_SECRETS, - data: { - instanceId: message.id, - keys: secrets.keys ?? [], - }, - } - } - @AuditLogLevel('disabled') @SubscribeMessage(WS_TYPE_FOCUS_INPUT) async onFocusInput( From be6c022aa01344a532192419ac3e54db041a6756 Mon Sep 17 00:00:00 2001 From: Mate Vago Date: Wed, 27 Nov 2024 12:20:09 +0100 Subject: [PATCH 05/32] fix: migration --- web/crux-ui/src/models/deployment.ts | 1 + .../20241017094935_config_rework/migration.sql | 9 ++++----- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web/crux-ui/src/models/deployment.ts b/web/crux-ui/src/models/deployment.ts index 181c888a21..0a555f9e7c 100644 --- a/web/crux-ui/src/models/deployment.ts +++ b/web/crux-ui/src/models/deployment.ts @@ -140,6 +140,7 @@ export type StartDeployment = { } // ws +// TODO (@m8vago): move this to the container-config ws endpoint export const WS_TYPE_DEPLOYMENT_BUNDLES_UPDATED = 'deployment-bundles-updated' export type DeploymentBundlesUpdatedMessage = { bundles: ConfigBundleDetails[] diff --git a/web/crux/prisma/migrations/20241017094935_config_rework/migration.sql b/web/crux/prisma/migrations/20241017094935_config_rework/migration.sql index 920069a33e..233300debb 100644 --- a/web/crux/prisma/migrations/20241017094935_config_rework/migration.sql +++ b/web/crux/prisma/migrations/20241017094935_config_rework/migration.sql @@ -205,7 +205,7 @@ WHERE "i"."id" IN (SELECT id FROM "_prisma_migrations_ConfiglessInstances"); DROP TABLE "_prisma_migrations_ConfiglessInstances"; --- AlterTable +-- add deployedAt and deployedBy ALTER TABLE "Deployment" ADD COLUMN "deployedAt" TIMESTAMPTZ(6), ADD COLUMN "deployedBy" UUID; @@ -214,18 +214,17 @@ SET "deployedAt" = "d"."updatedAt" FROM (select "id", "updatedAt" FROM "Deployment") AS "d" WHERE "d"."id" = "Deployment"."id"; --- AlterTable +-- set config id not null ALTER TABLE "ConfigBundle" ALTER COLUMN "configId" SET NOT NULL; --- AlterTable ALTER TABLE "Deployment" ALTER COLUMN "configId" SET NOT NULL; --- AlterTable ALTER TABLE "Image" ALTER COLUMN "configId" SET NOT NULL; --- AlterTable ALTER TABLE "Instance" ALTER COLUMN "configId" SET NOT NULL; + +-- create indices and constraints -- CreateIndex CREATE UNIQUE INDEX "ConfigBundle_configId_key" ON "ConfigBundle"("configId"); From b0c93cb9c09150e80e713b355d0fac14d2a9a89d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20Nagygy=C3=B6rgy?= Date: Wed, 27 Nov 2024 12:12:54 +0100 Subject: [PATCH 06/32] feat(web): api & ui --- .../components/nodes/select-node-chips.tsx | 12 +- web/crux-ui/src/models/deployment.ts | 10 +- .../src/pages/[teamSlug]/deployments.tsx | 313 ++++++++++-------- web/crux-ui/src/routes.ts | 11 +- web/crux/src/app/deploy/deploy.dto.ts | 38 +++ .../src/app/deploy/deploy.http.controller.ts | 25 +- web/crux/src/app/deploy/deploy.mapper.ts | 9 + web/crux/src/app/deploy/deploy.service.ts | 113 +++++-- web/crux/src/app/node/node.http.controller.ts | 6 +- 9 files changed, 356 insertions(+), 181 deletions(-) diff --git a/web/crux-ui/src/components/nodes/select-node-chips.tsx b/web/crux-ui/src/components/nodes/select-node-chips.tsx index a22598e16f..8c314589d9 100644 --- a/web/crux-ui/src/components/nodes/select-node-chips.tsx +++ b/web/crux-ui/src/components/nodes/select-node-chips.tsx @@ -12,13 +12,14 @@ type SelectNodeChipsProps = { className?: string name: string selection: DyoNode | null - onSelectionChange: (node: DyoNode) => Promise errorMessage?: string | null + allowNull?: boolean + onSelectionChange: (node: DyoNode | null) => Promise onNodesFetched?: (nodes: DyoNode[] | null) => void } const SelectNodeChips = (props: SelectNodeChipsProps) => { - const { className, name, selection, onSelectionChange, errorMessage, onNodesFetched } = props + const { className, name, selection, onSelectionChange, errorMessage, onNodesFetched, allowNull } = props const { t } = useTranslation('common') const routes = useTeamRoutes() @@ -31,6 +32,9 @@ const SelectNodeChips = (props: SelectNodeChipsProps) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [nodes]) + const baseChoices = allowNull === true ? [null] : [] + const choices = [...baseChoices, ...(nodes ?? [])] + return fetchError ? ( {t('errors:fetchFailed', { @@ -46,8 +50,8 @@ const SelectNodeChips = (props: SelectNodeChipsProps) => { it.name} + choices={choices} + converter={(it: DyoNode | null) => (allowNull === true && it == null ? t('none') : it.name)} selection={selection} onSelectionChange={onSelectionChange} /> diff --git a/web/crux-ui/src/models/deployment.ts b/web/crux-ui/src/models/deployment.ts index 1ae42fc703..6d6e9aad18 100644 --- a/web/crux-ui/src/models/deployment.ts +++ b/web/crux-ui/src/models/deployment.ts @@ -1,5 +1,5 @@ import { Audit } from './audit' -import { DeploymentStatus, DyoApiError, slugify } from './common' +import { DeploymentStatus, DyoApiError, PaginatedList, PaginationQuery, slugify } from './common' import { ContainerIdentifier, ContainerState, InstanceContainerConfigData, UniqueKeyValue } from './container' import { ImageConfigProperty, ImageDeletedMessage } from './image' import { Instance } from './instance' @@ -29,6 +29,14 @@ export type Deployment = { version: BasicVersion } +export type DeploymentQuery = PaginationQuery & { + nodeId?: string + filter?: string + status?: DeploymentStatus +} + +export type DeploymentList = PaginatedList + export type DeploymentToken = { id: string name: string diff --git a/web/crux-ui/src/pages/[teamSlug]/deployments.tsx b/web/crux-ui/src/pages/[teamSlug]/deployments.tsx index b66206c6dd..24a93a550c 100644 --- a/web/crux-ui/src/pages/[teamSlug]/deployments.tsx +++ b/web/crux-ui/src/pages/[teamSlug]/deployments.tsx @@ -3,53 +3,63 @@ import CopyDeploymentCard from '@app/components/deployments/copy-deployment-card import DeploymentStatusTag, { deploymentStatusTranslation } from '@app/components/deployments/deployment-status-tag' import useCopyDeploymentState from '@app/components/deployments/use-copy-deployment-state' import { Layout } from '@app/components/layout' +import SelectNodeChips from '@app/components/nodes/select-node-chips' import { BreadcrumbLink } from '@app/components/shared/breadcrumb' import Filters from '@app/components/shared/filters' import PageHeading from '@app/components/shared/page-heading' +import { PaginationSettings } from '@app/components/shared/paginator' import DyoButton from '@app/elements/dyo-button' import { DyoCard } from '@app/elements/dyo-card' import { chipsQALabelFromValue } from '@app/elements/dyo-chips' import DyoFilterChips from '@app/elements/dyo-filter-chips' import { DyoHeading } from '@app/elements/dyo-heading' import DyoIcon from '@app/elements/dyo-icon' +import { DyoInput } from '@app/elements/dyo-input' import DyoLink from '@app/elements/dyo-link' import DyoModal, { DyoConfirmationModal } from '@app/elements/dyo-modal' import DyoTable, { DyoColumn, sortDate, sortEnum, sortString } from '@app/elements/dyo-table' import { defaultApiErrorHandler } from '@app/errors' import useConfirmation from '@app/hooks/use-confirmation' -import { EnumFilter, TextFilter, enumFilterFor, textFilterFor, useFilters } from '@app/hooks/use-filters' import useTeamRoutes from '@app/hooks/use-team-routes' +import { useThrottling } from '@app/hooks/use-throttleing' import { DEPLOYMENT_STATUS_VALUES, Deployment, + DeploymentList, + DeploymentQuery, DeploymentStatus, + DyoNode, deploymentIsCopiable, deploymentIsDeletable, } from '@app/models' -import { TeamRoutes } from '@app/routes' -import { auditToLocaleDate, withContextAuthorization } from '@app/utils' -import { getCruxFromContext } from '@server/crux-api' +import { auditToLocaleDate } from '@app/utils' import clsx from 'clsx' -import { GetServerSidePropsContext } from 'next' import useTranslation from 'next-translate/useTranslation' import { useRouter } from 'next/router' import { QA_DIALOG_LABEL_DELETE_DEPLOYMENT, QA_MODAL_LABEL_DEPLOYMENT_NOTE } from 'quality-assurance' import { useEffect, useState } from 'react' -interface DeploymentsPageProps { - deployments: Deployment[] -} - -type DeploymentFilter = TextFilter & EnumFilter +const defaultPagination: PaginationSettings = { pageNumber: 0, pageSize: 10 } -const DeploymentsPage = (props: DeploymentsPageProps) => { - const { deployments: propsDeployments } = props +type FilterState = { + filter: string + status: DeploymentStatus | null +} +const DeploymentsPage = () => { const { t } = useTranslation('deployments') const routes = useTeamRoutes() const router = useRouter() - const [deployments, setDeployments] = useState(propsDeployments) + const [deployments, setDeployments] = useState([]) + const [total, setTotal] = useState(0) + const [filter, setFilter] = useState({ + filter: '', + status: null, + }) + const [filterNode, setFilterNode] = useState(null) + + const [pagination, setPagination] = useState(defaultPagination) const [creating, setCreating] = useState(false) @@ -61,15 +71,39 @@ const DeploymentsPage = (props: DeploymentsPageProps) => { }) const [confirmModalConfig, confirm] = useConfirmation() - const filters = useFilters({ - filters: [ - textFilterFor(it => [it.project.name, it.version.name, it.node.name, it.prefix]), - enumFilterFor(it => [it.status]), - ], - initialData: deployments, - }) + const throttle = useThrottling(1000) + + const fetchData = async () => { + const { status } = filter + + const query: DeploymentQuery = { + skip: pagination.pageNumber * pagination.pageSize, + take: pagination.pageSize, + filter: !filter.filter || filter.filter.trim() === '' ? null : filter.filter, + nodeId: filterNode?.id ?? null, + status: status, + } + + const res = await fetch(routes.deployment.api.list(query)) + + if (res.ok) { + const list = (await res.json()) as DeploymentList + setDeployments(list.items) + setTotal(list.total) + } else { + setDeployments([]) + } + } + + useEffect(() => { + throttle(fetchData) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filter, filterNode]) - useEffect(() => filters.setItems(deployments), [deployments]) + useEffect(() => { + throttle(fetchData, true) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pagination]) const selfLink: BreadcrumbLink = { name: t('common:deployments'), @@ -135,121 +169,134 @@ const DeploymentsPage = (props: DeploymentsPageProps) => { /> )} - {deployments.length ? ( - <> - filters.setFilter({ text: it })}> + +
+ + {t('common:filters')} + + +
+ setFilter({ ...filter, filter: e.target.value })} + grow + /> + t(deploymentStatusTranslation(it))} - selection={filters.filter?.enum} + selection={filter.status} onSelectionChange={type => { - filters.setFilter({ - enum: type, + setFilter({ + ...filter, + status: type === 'all' ? null : (type as DeploymentStatus), }) }} qaLabel={chipsQALabelFromValue} /> - - - - - - - - - it.audit.updatedAt ?? it.audit.createdAt} - sort={sortDate} - body={(it: Deployment) => auditToLocaleDate(it.audit)} - /> - } - /> - ( - <> - - - - - 0 ? 'cursor-pointer' : 'cursor-not-allowed opacity-30', - 'mr-2', - )} - onClick={() => !!it.note && it.note.length > 0 && setShowInfo(it)} - /> - - deploymentIsCopiable(it.status) && setCopyDeploymentTarget(it.id)} - /> - - {deploymentIsDeletable(it.status) ? ( - onDeleteDeployment(it)} - /> - ) : null} - - )} - /> - - - - ) : ( - - {t('noItems')} - - )} +
+
+ +
+ + {t('common:nodes')} + + + setFilterNode(it)} + selection={filterNode} + /> +
+
+ + + + + + + + it.audit.updatedAt ?? it.audit.createdAt} + sort={sortDate} + body={(it: Deployment) => auditToLocaleDate(it.audit)} + /> + } + /> + ( + <> + + + + + 0 ? 'cursor-pointer' : 'cursor-not-allowed opacity-30', + 'mr-2', + )} + onClick={() => !!it.note && it.note.length > 0 && setShowInfo(it)} + /> + + deploymentIsCopiable(it.status) && setCopyDeploymentTarget(it.id)} + /> + + {deploymentIsDeletable(it.status) ? ( + onDeleteDeployment(it)} + /> + ) : null} + + )} + /> + + {!showInfo ? null : ( { } export default DeploymentsPage - -const getPageServerSideProps = async (context: GetServerSidePropsContext) => { - const routes = TeamRoutes.fromContext(context) - - const deployments = await getCruxFromContext(context, routes.deployment.api.list()) - - return { - props: { - deployments, - }, - } -} - -export const getServerSideProps = withContextAuthorization(getPageServerSideProps) diff --git a/web/crux-ui/src/routes.ts b/web/crux-ui/src/routes.ts index 0782c73417..5d19386fe1 100644 --- a/web/crux-ui/src/routes.ts +++ b/web/crux-ui/src/routes.ts @@ -1,6 +1,13 @@ /* eslint-disable no-underscore-dangle */ import { GetServerSidePropsContext } from 'next' -import { AuditLogQuery, ContainerIdentifier, ContainerOperation, PaginationQuery, VersionSectionsState } from './models' +import { + AuditLogQuery, + ContainerIdentifier, + ContainerOperation, + DeploymentQuery, + PaginationQuery, + VersionSectionsState, +} from './models' // Routes: export const ROUTE_DOCS = 'https://docs.dyrector.io' @@ -436,7 +443,7 @@ class DeploymentApi { this.root = `/api${root}` } - list = () => this.root + list = (query?: DeploymentQuery) => urlQuery(this.root, query) details = (id: string) => `${this.root}/${id}` diff --git a/web/crux/src/app/deploy/deploy.dto.ts b/web/crux/src/app/deploy/deploy.dto.ts index 217e7b49d5..939fbc41fb 100644 --- a/web/crux/src/app/deploy/deploy.dto.ts +++ b/web/crux/src/app/deploy/deploy.dto.ts @@ -38,6 +38,37 @@ export type DeploymentStatusDto = (typeof DEPLOYMENT_STATUS_VALUES)[number] export type EnvironmentToConfigBundleNameMap = Record +export class DeploymentQueryDto { + @IsOptional() + @IsInt() + @Type(() => Number) + @ApiProperty() + readonly skip?: number + + @IsOptional() + @IsInt() + @Type(() => Number) + @ApiProperty() + readonly take?: number + + @IsOptional() + @IsString() + @Type(() => String) + @ApiProperty() + readonly nodeId?: string + + @IsOptional() + @IsString() + @Type(() => String) + @ApiProperty() + readonly filter?: string + + @IsOptional() + @ApiProperty({ enum: DEPLOYMENT_STATUS_VALUES }) + @IsIn(DEPLOYMENT_STATUS_VALUES) + readonly status?: DeploymentStatusDto +} + export class BasicDeploymentDto { @IsUUID() id: string @@ -353,3 +384,10 @@ export type DeploymentDetails = DeploymentWithNodeVersion & { } }[] } + +export class DeploymentListDto extends PaginatedList { + @Type(() => DeploymentDto) + items: DeploymentDto[] + + total: number +} diff --git a/web/crux/src/app/deploy/deploy.http.controller.ts b/web/crux/src/app/deploy/deploy.http.controller.ts index 5bd22a191b..82974f14bc 100644 --- a/web/crux/src/app/deploy/deploy.http.controller.ts +++ b/web/crux/src/app/deploy/deploy.http.controller.ts @@ -18,12 +18,14 @@ import { ApiBody, ApiConflictResponse, ApiCreatedResponse, + ApiExtraModels, ApiForbiddenResponse, ApiNoContentResponse, ApiNotFoundResponse, ApiOkResponse, ApiOperation, ApiTags, + refs, } from '@nestjs/swagger' import { Identity } from '@ory/kratos-client' import UuidParams from 'src/decorators/api-params.decorator' @@ -35,8 +37,10 @@ import { CreateDeploymentTokenDto, DeploymentDetailsDto, DeploymentDto, + DeploymentListDto, DeploymentLogListDto, DeploymentLogPaginationQuery, + DeploymentQueryDto, DeploymentTokenCreatedDto, InstanceDto, InstanceSecretsDto, @@ -79,17 +83,28 @@ export default class DeployHttpController { @HttpCode(HttpStatus.OK) @ApiOperation({ description: - 'Get the list of deployments. Request needs to include `teamSlug` in URL. A deployment should include `id`, `prefix`, `status`, `note`, `audit` log details, project `name`, `id`, `type`, version `name`, `type`, `id`, and node `name`, `id`, `type`.', + 'Get the list of deployments. Request needs to include `teamSlug` in URL. Query could include `skip` and `take` to paginate. A deployment should include `id`, `prefix`, `status`, `note`, `audit` log details, project `name`, `id`, `type`, version `name`, `type`, `id`, and node `name`, `id`, `type`.', summary: 'Fetch the list of deployments.', }) + @ApiExtraModels(DeploymentDto, DeploymentListDto) @ApiOkResponse({ - type: DeploymentDto, - isArray: true, + schema: { + anyOf: refs(DeploymentDto, DeploymentListDto), + }, description: 'List of deployments.', }) @ApiForbiddenResponse({ description: 'Unauthorized request for deployments.' }) - async getDeployments(@TeamSlug() teamSlug: string): Promise { - return await this.service.getDeployments(teamSlug) + async getDeployments( + @TeamSlug() teamSlug: string, + @Query() query: DeploymentQueryDto, + ): Promise { + const page = await this.service.getDeployments(teamSlug, query) + if (!!query.skip || !!query.take) { + return page + } + + // NOTE(@robot9706): If no pagination parameters are present return the items only to be backward compatible + return page.items } @Get(ROUTE_DEPLOYMENT_ID) diff --git a/web/crux/src/app/deploy/deploy.mapper.ts b/web/crux/src/app/deploy/deploy.mapper.ts index 7a658e886c..ac0aa56f31 100644 --- a/web/crux/src/app/deploy/deploy.mapper.ts +++ b/web/crux/src/app/deploy/deploy.mapper.ts @@ -89,6 +89,15 @@ export default class DeployMapper { } } + statusDtoToDb(it: DeploymentStatusDto): DeploymentStatusEnum { + switch (it) { + case 'in-progress': + return 'inProgress' + default: + return it as DeploymentStatusEnum + } + } + toDeploymentWithBasicNodeDto(it: DeploymentWithNode, nodeStatus: NodeConnectionStatus): DeploymentWithBasicNodeDto { return { id: it.id, diff --git a/web/crux/src/app/deploy/deploy.service.ts b/web/crux/src/app/deploy/deploy.service.ts index 07ecc46346..a86b4e0c74 100644 --- a/web/crux/src/app/deploy/deploy.service.ts +++ b/web/crux/src/app/deploy/deploy.service.ts @@ -35,8 +35,10 @@ import { DeploymentDto, DeploymentEventDto, DeploymentImageEvent, + DeploymentListDto, DeploymentLogListDto, DeploymentLogPaginationQuery, + DeploymentQueryDto, DeploymentTokenCreatedDto, EnvironmentToConfigBundleNameMap, InstanceDto, @@ -835,44 +837,99 @@ export default class DeployService { return this.deploymentImageEvents.pipe(filter(it => it.deploymentIds.includes(deploymentId))) } - async getDeployments(teamSlug: string, nodeId?: string): Promise { - const deployments = await this.prisma.deployment.findMany({ - where: { - version: { - project: { - team: { - slug: teamSlug, - }, + async getDeployments(teamSlug: string, query?: DeploymentQueryDto): Promise { + let where: Prisma.DeploymentWhereInput = { + version: { + project: { + team: { + slug: teamSlug, }, }, - nodeId, }, - include: { - version: { - select: { - id: true, - name: true, - type: true, - project: { - select: { - id: true, - name: true, - type: true, + nodeId: query?.nodeId, + status: query?.status ? this.mapper.statusDtoToDb(query.status) : undefined, + } + + if (query.filter) { + const filter = query.filter + where = { + ...where, + OR: [ + { + prefix: { + contains: filter, + mode: 'insensitive', + }, + }, + { + node: { + name: { + contains: filter, + mode: 'insensitive', + }, + }, + }, + { + version: { + name: { + contains: filter, + mode: 'insensitive', + }, + }, + }, + { + version: { + project: { + name: { + contains: filter, + mode: 'insensitive', + }, }, }, }, + ], + } + } + + const [deployments, total] = await this.prisma.$transaction([ + this.prisma.deployment.findMany({ + where, + orderBy: { + createdAt: 'desc', }, - node: { - select: { - id: true, - name: true, - type: true, + skip: query?.skip, + take: query?.take, + include: { + version: { + select: { + id: true, + name: true, + type: true, + project: { + select: { + id: true, + name: true, + type: true, + }, + }, + }, + }, + node: { + select: { + id: true, + name: true, + type: true, + }, }, }, - }, - }) + }), + this.prisma.deployment.count({ where }), + ]) - return deployments.map(it => this.mapper.toDto(it)) + return { + items: deployments.map(it => this.mapper.toDto(it)), + total, + } } async getInstanceSecrets(instanceId: string): Promise { diff --git a/web/crux/src/app/node/node.http.controller.ts b/web/crux/src/app/node/node.http.controller.ts index cb23483d09..7393d554ba 100644 --- a/web/crux/src/app/node/node.http.controller.ts +++ b/web/crux/src/app/node/node.http.controller.ts @@ -271,7 +271,11 @@ export default class NodeHttpController { }) @ApiForbiddenResponse({ description: 'Unauthorized request for deployments.' }) async getDeployments(@TeamSlug() teamSlug: string, @NodeId() nodeId: string): Promise { - return await this.deployService.getDeployments(teamSlug, nodeId) + const paged = await this.deployService.getDeployments(teamSlug, { + nodeId, + }) + + return paged.items } @Post(`${ROUTE_NODE_ID}/kick`) From 95e13d6b659a23518c58f4f22937623bf908edd1 Mon Sep 17 00:00:00 2001 From: Mate Vago Date: Wed, 27 Nov 2024 12:20:09 +0100 Subject: [PATCH 07/32] fix: migration --- web/crux/src/app/image/image.const.ts | 1 - 1 file changed, 1 deletion(-) delete mode 100644 web/crux/src/app/image/image.const.ts diff --git a/web/crux/src/app/image/image.const.ts b/web/crux/src/app/image/image.const.ts deleted file mode 100644 index 40d785a1b6..0000000000 --- a/web/crux/src/app/image/image.const.ts +++ /dev/null @@ -1 +0,0 @@ -// TODO: move this to the container domain From 54e16d02f07c4da0420011661af2c68bb8aaae5a Mon Sep 17 00:00:00 2001 From: Mate Vago Date: Thu, 28 Nov 2024 08:47:02 +0100 Subject: [PATCH 08/32] fix: trying to fix config-bundle related tests --- web/crux-ui/e2e/utils/config-bundle.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/web/crux-ui/e2e/utils/config-bundle.ts b/web/crux-ui/e2e/utils/config-bundle.ts index 4130beb1b3..571fd25492 100644 --- a/web/crux-ui/e2e/utils/config-bundle.ts +++ b/web/crux-ui/e2e/utils/config-bundle.ts @@ -18,17 +18,22 @@ export const createConfigBundle = async (page: Page, name: string, data: Record< await expect(page.locator('h4')).toContainText('New config bundle') await page.locator('input[name=name] >> visible=true').fill(name) - const sock = waitSocketRef(page) await page.locator('text=Save').click() await page.waitForURL(`${TEAM_ROUTES.configBundle.list()}/**`) - await page.waitForSelector(`h4:text-is("View ${name}")`) + await page.waitForSelector(`h3:text-is("${name}")`) const configBundleId = page.url().split('/').pop() + const sock = waitSocketRef(page) + await page.locator('button:has-text("Config")').click() + await page.waitForURL(TEAM_ROUTES.containerConfig.details('**')) + + const configId = page.url().split('/').pop() + const ws = await sock - const wsRoute = TEAM_ROUTES.configBundle.detailsSocket(configBundleId) + const wsRoute = TEAM_ROUTES.configBundle.detailsSocket(configId) - await page.locator('button:has-text("Edit")').click() + await page.locator('button:has-text("Environment")').click() const wsPatchReceived = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, matchPatchEnvironment(data)) From 4badf495873ce635b36758a409bb363deb803100 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20Nagygy=C3=B6rgy?= Date: Thu, 28 Nov 2024 11:47:51 +0100 Subject: [PATCH 09/32] fix: lint --- web/crux-ui/src/pages/[teamSlug]/deployments.tsx | 3 +-- web/crux/src/app/deploy/deploy.service.ts | 10 +++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/web/crux-ui/src/pages/[teamSlug]/deployments.tsx b/web/crux-ui/src/pages/[teamSlug]/deployments.tsx index 24a93a550c..dd014d9409 100644 --- a/web/crux-ui/src/pages/[teamSlug]/deployments.tsx +++ b/web/crux-ui/src/pages/[teamSlug]/deployments.tsx @@ -5,7 +5,6 @@ import useCopyDeploymentState from '@app/components/deployments/use-copy-deploym import { Layout } from '@app/components/layout' import SelectNodeChips from '@app/components/nodes/select-node-chips' import { BreadcrumbLink } from '@app/components/shared/breadcrumb' -import Filters from '@app/components/shared/filters' import PageHeading from '@app/components/shared/page-heading' import { PaginationSettings } from '@app/components/shared/paginator' import DyoButton from '@app/elements/dyo-button' @@ -81,7 +80,7 @@ const DeploymentsPage = () => { take: pagination.pageSize, filter: !filter.filter || filter.filter.trim() === '' ? null : filter.filter, nodeId: filterNode?.id ?? null, - status: status, + status, } const res = await fetch(routes.deployment.api.list(query)) diff --git a/web/crux/src/app/deploy/deploy.service.ts b/web/crux/src/app/deploy/deploy.service.ts index a86b4e0c74..2f4b5abb79 100644 --- a/web/crux/src/app/deploy/deploy.service.ts +++ b/web/crux/src/app/deploy/deploy.service.ts @@ -851,20 +851,20 @@ export default class DeployService { } if (query.filter) { - const filter = query.filter + const { filter: filterKeyword } = query where = { ...where, OR: [ { prefix: { - contains: filter, + contains: filterKeyword, mode: 'insensitive', }, }, { node: { name: { - contains: filter, + contains: filterKeyword, mode: 'insensitive', }, }, @@ -872,7 +872,7 @@ export default class DeployService { { version: { name: { - contains: filter, + contains: filterKeyword, mode: 'insensitive', }, }, @@ -881,7 +881,7 @@ export default class DeployService { version: { project: { name: { - contains: filter, + contains: filterKeyword, mode: 'insensitive', }, }, From 20ff4481c6a32d91a88920e12b1d7ce29faf0d49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20Nagygy=C3=B6rgy?= Date: Thu, 28 Nov 2024 13:32:42 +0100 Subject: [PATCH 10/32] fix: merge --- web/crux/src/app/deploy/deploy.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web/crux/src/app/deploy/deploy.service.ts b/web/crux/src/app/deploy/deploy.service.ts index 15b9424ccf..750c281f47 100644 --- a/web/crux/src/app/deploy/deploy.service.ts +++ b/web/crux/src/app/deploy/deploy.service.ts @@ -51,6 +51,7 @@ import { DeploymentDetailsDto, DeploymentDto, DeploymentEventDto, + DeploymentListDto, DeploymentLogListDto, DeploymentLogPaginationQuery, DeploymentQueryDto, From 8a28439e51b0ef646f5eac800cedd25a1129fdec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20Nagygy=C3=B6rgy?= Date: Thu, 28 Nov 2024 13:45:51 +0100 Subject: [PATCH 11/32] feat: deploy list by configbundle --- web/crux/src/app/deploy/deploy.dto.ts | 6 ++++++ web/crux/src/app/deploy/deploy.service.ts | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/web/crux/src/app/deploy/deploy.dto.ts b/web/crux/src/app/deploy/deploy.dto.ts index 2dd0fe59b6..00e81a1435 100644 --- a/web/crux/src/app/deploy/deploy.dto.ts +++ b/web/crux/src/app/deploy/deploy.dto.ts @@ -62,6 +62,12 @@ export class DeploymentQueryDto { @ApiProperty({ enum: DEPLOYMENT_STATUS_VALUES }) @IsIn(DEPLOYMENT_STATUS_VALUES) readonly status?: DeploymentStatusDto + + @IsOptional() + @IsString() + @Type(() => String) + @ApiProperty() + readonly configBundleId?: string } export class BasicDeploymentDto { diff --git a/web/crux/src/app/deploy/deploy.service.ts b/web/crux/src/app/deploy/deploy.service.ts index 750c281f47..eba6e59b15 100644 --- a/web/crux/src/app/deploy/deploy.service.ts +++ b/web/crux/src/app/deploy/deploy.service.ts @@ -818,6 +818,17 @@ export default class DeployService { status: query?.status ? this.mapper.statusDtoToDb(query.status) : undefined, } + if (!!query.configBundleId) { + where = { + ...where, + configBundles: { + some: { + configBundleId: query.configBundleId, + } + } + } + } + if (query.filter) { const { filter: filterKeyword } = query where = { From 66bfe63c7a3be05d47e39862c3bb581196706dc6 Mon Sep 17 00:00:00 2001 From: robot9706 Date: Thu, 28 Nov 2024 16:32:33 +0100 Subject: [PATCH 12/32] feat: config bundle deployment list --- web/crux-ui/src/models/deployment.ts | 1 + .../config-bundles/[configBundleId].tsx | 91 ++++++++++++++++++- web/crux-ui/src/routes.ts | 4 + 3 files changed, 92 insertions(+), 4 deletions(-) diff --git a/web/crux-ui/src/models/deployment.ts b/web/crux-ui/src/models/deployment.ts index 2a74b12f19..bab857e667 100644 --- a/web/crux-ui/src/models/deployment.ts +++ b/web/crux-ui/src/models/deployment.ts @@ -34,6 +34,7 @@ export type DeploymentQuery = PaginationQuery & { nodeId?: string filter?: string status?: DeploymentStatus + configBundleId?: string } export type DeploymentList = PaginatedList diff --git a/web/crux-ui/src/pages/[teamSlug]/config-bundles/[configBundleId].tsx b/web/crux-ui/src/pages/[teamSlug]/config-bundles/[configBundleId].tsx index 134273bb00..1368d460c9 100644 --- a/web/crux-ui/src/pages/[teamSlug]/config-bundles/[configBundleId].tsx +++ b/web/crux-ui/src/pages/[teamSlug]/config-bundles/[configBundleId].tsx @@ -1,20 +1,28 @@ import ConfigBundleCard from '@app/components/config-bundles/config-bundle-card' import EditConfigBundleCard from '@app/components/config-bundles/edit-config-bundle-card' +import DeploymentStatusTag from '@app/components/deployments/deployment-status-tag' import { Layout } from '@app/components/layout' import { BreadcrumbLink } from '@app/components/shared/breadcrumb' import PageHeading from '@app/components/shared/page-heading' import { DetailsPageMenu } from '@app/components/shared/page-menu' +import { PaginationSettings } from '@app/components/shared/paginator' +import { DyoCard } from '@app/elements/dyo-card' +import DyoIcon from '@app/elements/dyo-icon' +import DyoLink from '@app/elements/dyo-link' +import DyoTable, { DyoColumn, sortDate, sortEnum, sortString } from '@app/elements/dyo-table' import { defaultApiErrorHandler } from '@app/errors' import useSubmit from '@app/hooks/use-submit' import useTeamRoutes from '@app/hooks/use-team-routes' -import { ConfigBundleDetails, detailsToConfigBundle } from '@app/models' +import { ConfigBundleDetails, Deployment, DEPLOYMENT_STATUS_VALUES, DeploymentList, DeploymentQuery, detailsToConfigBundle } from '@app/models' import { TeamRoutes } from '@app/routes' -import { withContextAuthorization } from '@app/utils' +import { auditToLocaleDate, withContextAuthorization } from '@app/utils' import { getCruxFromContext } from '@server/crux-api' import { GetServerSidePropsContext } from 'next' import useTranslation from 'next-translate/useTranslation' import { useRouter } from 'next/router' -import { useState } from 'react' +import { useEffect, useState } from 'react' + +const defaultPagination: PaginationSettings = { pageNumber: 0, pageSize: 10 } type ConfigBundleDetailsPageProps = { configBundle: ConfigBundleDetails @@ -34,6 +42,11 @@ const ConfigBundleDetailsPage = (props: ConfigBundleDetailsPageProps) => { const submit = useSubmit() + const [pagination, setPagination] = useState(defaultPagination) + + const [deployments, setDeployments] = useState([]) + const [total, setTotal] = useState(0) + const onDelete = async () => { const res = await fetch(routes.configBundle.api.details(configBundle.id), { method: 'DELETE', @@ -46,6 +59,30 @@ const ConfigBundleDetailsPage = (props: ConfigBundleDetailsPageProps) => { } } + + const fetchData = async () => { + const query: DeploymentQuery = { + skip: pagination.pageNumber * pagination.pageSize, + take: pagination.pageSize, + configBundleId: propsConfigBundle.id, + } + + const res = await fetch(routes.deployment.api.list(query)) + + if (res.ok) { + const list = (await res.json()) as DeploymentList + setDeployments(list.items) + setTotal(list.total) + } else { + setDeployments([]) + } + } + + useEffect(() => { + fetchData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [pagination]) + const pageLink: BreadcrumbLink = { name: t('common:configBundles'), url: routes.configBundle.list(), @@ -78,7 +115,53 @@ const ConfigBundleDetailsPage = (props: ConfigBundleDetailsPageProps) => { ) : ( )} - TODO deployment list and config + + + + + + + it.audit.updatedAt ?? it.audit.createdAt} + sort={sortDate} + body={(it: Deployment) => auditToLocaleDate(it.audit)} + /> + } + /> + ( + + + + )} + /> + + ) } diff --git a/web/crux-ui/src/routes.ts b/web/crux-ui/src/routes.ts index bdf84ee2ea..99b505fc36 100644 --- a/web/crux-ui/src/routes.ts +++ b/web/crux-ui/src/routes.ts @@ -123,6 +123,10 @@ const appendUrlParams = (url: string, params?: AnchorUrlParams): string => { } const urlQuery = (url: string, query: object) => { + if (!query) { + return url + } + const params = Object.entries(query) .map(it => { const [key, value] = it From 2a20744f0503c120c5f2466c4c1c5891e23c4f52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20Nagygy=C3=B6rgy?= Date: Fri, 29 Nov 2024 11:04:19 +0100 Subject: [PATCH 13/32] feat: use pagination --- .../projects/versions/version-view-list.tsx | 3 +- web/crux-ui/src/hooks/use-pagination.ts | 12 ++- .../config-bundles/[configBundleId].tsx | 66 ++++++++-------- .../src/pages/[teamSlug]/dashboard.tsx | 2 +- .../src/pages/[teamSlug]/deployments.tsx | 79 ++++++++++--------- 5 files changed, 88 insertions(+), 74 deletions(-) diff --git a/web/crux-ui/src/components/projects/versions/version-view-list.tsx b/web/crux-ui/src/components/projects/versions/version-view-list.tsx index 48a71581f9..6a8b19359b 100644 --- a/web/crux-ui/src/components/projects/versions/version-view-list.tsx +++ b/web/crux-ui/src/components/projects/versions/version-view-list.tsx @@ -67,7 +67,8 @@ const VersionViewList = (props: VersionViewListProps) => { sortField={containerNameOfImage} body={containerNameOfImage} sortable - sort={sortString} /> + sort={sortString} + /> = { defaultSettings: PaginationSettings @@ -9,13 +9,18 @@ export type PaginationOptions = { export type DispatchPagination = Dispatch> +export type RefreshPageFunc = () => void + export type Pagination = { total: number settings: PaginationSettings data: T[] } -const usePagination = (options: PaginationOptions): [Pagination, DispatchPagination] => { +const usePagination = ( + options: PaginationOptions, + deps?: DependencyList, +): [Pagination, DispatchPagination, RefreshPageFunc] => { const [total, setTotal] = useState(0) const [settings, setSettings] = useState(options.defaultSettings) const [data, setData] = useState(null) @@ -39,7 +44,7 @@ const usePagination = (options: PaginationOptions): [Pagination, Dispat setData(list.items) setTotal(list.total) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [settings]) + }, [settings, ...(deps ?? [])]) useEffect(() => { // eslint-disable-next-line @typescript-eslint/no-floating-promises @@ -54,6 +59,7 @@ const usePagination = (options: PaginationOptions): [Pagination, Dispat total, }, setSettings, + onFetchData, ] } diff --git a/web/crux-ui/src/pages/[teamSlug]/config-bundles/[configBundleId].tsx b/web/crux-ui/src/pages/[teamSlug]/config-bundles/[configBundleId].tsx index 1368d460c9..c07b41f346 100644 --- a/web/crux-ui/src/pages/[teamSlug]/config-bundles/[configBundleId].tsx +++ b/web/crux-ui/src/pages/[teamSlug]/config-bundles/[configBundleId].tsx @@ -11,16 +11,25 @@ import DyoIcon from '@app/elements/dyo-icon' import DyoLink from '@app/elements/dyo-link' import DyoTable, { DyoColumn, sortDate, sortEnum, sortString } from '@app/elements/dyo-table' import { defaultApiErrorHandler } from '@app/errors' +import usePagination from '@app/hooks/use-pagination' import useSubmit from '@app/hooks/use-submit' import useTeamRoutes from '@app/hooks/use-team-routes' -import { ConfigBundleDetails, Deployment, DEPLOYMENT_STATUS_VALUES, DeploymentList, DeploymentQuery, detailsToConfigBundle } from '@app/models' +import { + ConfigBundleDetails, + Deployment, + DEPLOYMENT_STATUS_VALUES, + DeploymentQuery, + detailsToConfigBundle, + PaginatedList, + PaginationQuery, +} from '@app/models' import { TeamRoutes } from '@app/routes' import { auditToLocaleDate, withContextAuthorization } from '@app/utils' import { getCruxFromContext } from '@server/crux-api' import { GetServerSidePropsContext } from 'next' import useTranslation from 'next-translate/useTranslation' import { useRouter } from 'next/router' -import { useEffect, useState } from 'react' +import { useCallback, useState } from 'react' const defaultPagination: PaginationSettings = { pageNumber: 0, pageSize: 10 } @@ -42,10 +51,29 @@ const ConfigBundleDetailsPage = (props: ConfigBundleDetailsPageProps) => { const submit = useSubmit() - const [pagination, setPagination] = useState(defaultPagination) + const fetchData = useCallback( + async (paginationQuery: PaginationQuery): Promise> => { + const query: DeploymentQuery = { + ...paginationQuery, + configBundleId: propsConfigBundle.id, + } - const [deployments, setDeployments] = useState([]) - const [total, setTotal] = useState(0) + const res = await fetch(routes.deployment.api.list(query)) + + if (!res.ok) { + await onApiError(res) + return null + } + + return (await res.json()) as PaginatedList + }, + [routes, onApiError], + ) + + const [pagination, setPagination] = usePagination({ + defaultSettings: defaultPagination, + fetchData, + }) const onDelete = async () => { const res = await fetch(routes.configBundle.api.details(configBundle.id), { @@ -59,30 +87,6 @@ const ConfigBundleDetailsPage = (props: ConfigBundleDetailsPageProps) => { } } - - const fetchData = async () => { - const query: DeploymentQuery = { - skip: pagination.pageNumber * pagination.pageSize, - take: pagination.pageSize, - configBundleId: propsConfigBundle.id, - } - - const res = await fetch(routes.deployment.api.list(query)) - - if (res.ok) { - const list = (await res.json()) as DeploymentList - setDeployments(list.items) - setTotal(list.total) - } else { - setDeployments([]) - } - } - - useEffect(() => { - fetchData(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pagination]) - const pageLink: BreadcrumbLink = { name: t('common:configBundles'), url: routes.configBundle.list(), @@ -117,10 +121,10 @@ const ConfigBundleDetailsPage = (props: ConfigBundleDetailsPageProps) => { )} { const formatCount = (count: number) => Intl.NumberFormat(lang, { notation: 'compact' }).format(count) const statisticItem = (property: string, count: number) => ( - + { @@ -50,20 +53,16 @@ const DeploymentsPage = () => { const routes = useTeamRoutes() const router = useRouter() - const [deployments, setDeployments] = useState([]) - const [total, setTotal] = useState(0) + const handleApiError = defaultApiErrorHandler(t) + const [filter, setFilter] = useState({ filter: '', status: null, + node: null, }) - const [filterNode, setFilterNode] = useState(null) - - const [pagination, setPagination] = useState(defaultPagination) const [creating, setCreating] = useState(false) - const handleApiError = defaultApiErrorHandler(t) - const [showInfo, setShowInfo] = useState(null) const [copyDeploymentTarget, setCopyDeploymentTarget] = useCopyDeploymentState({ handleApiError, @@ -72,37 +71,41 @@ const DeploymentsPage = () => { const throttle = useThrottling(1000) - const fetchData = async () => { - const { status } = filter + const fetchData = useCallback( + async (paginationQuery: PaginationQuery): Promise> => { + const { filter: keywordFilter, node, status } = filter - const query: DeploymentQuery = { - skip: pagination.pageNumber * pagination.pageSize, - take: pagination.pageSize, - filter: !filter.filter || filter.filter.trim() === '' ? null : filter.filter, - nodeId: filterNode?.id ?? null, - status, - } + const query: DeploymentQuery = { + ...paginationQuery, + filter: !keywordFilter || keywordFilter.trim() === '' ? null : keywordFilter, + nodeId: node?.id ?? null, + status, + } - const res = await fetch(routes.deployment.api.list(query)) + const res = await fetch(routes.deployment.api.list(query)) - if (res.ok) { - const list = (await res.json()) as DeploymentList - setDeployments(list.items) - setTotal(list.total) - } else { - setDeployments([]) - } - } + if (!res.ok) { + await handleApiError(res) + return null + } - useEffect(() => { - throttle(fetchData) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [filter, filterNode]) + return (await res.json()) as PaginatedList + }, + [routes, handleApiError, filter], + ) + + const [pagination, setPagination, refreshPage] = usePagination( + { + defaultSettings: defaultPagination, + fetchData, + }, + [filter], + ) useEffect(() => { - throttle(fetchData, true) + throttle(refreshPage) // eslint-disable-next-line react-hooks/exhaustive-deps - }, [pagination]) + }, [filter]) const selfLink: BreadcrumbLink = { name: t('common:deployments'), @@ -134,7 +137,7 @@ const DeploymentsPage = () => { return } - setDeployments([...deployments.filter(it => it.id !== deployment.id)]) + refreshPage() } const onDeploymentCopied = async (deploymentId: string) => { @@ -208,18 +211,18 @@ const DeploymentsPage = () => { className="mt-4" name="nodes" allowNull - onSelectionChange={async it => setFilterNode(it)} - selection={filterNode} + onSelectionChange={async it => setFilter({ ...filter, node: it })} + selection={filter.node} />
Date: Fri, 29 Nov 2024 13:43:15 +0100 Subject: [PATCH 14/32] fix: lint --- web/crux/src/app/deploy/deploy.service.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/crux/src/app/deploy/deploy.service.ts b/web/crux/src/app/deploy/deploy.service.ts index eba6e59b15..9203068624 100644 --- a/web/crux/src/app/deploy/deploy.service.ts +++ b/web/crux/src/app/deploy/deploy.service.ts @@ -818,14 +818,14 @@ export default class DeployService { status: query?.status ? this.mapper.statusDtoToDb(query.status) : undefined, } - if (!!query.configBundleId) { + if (query.configBundleId) { where = { ...where, configBundles: { some: { configBundleId: query.configBundleId, - } - } + }, + }, } } From c2b38c93a44a414e43b8ebe113a3861731cbb1eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20Nagygy=C3=B6rgy?= Date: Fri, 29 Nov 2024 14:04:36 +0100 Subject: [PATCH 15/32] fix: update test --- web/crux-ui/src/utils.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/crux-ui/src/utils.spec.ts b/web/crux-ui/src/utils.spec.ts index aa71dc33fb..706b0cf463 100644 --- a/web/crux-ui/src/utils.spec.ts +++ b/web/crux-ui/src/utils.spec.ts @@ -3,9 +3,9 @@ import { toNumber } from './utils' describe('toNumber() tests', () => { beforeEach(() => {}) - it('given string return NaN value', () => { + it('given string return null value', () => { const result = toNumber('test-string') - expect(result).toEqual(NaN) + expect(result).toEqual(null) }) it('given negative int return negative value', () => { From 4bf2c8f71ab865ec5508ed07cff3aae217d7f667 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20Nagygy=C3=B6rgy?= Date: Mon, 2 Dec 2024 08:09:21 +0100 Subject: [PATCH 16/32] fix: go lint --- golang/internal/mapper/grpc.go | 2 +- golang/internal/mapper/grpc_test.go | 6 +++--- golang/pkg/dagent/utils/docker.go | 3 +-- golang/pkg/dagent/utils/prefix_file.go | 20 ++++++++++---------- 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/golang/internal/mapper/grpc.go b/golang/internal/mapper/grpc.go index 0d05f0683c..c87a791a10 100644 --- a/golang/internal/mapper/grpc.go +++ b/golang/internal/mapper/grpc.go @@ -675,7 +675,7 @@ func MapDockerContainerEventToContainerState(event string) common.ContainerState } } -func MapContainerOrPrefixToPrefixName(target *common.ContainerOrPrefix) (string, string, error) { +func MapContainerOrPrefixToPrefixName(target *common.ContainerOrPrefix) (prefix, name string, err error) { if target == nil { return "", "", ErrNoTargetContainerOrPrefix } diff --git a/golang/internal/mapper/grpc_test.go b/golang/internal/mapper/grpc_test.go index 1189e5af3d..c6531b5e84 100644 --- a/golang/internal/mapper/grpc_test.go +++ b/golang/internal/mapper/grpc_test.go @@ -24,7 +24,7 @@ func TestMapDeployImageRequest(t *testing.T) { req := testDeployRequest() cfg := testAppConfig() - res := MapDeployImage(req, cfg) + res := MapDeployImage("", req, cfg) expected := testExpectedCommon(req) assert.Equal(t, expected, res) @@ -52,7 +52,7 @@ func TestMapDeployImageRequestRestartPolicies(t *testing.T) { for _, tC := range cases { req.Dagent.RestartPolicy = tC.policy expected.ContainerConfig.RestartPolicy = tC.dockerType - res := MapDeployImage(req, cfg) + res := MapDeployImage("", req, cfg) assert.Equal(t, expected, res) } } @@ -508,7 +508,7 @@ func TestMapDeployImageLogConfig(t *testing.T) { req := testDeployRequestWithLogDriver(tC.driver) cfg := testAppConfig() - res := MapDeployImage(req, cfg) + res := MapDeployImage("", req, cfg) expected := testExpectedCommonWithLogConfigType(req, tC.want) assert.Equal(t, expected, res) diff --git a/golang/pkg/dagent/utils/docker.go b/golang/pkg/dagent/utils/docker.go index 6c32fc8819..d4c7743fbc 100644 --- a/golang/pkg/dagent/utils/docker.go +++ b/golang/pkg/dagent/utils/docker.go @@ -365,7 +365,6 @@ func DeployImage(ctx context.Context, pf := NewSharedEnvPrefixFile(cfg.InternalMountPath, prefix) if len(deployImageRequest.InstanceConfig.SharedEnvironment) > 0 { - err = pf.WriteVariables(deployImageRequest.InstanceConfig.SharedEnvironment) if err != nil { dog.WriteError("could not write shared environment variables, aborting...", err.Error()) @@ -724,7 +723,7 @@ func setImageLabels(expandedImageName string, return labels, nil } -func SecretList(ctx context.Context, prefix string, name string) ([]string, error) { +func SecretList(ctx context.Context, prefix, name string) ([]string, error) { if name == "" { cfg := grpc.GetConfigFromContext(ctx).(*config.Configuration) diff --git a/golang/pkg/dagent/utils/prefix_file.go b/golang/pkg/dagent/utils/prefix_file.go index 1f5a1587fc..09754b668e 100644 --- a/golang/pkg/dagent/utils/prefix_file.go +++ b/golang/pkg/dagent/utils/prefix_file.go @@ -24,29 +24,29 @@ func NewErrPrefixFileParamEmpty(param string) PrefixFileParamError { return PrefixFileParamError{variable: param} } -type prefixFile struct { +type PrefixFile struct { DataRoot string Prefix string FileName string } -func NewSharedEnvPrefixFile(dataRoot, prefix string) prefixFile { - return prefixFile{ +func NewSharedEnvPrefixFile(dataRoot, prefix string) PrefixFile { + return PrefixFile{ DataRoot: dataRoot, Prefix: prefix, FileName: ".shared-env", } } -func NewSecretsPrefixFile(dataRoot, prefix string) prefixFile { - return prefixFile{ +func NewSecretsPrefixFile(dataRoot, prefix string) PrefixFile { + return PrefixFile{ DataRoot: dataRoot, Prefix: prefix, FileName: ".shared-secrets", } } -func (pf *prefixFile) validatePath() error { +func (pf *PrefixFile) validatePath() error { if pf.DataRoot == "" { return NewErrPrefixFileParamEmpty("dataRoot") } else if pf.Prefix == "" { @@ -56,15 +56,15 @@ func (pf *prefixFile) validatePath() error { return nil } -func (pf *prefixFile) getDirectory() string { +func (pf *PrefixFile) getDirectory() string { return filepath.Join(pf.DataRoot, pf.Prefix) } -func (pf *prefixFile) getFilePath() string { +func (pf *PrefixFile) getFilePath() string { return filepath.Join(pf.getDirectory(), pf.FileName) } -func (pf *prefixFile) ReadVariables() (map[string]string, error) { +func (pf *PrefixFile) ReadVariables() (map[string]string, error) { err := pf.validatePath() if err != nil { return nil, err @@ -84,7 +84,7 @@ func (pf *prefixFile) ReadVariables() (map[string]string, error) { return godotenv.UnmarshalBytes(file) } -func (pf *prefixFile) WriteVariables(in map[string]string) error { +func (pf *PrefixFile) WriteVariables(in map[string]string) error { var err error err = pf.validatePath() if err != nil { From c5f654f67193f107c61f9a81bff0c2103d6ff739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20Nagygy=C3=B6rgy?= Date: Mon, 2 Dec 2024 16:24:36 +0100 Subject: [PATCH 17/32] fix: deploy bug & separate config schema --- .../edit-container-config-card.tsx | 4 +-- .../container-configurations/[configId].tsx | 14 ++------- web/crux-ui/src/validations/container.ts | 29 ++++++++++++------- web/crux-ui/src/validations/deployment.ts | 4 +-- web/crux-ui/src/validations/image.ts | 4 +-- web/crux-ui/src/validations/index.ts | 1 - web/crux-ui/src/validations/instance.ts | 12 -------- web/crux/src/app/image/image.service.ts | 7 ++++- web/crux/src/domain/start-deployment.ts | 4 +++ 9 files changed, 37 insertions(+), 42 deletions(-) delete mode 100644 web/crux-ui/src/validations/instance.ts diff --git a/web/crux-ui/src/components/container-configs/edit-container-config-card.tsx b/web/crux-ui/src/components/container-configs/edit-container-config-card.tsx index 3564fde5ee..8771e25633 100644 --- a/web/crux-ui/src/components/container-configs/edit-container-config-card.tsx +++ b/web/crux-ui/src/components/container-configs/edit-container-config-card.tsx @@ -10,7 +10,7 @@ import DyoMessage from '@app/elements/dyo-message' import { DyoConfirmationModal } from '@app/elements/dyo-modal' import useConfirmation from '@app/hooks/use-confirmation' import { ContainerConfig, ContainerConfigParent, VersionImage } from '@app/models' -import { createContainerConfigSchema, getValidationError } from '@app/validations' +import { createConfigSchema, getValidationError } from '@app/validations' import useTranslation from 'next-translate/useTranslation' import { QA_DIALOG_LABEL_DELETE_IMAGE } from 'quality-assurance' import ContainerConfigJsonEditor from './container-config-json-editor' @@ -74,7 +74,7 @@ const EditContainerConfigCard = (props: Ed const errorMessage = state.parseError ?? - getValidationError(createContainerConfigSchema(image?.labels ?? {}), state.config, null, t)?.message + getValidationError(createConfigSchema("image", image?.labels ?? {}), state.config, null, t)?.message return ( <> diff --git a/web/crux-ui/src/pages/[teamSlug]/container-configurations/[configId].tsx b/web/crux-ui/src/pages/[teamSlug]/container-configurations/[configId].tsx index ac1fad5f21..dae84c7e14 100644 --- a/web/crux-ui/src/pages/[teamSlug]/container-configurations/[configId].tsx +++ b/web/crux-ui/src/pages/[teamSlug]/container-configurations/[configId].tsx @@ -50,8 +50,8 @@ import { TeamRoutes } from '@app/routes' import { withContextAuthorization } from '@app/utils' import { ContainerConfigValidationErrors, - getConcreteContainerConfigFieldErrors, - getContainerConfigFieldErrors, + createConfigSchema, + getConfigFieldErrorsForSchema, jsonErrorOf, } from '@app/validations' import { WsMessage } from '@app/websockets/common' @@ -134,15 +134,7 @@ const getConfigErrors = ( config: ContainerConfigDetails, imageLabels: Record, t: Translate, -): ContainerConfigValidationErrors => { - const type = containerConfigTypeToSectionType(config.type) - - if (type === 'concrete') { - return getConcreteContainerConfigFieldErrors(config as ConcreteContainerConfigData, imageLabels, t) - } - - return getContainerConfigFieldErrors(config, imageLabels, t) -} +): ContainerConfigValidationErrors => getConfigFieldErrorsForSchema(createConfigSchema(config.type, imageLabels), config, t) const getBaseConfig = (config: ContainerConfigDetails, relations: ContainerConfigRelations): ContainerConfigData => { switch (config.type) { diff --git a/web/crux-ui/src/validations/container.ts b/web/crux-ui/src/validations/container.ts index 4cf4f6f3dc..ea89de8cd7 100644 --- a/web/crux-ui/src/validations/container.ts +++ b/web/crux-ui/src/validations/container.ts @@ -18,6 +18,7 @@ import { Metrics, UniqueKeyValue, VolumeType, + ContainerConfigType, } from '@app/models' import * as yup from 'yup' import { matchNoLeadingOrTrailingWhitespaces, matchNoWhitespace } from './common' @@ -499,9 +500,11 @@ const testEnvironment = (imageLabels: Record) => (arr: UniqueKey return true } -const createContainerConfigBaseSchema = (imageLabels: Record) => +const createContainerConfigBaseSchema = (imageLabels: Record, nameRequired: boolean, secretsHaveValue: boolean) => yup.object().shape({ - name: matchContainerName(yup.string().required().label('container:common.containerName')), + name: matchContainerName(nameRequired + ? yup.string().required().label('container:common.containerName') + : yup.string().optional().nullable().label('container:common.containerName')), environment: uniqueKeyValuesSchema .default(null) .nullable() @@ -522,6 +525,7 @@ const createContainerConfigBaseSchema = (imageLabels: Record) => initContainers: initContainerRule, capabilities: uniqueKeyValuesSchema.default(null).nullable().optional().label('container:common.capabilities'), storage: storageRule, + secrets: (secretsHaveValue ? uniqueKeyValuesSchema : uniqueKeySchema).default(null).nullable().optional().label('container:common.secrets'), // dagent: logConfig: logConfigRule, @@ -548,12 +552,15 @@ const createContainerConfigBaseSchema = (imageLabels: Record) => metrics: metricsRule, }) -export const createContainerConfigSchema = (imageLabels: Record) => - createContainerConfigBaseSchema(imageLabels).shape({ - secrets: uniqueKeySchema.default(null).nullable().optional().label('container:common.secrets'), - }) - -export const createConcreteContainerConfigSchema = (imageLabels: Record) => - createContainerConfigBaseSchema(imageLabels).shape({ - secrets: uniqueKeyValuesSchema.default(null).nullable().optional().label('container:common.secrets'), - }) +export const createConfigSchema = (type: ContainerConfigType, imageLabels: Record) => { + /** + * image/instance should require a container name + * bundle/deployment should not + * + * config bundles should also be able to store secret values (no agent key though...) + */ + + const nameRequired = (type === "image" || type === "instance") + const secretsHaveValue = (type === "image" || type === "deployment") + return createContainerConfigBaseSchema(imageLabels, nameRequired, secretsHaveValue) +} diff --git a/web/crux-ui/src/validations/deployment.ts b/web/crux-ui/src/validations/deployment.ts index 88f9eeca76..99f14cbdd9 100644 --- a/web/crux-ui/src/validations/deployment.ts +++ b/web/crux-ui/src/validations/deployment.ts @@ -1,6 +1,6 @@ import yup from './yup' import { nameRule } from './common' -import { createConcreteContainerConfigSchema, uniqueKeyValuesSchema } from './container' +import { createConfigSchema, uniqueKeyValuesSchema } from './container' export const prefixRule = yup .string() @@ -41,7 +41,7 @@ export const startDeploymentSchema = yup.object({ instances: yup .array( yup.object().shape({ - config: createConcreteContainerConfigSchema(null), + config: createConfigSchema("instance", null), }), ) .ensure() diff --git a/web/crux-ui/src/validations/image.ts b/web/crux-ui/src/validations/image.ts index 1acd108e23..f2898368ca 100644 --- a/web/crux-ui/src/validations/image.ts +++ b/web/crux-ui/src/validations/image.ts @@ -1,8 +1,8 @@ import { ContainerConfigData } from '@app/models' import yup from './yup' import { ErrorWithPath, getValidationError } from './common' -import { createContainerConfigSchema } from './container' import { Translate } from 'next-translate' +import { createConfigSchema } from './container' export type ContainerConfigValidationErrors = Record @@ -23,7 +23,7 @@ export const getContainerConfigFieldErrors = ( imageLabels: Record, t: Translate, ): ContainerConfigValidationErrors => - getConfigFieldErrorsForSchema(createContainerConfigSchema(imageLabels), newConfig, t) + getConfigFieldErrorsForSchema(createConfigSchema("image", imageLabels), newConfig, t) export const jsonErrorOf = (fieldErrors: ContainerConfigValidationErrors) => { const entries = Object.entries(fieldErrors) diff --git a/web/crux-ui/src/validations/index.ts b/web/crux-ui/src/validations/index.ts index 3bc7b0beb9..0f2a2ccfe0 100644 --- a/web/crux-ui/src/validations/index.ts +++ b/web/crux-ui/src/validations/index.ts @@ -4,7 +4,6 @@ export * from './config-bundle' export * from './container' export * from './deployment' export * from './image' -export * from './instance' export * from './node' export * from './notification' export * from './pipeline' diff --git a/web/crux-ui/src/validations/instance.ts b/web/crux-ui/src/validations/instance.ts deleted file mode 100644 index e4f038f645..0000000000 --- a/web/crux-ui/src/validations/instance.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { ConcreteContainerConfigData } from '@app/models' -import { Translate } from 'next-translate' -import { getConfigFieldErrorsForSchema, ContainerConfigValidationErrors } from './image' -import { createConcreteContainerConfigSchema } from './container' - -// eslint-disable-next-line import/prefer-default-export -export const getConcreteContainerConfigFieldErrors = ( - newConfig: ConcreteContainerConfigData, - validation: Record, - t: Translate, -): ContainerConfigValidationErrors => - getConfigFieldErrorsForSchema(createConcreteContainerConfigSchema(validation), newConfig, t) diff --git a/web/crux/src/app/image/image.service.ts b/web/crux/src/app/image/image.service.ts index f658356a72..10dbb9399a 100644 --- a/web/crux/src/app/image/image.service.ts +++ b/web/crux/src/app/image/image.service.ts @@ -122,7 +122,12 @@ export default class ImageService { data: { registry: { connect: { id: registyImages.registryId } }, version: { connect: { id: versionId } }, - config: { create: { type: 'image' } }, + config: { + create: { + type: 'image', + name: imageName, + } + }, createdBy: identity.id, name: imageName, tag: imageTag, diff --git a/web/crux/src/domain/start-deployment.ts b/web/crux/src/domain/start-deployment.ts index 259df810ef..af52b93f3d 100644 --- a/web/crux/src/domain/start-deployment.ts +++ b/web/crux/src/domain/start-deployment.ts @@ -137,6 +137,10 @@ export const mergePrefixNeighborSecrets = ( deployments .sort((one, other) => other.createdAt.getTime() - one.createdAt.getTime()) .forEach(depl => { + if (!depl.config.secrets) { + return + } + const secrets = depl.config.secrets as UniqueSecretKeyValue[] secrets.forEach(it => { if (it.publicKey !== publicKey) { From a2482b8532b0ce549ce5cfba258352694e83c339 Mon Sep 17 00:00:00 2001 From: Mate Vago Date: Tue, 3 Dec 2024 12:15:01 +0100 Subject: [PATCH 18/32] Revert "fix: deploy bug & separate config schema" This reverts commit c5f654f67193f107c61f9a81bff0c2103d6ff739. --- .../edit-container-config-card.tsx | 4 +-- .../container-configurations/[configId].tsx | 14 +++++++-- web/crux-ui/src/validations/container.ts | 29 +++++++------------ web/crux-ui/src/validations/deployment.ts | 4 +-- web/crux-ui/src/validations/image.ts | 4 +-- web/crux-ui/src/validations/index.ts | 1 + web/crux-ui/src/validations/instance.ts | 12 ++++++++ web/crux/src/app/image/image.service.ts | 7 +---- web/crux/src/domain/start-deployment.ts | 4 --- 9 files changed, 42 insertions(+), 37 deletions(-) create mode 100644 web/crux-ui/src/validations/instance.ts diff --git a/web/crux-ui/src/components/container-configs/edit-container-config-card.tsx b/web/crux-ui/src/components/container-configs/edit-container-config-card.tsx index 8771e25633..3564fde5ee 100644 --- a/web/crux-ui/src/components/container-configs/edit-container-config-card.tsx +++ b/web/crux-ui/src/components/container-configs/edit-container-config-card.tsx @@ -10,7 +10,7 @@ import DyoMessage from '@app/elements/dyo-message' import { DyoConfirmationModal } from '@app/elements/dyo-modal' import useConfirmation from '@app/hooks/use-confirmation' import { ContainerConfig, ContainerConfigParent, VersionImage } from '@app/models' -import { createConfigSchema, getValidationError } from '@app/validations' +import { createContainerConfigSchema, getValidationError } from '@app/validations' import useTranslation from 'next-translate/useTranslation' import { QA_DIALOG_LABEL_DELETE_IMAGE } from 'quality-assurance' import ContainerConfigJsonEditor from './container-config-json-editor' @@ -74,7 +74,7 @@ const EditContainerConfigCard = (props: Ed const errorMessage = state.parseError ?? - getValidationError(createConfigSchema("image", image?.labels ?? {}), state.config, null, t)?.message + getValidationError(createContainerConfigSchema(image?.labels ?? {}), state.config, null, t)?.message return ( <> diff --git a/web/crux-ui/src/pages/[teamSlug]/container-configurations/[configId].tsx b/web/crux-ui/src/pages/[teamSlug]/container-configurations/[configId].tsx index dae84c7e14..ac1fad5f21 100644 --- a/web/crux-ui/src/pages/[teamSlug]/container-configurations/[configId].tsx +++ b/web/crux-ui/src/pages/[teamSlug]/container-configurations/[configId].tsx @@ -50,8 +50,8 @@ import { TeamRoutes } from '@app/routes' import { withContextAuthorization } from '@app/utils' import { ContainerConfigValidationErrors, - createConfigSchema, - getConfigFieldErrorsForSchema, + getConcreteContainerConfigFieldErrors, + getContainerConfigFieldErrors, jsonErrorOf, } from '@app/validations' import { WsMessage } from '@app/websockets/common' @@ -134,7 +134,15 @@ const getConfigErrors = ( config: ContainerConfigDetails, imageLabels: Record, t: Translate, -): ContainerConfigValidationErrors => getConfigFieldErrorsForSchema(createConfigSchema(config.type, imageLabels), config, t) +): ContainerConfigValidationErrors => { + const type = containerConfigTypeToSectionType(config.type) + + if (type === 'concrete') { + return getConcreteContainerConfigFieldErrors(config as ConcreteContainerConfigData, imageLabels, t) + } + + return getContainerConfigFieldErrors(config, imageLabels, t) +} const getBaseConfig = (config: ContainerConfigDetails, relations: ContainerConfigRelations): ContainerConfigData => { switch (config.type) { diff --git a/web/crux-ui/src/validations/container.ts b/web/crux-ui/src/validations/container.ts index ea89de8cd7..4cf4f6f3dc 100644 --- a/web/crux-ui/src/validations/container.ts +++ b/web/crux-ui/src/validations/container.ts @@ -18,7 +18,6 @@ import { Metrics, UniqueKeyValue, VolumeType, - ContainerConfigType, } from '@app/models' import * as yup from 'yup' import { matchNoLeadingOrTrailingWhitespaces, matchNoWhitespace } from './common' @@ -500,11 +499,9 @@ const testEnvironment = (imageLabels: Record) => (arr: UniqueKey return true } -const createContainerConfigBaseSchema = (imageLabels: Record, nameRequired: boolean, secretsHaveValue: boolean) => +const createContainerConfigBaseSchema = (imageLabels: Record) => yup.object().shape({ - name: matchContainerName(nameRequired - ? yup.string().required().label('container:common.containerName') - : yup.string().optional().nullable().label('container:common.containerName')), + name: matchContainerName(yup.string().required().label('container:common.containerName')), environment: uniqueKeyValuesSchema .default(null) .nullable() @@ -525,7 +522,6 @@ const createContainerConfigBaseSchema = (imageLabels: Record, na initContainers: initContainerRule, capabilities: uniqueKeyValuesSchema.default(null).nullable().optional().label('container:common.capabilities'), storage: storageRule, - secrets: (secretsHaveValue ? uniqueKeyValuesSchema : uniqueKeySchema).default(null).nullable().optional().label('container:common.secrets'), // dagent: logConfig: logConfigRule, @@ -552,15 +548,12 @@ const createContainerConfigBaseSchema = (imageLabels: Record, na metrics: metricsRule, }) -export const createConfigSchema = (type: ContainerConfigType, imageLabels: Record) => { - /** - * image/instance should require a container name - * bundle/deployment should not - * - * config bundles should also be able to store secret values (no agent key though...) - */ - - const nameRequired = (type === "image" || type === "instance") - const secretsHaveValue = (type === "image" || type === "deployment") - return createContainerConfigBaseSchema(imageLabels, nameRequired, secretsHaveValue) -} +export const createContainerConfigSchema = (imageLabels: Record) => + createContainerConfigBaseSchema(imageLabels).shape({ + secrets: uniqueKeySchema.default(null).nullable().optional().label('container:common.secrets'), + }) + +export const createConcreteContainerConfigSchema = (imageLabels: Record) => + createContainerConfigBaseSchema(imageLabels).shape({ + secrets: uniqueKeyValuesSchema.default(null).nullable().optional().label('container:common.secrets'), + }) diff --git a/web/crux-ui/src/validations/deployment.ts b/web/crux-ui/src/validations/deployment.ts index 99f14cbdd9..88f9eeca76 100644 --- a/web/crux-ui/src/validations/deployment.ts +++ b/web/crux-ui/src/validations/deployment.ts @@ -1,6 +1,6 @@ import yup from './yup' import { nameRule } from './common' -import { createConfigSchema, uniqueKeyValuesSchema } from './container' +import { createConcreteContainerConfigSchema, uniqueKeyValuesSchema } from './container' export const prefixRule = yup .string() @@ -41,7 +41,7 @@ export const startDeploymentSchema = yup.object({ instances: yup .array( yup.object().shape({ - config: createConfigSchema("instance", null), + config: createConcreteContainerConfigSchema(null), }), ) .ensure() diff --git a/web/crux-ui/src/validations/image.ts b/web/crux-ui/src/validations/image.ts index f2898368ca..1acd108e23 100644 --- a/web/crux-ui/src/validations/image.ts +++ b/web/crux-ui/src/validations/image.ts @@ -1,8 +1,8 @@ import { ContainerConfigData } from '@app/models' import yup from './yup' import { ErrorWithPath, getValidationError } from './common' +import { createContainerConfigSchema } from './container' import { Translate } from 'next-translate' -import { createConfigSchema } from './container' export type ContainerConfigValidationErrors = Record @@ -23,7 +23,7 @@ export const getContainerConfigFieldErrors = ( imageLabels: Record, t: Translate, ): ContainerConfigValidationErrors => - getConfigFieldErrorsForSchema(createConfigSchema("image", imageLabels), newConfig, t) + getConfigFieldErrorsForSchema(createContainerConfigSchema(imageLabels), newConfig, t) export const jsonErrorOf = (fieldErrors: ContainerConfigValidationErrors) => { const entries = Object.entries(fieldErrors) diff --git a/web/crux-ui/src/validations/index.ts b/web/crux-ui/src/validations/index.ts index 0f2a2ccfe0..3bc7b0beb9 100644 --- a/web/crux-ui/src/validations/index.ts +++ b/web/crux-ui/src/validations/index.ts @@ -4,6 +4,7 @@ export * from './config-bundle' export * from './container' export * from './deployment' export * from './image' +export * from './instance' export * from './node' export * from './notification' export * from './pipeline' diff --git a/web/crux-ui/src/validations/instance.ts b/web/crux-ui/src/validations/instance.ts new file mode 100644 index 0000000000..e4f038f645 --- /dev/null +++ b/web/crux-ui/src/validations/instance.ts @@ -0,0 +1,12 @@ +import { ConcreteContainerConfigData } from '@app/models' +import { Translate } from 'next-translate' +import { getConfigFieldErrorsForSchema, ContainerConfigValidationErrors } from './image' +import { createConcreteContainerConfigSchema } from './container' + +// eslint-disable-next-line import/prefer-default-export +export const getConcreteContainerConfigFieldErrors = ( + newConfig: ConcreteContainerConfigData, + validation: Record, + t: Translate, +): ContainerConfigValidationErrors => + getConfigFieldErrorsForSchema(createConcreteContainerConfigSchema(validation), newConfig, t) diff --git a/web/crux/src/app/image/image.service.ts b/web/crux/src/app/image/image.service.ts index 10dbb9399a..f658356a72 100644 --- a/web/crux/src/app/image/image.service.ts +++ b/web/crux/src/app/image/image.service.ts @@ -122,12 +122,7 @@ export default class ImageService { data: { registry: { connect: { id: registyImages.registryId } }, version: { connect: { id: versionId } }, - config: { - create: { - type: 'image', - name: imageName, - } - }, + config: { create: { type: 'image' } }, createdBy: identity.id, name: imageName, tag: imageTag, diff --git a/web/crux/src/domain/start-deployment.ts b/web/crux/src/domain/start-deployment.ts index af52b93f3d..259df810ef 100644 --- a/web/crux/src/domain/start-deployment.ts +++ b/web/crux/src/domain/start-deployment.ts @@ -137,10 +137,6 @@ export const mergePrefixNeighborSecrets = ( deployments .sort((one, other) => other.createdAt.getTime() - one.createdAt.getTime()) .forEach(depl => { - if (!depl.config.secrets) { - return - } - const secrets = depl.config.secrets as UniqueSecretKeyValue[] secrets.forEach(it => { if (it.publicKey !== publicKey) { From a780d10464cd6a6e153392352d2977202c45a8d9 Mon Sep 17 00:00:00 2001 From: Mate Vago Date: Tue, 3 Dec 2024 12:16:44 +0100 Subject: [PATCH 19/32] fix: deployment null secrets --- web/crux/src/domain/start-deployment.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/web/crux/src/domain/start-deployment.ts b/web/crux/src/domain/start-deployment.ts index 259df810ef..af52b93f3d 100644 --- a/web/crux/src/domain/start-deployment.ts +++ b/web/crux/src/domain/start-deployment.ts @@ -137,6 +137,10 @@ export const mergePrefixNeighborSecrets = ( deployments .sort((one, other) => other.createdAt.getTime() - one.createdAt.getTime()) .forEach(depl => { + if (!depl.config.secrets) { + return + } + const secrets = depl.config.secrets as UniqueSecretKeyValue[] secrets.forEach(it => { if (it.publicKey !== publicKey) { From e214b33f7c4ce94d4607fb089deb80e0d6ddda29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20Nagygy=C3=B6rgy?= Date: Tue, 3 Dec 2024 12:35:43 +0100 Subject: [PATCH 20/32] feat(web): specify pipeline target branch --- web/crux-ui/locales/en/common.json | 1 + .../src/components/pipelines/edit-pipeline-card.tsx | 13 +++++++++++++ web/crux-ui/src/models/pipeline.ts | 1 + web/crux-ui/src/validations/pipeline.ts | 1 + web/crux/src/app/pipeline/pipeline.dto.ts | 4 ++++ web/crux/src/app/pipeline/pipeline.mapper.ts | 2 ++ web/crux/src/domain/pipeline.ts | 10 ++++++++++ web/crux/src/services/azure-devops.service.ts | 12 ++++++++++++ 8 files changed, 44 insertions(+) diff --git a/web/crux-ui/locales/en/common.json b/web/crux-ui/locales/en/common.json index 841215e310..5808db1a97 100644 --- a/web/crux-ui/locales/en/common.json +++ b/web/crux-ui/locales/en/common.json @@ -113,6 +113,7 @@ "version": "Version", "pipeline": "Pipeline", "token": "Token", + "branch": "Branch", "changeToken": "Change token", diff --git a/web/crux-ui/src/components/pipelines/edit-pipeline-card.tsx b/web/crux-ui/src/components/pipelines/edit-pipeline-card.tsx index ffea49ffab..5e1ec11a37 100644 --- a/web/crux-ui/src/components/pipelines/edit-pipeline-card.tsx +++ b/web/crux-ui/src/components/pipelines/edit-pipeline-card.tsx @@ -33,6 +33,7 @@ const DEFAULT_EDITABLE_PIPELINE: EditablePipeline = { repository: { organization: '', project: '', + branch: null, }, trigger: { name: '', @@ -202,6 +203,18 @@ const EditPipelineCard = (props: EditPipelineCardProps) => { message={formik.errors.repository?.project} /> + + {editing && ( )} diff --git a/web/crux-ui/src/models/pipeline.ts b/web/crux-ui/src/models/pipeline.ts index 5cc1c2043a..edc2a108ee 100644 --- a/web/crux-ui/src/models/pipeline.ts +++ b/web/crux-ui/src/models/pipeline.ts @@ -11,6 +11,7 @@ export type PipelineRunStatus = 'unknown' | 'queued' | 'running' | 'successful' export type AzureRepository = { organization: string project: string + branch?: string } export type PipelineRepository = AzureRepository diff --git a/web/crux-ui/src/validations/pipeline.ts b/web/crux-ui/src/validations/pipeline.ts index a965b85b59..bd32ffeb26 100644 --- a/web/crux-ui/src/validations/pipeline.ts +++ b/web/crux-ui/src/validations/pipeline.ts @@ -14,6 +14,7 @@ export const updatePipelineSchema = yup.object().shape({ yup.object().shape({ organization: yup.string().required().label('organization'), project: yup.string().required().label('common:project'), + branch: yup.string().optional().nullable().label('common:branch'), }), }), trigger: yup.object().shape({ diff --git a/web/crux/src/app/pipeline/pipeline.dto.ts b/web/crux/src/app/pipeline/pipeline.dto.ts index 02775a63b2..4f9201b617 100644 --- a/web/crux/src/app/pipeline/pipeline.dto.ts +++ b/web/crux/src/app/pipeline/pipeline.dto.ts @@ -110,6 +110,10 @@ export class AzureDevOpsRepositoryDto { @IsString() project: string + + @IsString() + @IsOptional() + branch?: string } export class PipelineTriggerDto { diff --git a/web/crux/src/app/pipeline/pipeline.mapper.ts b/web/crux/src/app/pipeline/pipeline.mapper.ts index 4d8fb28763..a43c855ebb 100644 --- a/web/crux/src/app/pipeline/pipeline.mapper.ts +++ b/web/crux/src/app/pipeline/pipeline.mapper.ts @@ -77,6 +77,7 @@ export default class PipelineMapper { repository: { organization: repo.organization, project: repo.project, + branch: repo.branch, }, audit: this.auditMapper.toDto(pipeline), trigger: pipeline.trigger as AzureTrigger, @@ -185,6 +186,7 @@ export default class PipelineMapper { organization: it.organization, project: it.project, projectId, + branch: it.branch && it.branch.length > 0 ? it.branch : null, } } diff --git a/web/crux/src/domain/pipeline.ts b/web/crux/src/domain/pipeline.ts index f1cd2ef0db..5dc002137c 100644 --- a/web/crux/src/domain/pipeline.ts +++ b/web/crux/src/domain/pipeline.ts @@ -27,6 +27,7 @@ export type AzureRepository = { organization: string project: string projectId: string + branch?: string } export type AzureTrigger = { @@ -58,8 +59,17 @@ export type AzureDevOpsVariable = { value: string } +export type AzureDevOpsRespositoryParameters = { + refName: string +} + +export type AzureDevOpsResources = { + repositories?: Record +} + export type AzureDevOpsPipelineTrigger = { variables: Record + resources?: AzureDevOpsResources } export type AzureDevOpsPipelineTriggerError = { diff --git a/web/crux/src/services/azure-devops.service.ts b/web/crux/src/services/azure-devops.service.ts index ce46910545..7acbe3f957 100644 --- a/web/crux/src/services/azure-devops.service.ts +++ b/web/crux/src/services/azure-devops.service.ts @@ -9,6 +9,7 @@ import { AzureDevOpsPipelineTrigger, AzureDevOpsPipelineTriggerError, AzureDevOpsProject, + AzureDevOpsResources, AzureDevOpsRun, AzureDevOpsVariable, PipelineHookOptions, @@ -113,8 +114,19 @@ export default class AzureDevOpsService { return result }, {}) + const resources: AzureDevOpsResources = creds.repo.branch + ? { + repositories: { + self: { + refName: `refs/heads/${creds.repo.branch}`, + }, + }, + } + : null + const body: AzureDevOpsPipelineTrigger = { variables, + resources, } const res = await this.post(creds, `/pipelines/${pipeline.id}/runs`, body) From 677d17b0871c9221ae7edf7faaf060de1759d48a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20Nagygy=C3=B6rgy?= Date: Tue, 3 Dec 2024 13:12:26 +0100 Subject: [PATCH 21/32] fix: lint --- .../edit-container-config-card.tsx | 2 +- .../container-configurations/[configId].tsx | 3 ++- web/crux-ui/src/validations/container.ts | 24 +++++++++++++------ web/crux-ui/src/validations/deployment.ts | 2 +- web/crux-ui/src/validations/image.ts | 2 +- web/crux/src/app/image/image.service.ts | 2 +- 6 files changed, 23 insertions(+), 12 deletions(-) diff --git a/web/crux-ui/src/components/container-configs/edit-container-config-card.tsx b/web/crux-ui/src/components/container-configs/edit-container-config-card.tsx index 8771e25633..f5964ebe38 100644 --- a/web/crux-ui/src/components/container-configs/edit-container-config-card.tsx +++ b/web/crux-ui/src/components/container-configs/edit-container-config-card.tsx @@ -74,7 +74,7 @@ const EditContainerConfigCard = (props: Ed const errorMessage = state.parseError ?? - getValidationError(createConfigSchema("image", image?.labels ?? {}), state.config, null, t)?.message + getValidationError(createConfigSchema('image', image?.labels ?? {}), state.config, null, t)?.message return ( <> diff --git a/web/crux-ui/src/pages/[teamSlug]/container-configurations/[configId].tsx b/web/crux-ui/src/pages/[teamSlug]/container-configurations/[configId].tsx index dae84c7e14..21b92ad4fc 100644 --- a/web/crux-ui/src/pages/[teamSlug]/container-configurations/[configId].tsx +++ b/web/crux-ui/src/pages/[teamSlug]/container-configurations/[configId].tsx @@ -134,7 +134,8 @@ const getConfigErrors = ( config: ContainerConfigDetails, imageLabels: Record, t: Translate, -): ContainerConfigValidationErrors => getConfigFieldErrorsForSchema(createConfigSchema(config.type, imageLabels), config, t) +): ContainerConfigValidationErrors => + getConfigFieldErrorsForSchema(createConfigSchema(config.type, imageLabels), config, t) const getBaseConfig = (config: ContainerConfigDetails, relations: ContainerConfigRelations): ContainerConfigData => { switch (config.type) { diff --git a/web/crux-ui/src/validations/container.ts b/web/crux-ui/src/validations/container.ts index ea89de8cd7..7cdd4a271f 100644 --- a/web/crux-ui/src/validations/container.ts +++ b/web/crux-ui/src/validations/container.ts @@ -500,11 +500,17 @@ const testEnvironment = (imageLabels: Record) => (arr: UniqueKey return true } -const createContainerConfigBaseSchema = (imageLabels: Record, nameRequired: boolean, secretsHaveValue: boolean) => +const createContainerConfigBaseSchema = ( + imageLabels: Record, + nameRequired: boolean, + secretsHaveValue: boolean, +) => yup.object().shape({ - name: matchContainerName(nameRequired - ? yup.string().required().label('container:common.containerName') - : yup.string().optional().nullable().label('container:common.containerName')), + name: matchContainerName( + nameRequired + ? yup.string().required().label('container:common.containerName') + : yup.string().optional().nullable().label('container:common.containerName'), + ), environment: uniqueKeyValuesSchema .default(null) .nullable() @@ -525,7 +531,11 @@ const createContainerConfigBaseSchema = (imageLabels: Record, na initContainers: initContainerRule, capabilities: uniqueKeyValuesSchema.default(null).nullable().optional().label('container:common.capabilities'), storage: storageRule, - secrets: (secretsHaveValue ? uniqueKeyValuesSchema : uniqueKeySchema).default(null).nullable().optional().label('container:common.secrets'), + secrets: (secretsHaveValue ? uniqueKeyValuesSchema : uniqueKeySchema) + .default(null) + .nullable() + .optional() + .label('container:common.secrets'), // dagent: logConfig: logConfigRule, @@ -560,7 +570,7 @@ export const createConfigSchema = (type: ContainerConfigType, imageLabels: Recor * config bundles should also be able to store secret values (no agent key though...) */ - const nameRequired = (type === "image" || type === "instance") - const secretsHaveValue = (type === "image" || type === "deployment") + const nameRequired = type === 'image' || type === 'instance' + const secretsHaveValue = type === 'image' || type === 'deployment' return createContainerConfigBaseSchema(imageLabels, nameRequired, secretsHaveValue) } diff --git a/web/crux-ui/src/validations/deployment.ts b/web/crux-ui/src/validations/deployment.ts index 99f14cbdd9..a6c2c4bc08 100644 --- a/web/crux-ui/src/validations/deployment.ts +++ b/web/crux-ui/src/validations/deployment.ts @@ -41,7 +41,7 @@ export const startDeploymentSchema = yup.object({ instances: yup .array( yup.object().shape({ - config: createConfigSchema("instance", null), + config: createConfigSchema('instance', null), }), ) .ensure() diff --git a/web/crux-ui/src/validations/image.ts b/web/crux-ui/src/validations/image.ts index f2898368ca..d48c673ecb 100644 --- a/web/crux-ui/src/validations/image.ts +++ b/web/crux-ui/src/validations/image.ts @@ -23,7 +23,7 @@ export const getContainerConfigFieldErrors = ( imageLabels: Record, t: Translate, ): ContainerConfigValidationErrors => - getConfigFieldErrorsForSchema(createConfigSchema("image", imageLabels), newConfig, t) + getConfigFieldErrorsForSchema(createConfigSchema('image', imageLabels), newConfig, t) export const jsonErrorOf = (fieldErrors: ContainerConfigValidationErrors) => { const entries = Object.entries(fieldErrors) diff --git a/web/crux/src/app/image/image.service.ts b/web/crux/src/app/image/image.service.ts index 10dbb9399a..0e0de94a46 100644 --- a/web/crux/src/app/image/image.service.ts +++ b/web/crux/src/app/image/image.service.ts @@ -126,7 +126,7 @@ export default class ImageService { create: { type: 'image', name: imageName, - } + }, }, createdBy: identity.id, name: imageName, From a942ad017c2d8814dcde6d808a15eeb2f40fdbb8 Mon Sep 17 00:00:00 2001 From: Mate Vago Date: Tue, 3 Dec 2024 14:15:01 +0100 Subject: [PATCH 22/32] fix: config bundle e2e --- web/crux-ui/e2e/utils/config-bundle.ts | 2 +- web/crux-ui/e2e/with-login/config-bundle.spec.ts | 6 ++++-- web/crux-ui/src/routes.ts | 2 -- web/crux-ui/src/validations/container.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/web/crux-ui/e2e/utils/config-bundle.ts b/web/crux-ui/e2e/utils/config-bundle.ts index 571fd25492..99b4f98513 100644 --- a/web/crux-ui/e2e/utils/config-bundle.ts +++ b/web/crux-ui/e2e/utils/config-bundle.ts @@ -31,7 +31,7 @@ export const createConfigBundle = async (page: Page, name: string, data: Record< const configId = page.url().split('/').pop() const ws = await sock - const wsRoute = TEAM_ROUTES.configBundle.detailsSocket(configId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(configId) await page.locator('button:has-text("Environment")').click() diff --git a/web/crux-ui/e2e/with-login/config-bundle.spec.ts b/web/crux-ui/e2e/with-login/config-bundle.spec.ts index 4c89a69c51..62751d66db 100644 --- a/web/crux-ui/e2e/with-login/config-bundle.spec.ts +++ b/web/crux-ui/e2e/with-login/config-bundle.spec.ts @@ -13,12 +13,14 @@ test('Creating a config bundle', async ({ page }) => { }) await page.goto(TEAM_ROUTES.configBundle.details(configBundleId)) + await page.waitForSelector(`h3:text-is("${BUNDLE_NAME}")`) + + await page.locator('button:has-text("Config")').click() + await page.waitForURL(TEAM_ROUTES.containerConfig.details('**')) const keyInput = page.locator('input[placeholder="Key"]').first() - await expect(keyInput).toBeDisabled() await expect(keyInput).toHaveValue(ENV_KEY) const valueInput = page.locator('input[placeholder="Value"]').first() - await expect(valueInput).toBeDisabled() await expect(valueInput).toHaveValue(ENV_VALUE) }) diff --git a/web/crux-ui/src/routes.ts b/web/crux-ui/src/routes.ts index 99b505fc36..61f2e8a0ff 100644 --- a/web/crux-ui/src/routes.ts +++ b/web/crux-ui/src/routes.ts @@ -699,8 +699,6 @@ class ConfigBundleRoutes { list = (options?: ListRouteOptions) => appendAnchorWhenDeclared(this.root, ANCHOR_NEW, options?.new) details = (id: string) => `${this.root}/${id}` - - detailsSocket = (id: string) => this.details(id) } export class PackageApi { diff --git a/web/crux-ui/src/validations/container.ts b/web/crux-ui/src/validations/container.ts index 4cf4f6f3dc..5abd0e91c7 100644 --- a/web/crux-ui/src/validations/container.ts +++ b/web/crux-ui/src/validations/container.ts @@ -501,7 +501,7 @@ const testEnvironment = (imageLabels: Record) => (arr: UniqueKey const createContainerConfigBaseSchema = (imageLabels: Record) => yup.object().shape({ - name: matchContainerName(yup.string().required().label('container:common.containerName')), + name: matchContainerName(yup.string().nullable().optional().label('container:common.containerName')), environment: uniqueKeyValuesSchema .default(null) .nullable() From 0b3741043636ea2494eeda6695a41fca98bd24e5 Mon Sep 17 00:00:00 2001 From: Mate Vago Date: Tue, 3 Dec 2024 14:43:13 +0100 Subject: [PATCH 23/32] fix: golang unit tests --- golang/internal/mapper/grpc_test.go | 37 +-- protobuf/go/agent/agent.pb.go | 270 +++++++++++----------- protobuf/proto/agent.proto | 15 +- web/crux/proto/agent.proto | 15 +- web/crux/src/app/deploy/deploy.service.ts | 1 - web/crux/src/grpc/protobuf/proto/agent.ts | 5 +- 6 files changed, 156 insertions(+), 187 deletions(-) diff --git a/golang/internal/mapper/grpc_test.go b/golang/internal/mapper/grpc_test.go index c6531b5e84..08e8e57ae7 100644 --- a/golang/internal/mapper/grpc_test.go +++ b/golang/internal/mapper/grpc_test.go @@ -67,17 +67,13 @@ func testExpectedCommon(req *agent.DeployWorkloadRequest) *v1.DeployImageRequest Password: "test-pass", }, InstanceConfig: v1.InstanceConfig{ - ContainerPreName: "test-prefix", - MountPath: "/path/to/mount", - Name: "test-prefix", - Environment: map[string]string{"Evn1": "Val1", "Env2": "Val2"}, - Registry: "", - RepositoryPreName: "repo-prefix", - SharedEnvironment: map[string]string{}, UseSharedEnvs: false, + Environment: map[string]string{}, + SharedEnvironment: map[string]string{}, + ContainerPreName: "", }, ContainerConfig: v1.ContainerConfig{ - ContainerPreName: "test-prefix", + ContainerPreName: "", Container: "test-common-config", Ports: []builder.PortBinding{{ExposedPort: 0x4d2, PortBinding: pointer.ToUint16(0x1a85)}}, PortRanges: []builder.PortRangeBinding{{Internal: builder.PortRange{From: 0x0, To: 0x18}, External: builder.PortRange{From: 0x40, To: 0x80}}}, @@ -153,7 +149,7 @@ func testExpectedCommon(req *agent.DeployWorkloadRequest) *v1.DeployImageRequest UseLoadBalancer: true, ExtraLBAnnotations: map[string]string{"annotation1": "value1"}, }, - RuntimeConfig: v1.Base64JSONBytes{0x6b, 0x65, 0x79, 0x31, 0x3d, 0x76, 0x61, 0x6c, 0x31, 0x2c, 0x6b, 0x65, 0x79, 0x32, 0x3d, 0x76, 0x61, 0x6c, 0x32}, // encoded string: a2V5MT12YWwxLGtleTI9dmFsMg== + RuntimeConfig: nil, Registry: req.Registry, ImageName: "test-image", Tag: "test-tag", @@ -212,22 +208,17 @@ func TestMapDockerContainerEventToContainerState(t *testing.T) { func testDeployRequest() *agent.DeployWorkloadRequest { registry := "https://my-registry.com" - runtimeCfg := "key1=val1,key2=val2" var uid int64 = 777 upLimit := "5Mi" - mntPath := "/path/to/mount" - repoPrefix := "repo-prefix" strategy := common.ExposeStrategy_EXPOSE_WITH_TLS b := true return &agent.DeployWorkloadRequest{ - Id: "testID", - ContainerName: "test-container", - ImageName: "test-image", - Tag: "test-tag", - Registry: ®istry, - RuntimeConfig: &runtimeCfg, - Dagent: testDagentConfig(), - Crane: testCraneConfig(), + Id: "testID", + ImageName: "test-image", + Tag: "test-tag", + Registry: ®istry, + Dagent: testDagentConfig(), + Crane: testCraneConfig(), Common: &agent.CommonContainerConfig{ Name: "test-common-config", Commands: []string{"make", "test"}, @@ -274,12 +265,6 @@ func testDeployRequest() *agent.DeployWorkloadRequest { Password: "test-pass", Url: "https://test-url.com", }, - InstanceConfig: &agent.InstanceConfig{ - Prefix: "test-prefix", - MountPath: &mntPath, - RepositoryPrefix: &repoPrefix, - Environment: map[string]string{"Evn1": "Val1", "Env2": "Val2"}, - }, } } diff --git a/protobuf/go/agent/agent.pb.go b/protobuf/go/agent/agent.pb.go index 819dd03c05..30837f2dbb 100644 --- a/protobuf/go/agent/agent.pb.go +++ b/protobuf/go/agent/agent.pb.go @@ -1865,16 +1865,15 @@ type DeployWorkloadRequest struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - ContainerName string `protobuf:"bytes,2,opt,name=containerName,proto3" json:"containerName,omitempty"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // ContainerConfigs - Common *CommonContainerConfig `protobuf:"bytes,3,opt,name=common,proto3,oneof" json:"common,omitempty"` - Dagent *DagentContainerConfig `protobuf:"bytes,4,opt,name=dagent,proto3,oneof" json:"dagent,omitempty"` - Crane *CraneContainerConfig `protobuf:"bytes,5,opt,name=crane,proto3,oneof" json:"crane,omitempty"` - Registry *string `protobuf:"bytes,6,opt,name=registry,proto3,oneof" json:"registry,omitempty"` - ImageName string `protobuf:"bytes,7,opt,name=imageName,proto3" json:"imageName,omitempty"` - Tag string `protobuf:"bytes,8,opt,name=tag,proto3" json:"tag,omitempty"` - RegistryAuth *RegistryAuth `protobuf:"bytes,9,opt,name=registryAuth,proto3,oneof" json:"registryAuth,omitempty"` + Common *CommonContainerConfig `protobuf:"bytes,2,opt,name=common,proto3,oneof" json:"common,omitempty"` + Dagent *DagentContainerConfig `protobuf:"bytes,3,opt,name=dagent,proto3,oneof" json:"dagent,omitempty"` + Crane *CraneContainerConfig `protobuf:"bytes,4,opt,name=crane,proto3,oneof" json:"crane,omitempty"` + Registry *string `protobuf:"bytes,5,opt,name=registry,proto3,oneof" json:"registry,omitempty"` + ImageName string `protobuf:"bytes,6,opt,name=imageName,proto3" json:"imageName,omitempty"` + Tag string `protobuf:"bytes,7,opt,name=tag,proto3" json:"tag,omitempty"` + RegistryAuth *RegistryAuth `protobuf:"bytes,8,opt,name=registryAuth,proto3,oneof" json:"registryAuth,omitempty"` } func (x *DeployWorkloadRequest) Reset() { @@ -1916,13 +1915,6 @@ func (x *DeployWorkloadRequest) GetId() string { return "" } -func (x *DeployWorkloadRequest) GetContainerName() string { - if x != nil { - return x.ContainerName - } - return "" -} - func (x *DeployWorkloadRequest) GetCommon() *CommonContainerConfig { if x != nil { return x.Common @@ -2834,133 +2826,131 @@ var file_protobuf_proto_agent_proto_rawDesc = []byte{ 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x42, 0x07, 0x0a, 0x05, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x42, 0x06, 0x0a, 0x04, 0x5f, 0x54, 0x54, 0x59, 0x42, 0x13, 0x0a, 0x11, 0x5f, 0x77, 0x6f, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, - 0x22, 0xc8, 0x03, 0x0a, 0x15, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x57, 0x6f, 0x72, 0x6b, 0x6c, + 0x22, 0xa2, 0x03, 0x0a, 0x15, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x57, 0x6f, 0x72, 0x6b, 0x6c, 0x6f, 0x61, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x24, 0x0a, 0x0d, 0x63, 0x6f, - 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, - 0x12, 0x39, 0x0a, 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1c, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x43, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, - 0x52, 0x06, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x39, 0x0a, 0x06, 0x64, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, - 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x01, 0x52, 0x06, 0x64, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x12, 0x36, 0x0a, 0x05, 0x63, 0x72, 0x61, 0x6e, 0x65, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x72, - 0x61, 0x6e, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, - 0x69, 0x67, 0x48, 0x02, 0x52, 0x05, 0x63, 0x72, 0x61, 0x6e, 0x65, 0x88, 0x01, 0x01, 0x12, 0x1f, - 0x0a, 0x08, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, - 0x48, 0x03, 0x52, 0x08, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x88, 0x01, 0x01, 0x12, - 0x1c, 0x0a, 0x09, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x07, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x09, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, - 0x03, 0x74, 0x61, 0x67, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, - 0x3c, 0x0a, 0x0c, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x41, 0x75, 0x74, 0x68, 0x18, - 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x41, 0x75, 0x74, 0x68, 0x48, 0x04, 0x52, 0x0c, 0x72, 0x65, - 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x41, 0x75, 0x74, 0x68, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, - 0x07, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x64, 0x61, 0x67, - 0x65, 0x6e, 0x74, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x63, 0x72, 0x61, 0x6e, 0x65, 0x42, 0x0b, 0x0a, - 0x09, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x72, - 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x41, 0x75, 0x74, 0x68, 0x22, 0x6a, 0x0a, 0x15, 0x43, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x88, 0x01, - 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x6f, 0x6e, 0x65, 0x53, 0x68, 0x6f, 0x74, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x08, 0x48, 0x01, 0x52, 0x07, 0x6f, 0x6e, 0x65, 0x53, 0x68, 0x6f, 0x74, 0x88, 0x01, 0x01, - 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x42, 0x0a, 0x0a, 0x08, 0x5f, - 0x6f, 0x6e, 0x65, 0x53, 0x68, 0x6f, 0x74, 0x22, 0x44, 0x0a, 0x16, 0x43, 0x6f, 0x6e, 0x74, 0x61, - 0x69, 0x6e, 0x65, 0x72, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x47, 0x0a, - 0x13, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x65, - 0x67, 0x61, 0x63, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0x22, 0x64, 0x0a, 0x12, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, - 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x26, - 0x0a, 0x0e, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0e, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, - 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x2b, 0x0a, 0x13, - 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x28, 0x0a, 0x10, 0x41, 0x67, 0x65, - 0x6e, 0x74, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, - 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, - 0x72, 0x6f, 0x72, 0x22, 0x82, 0x01, 0x0a, 0x13, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, - 0x72, 0x4c, 0x6f, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x09, 0x63, - 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x39, 0x0a, 0x06, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x67, 0x65, + 0x6e, 0x74, 0x2e, 0x43, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x00, 0x52, 0x06, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x88, 0x01, 0x01, 0x12, 0x39, 0x0a, 0x06, 0x64, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x18, + 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x61, + 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x48, 0x01, 0x52, 0x06, 0x64, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x88, 0x01, 0x01, + 0x12, 0x36, 0x0a, 0x05, 0x63, 0x72, 0x61, 0x6e, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1b, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x72, 0x61, 0x6e, 0x65, 0x43, 0x6f, 0x6e, + 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x02, 0x52, 0x05, + 0x63, 0x72, 0x61, 0x6e, 0x65, 0x88, 0x01, 0x01, 0x12, 0x1f, 0x0a, 0x08, 0x72, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, 0x03, 0x52, 0x08, 0x72, 0x65, + 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, 0x88, 0x01, 0x01, 0x12, 0x1c, 0x0a, 0x09, 0x69, 0x6d, 0x61, + 0x67, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, 0x6d, + 0x61, 0x67, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x07, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x3c, 0x0a, 0x0c, 0x72, 0x65, 0x67, + 0x69, 0x73, 0x74, 0x72, 0x79, 0x41, 0x75, 0x74, 0x68, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x13, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, + 0x41, 0x75, 0x74, 0x68, 0x48, 0x04, 0x52, 0x0c, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, 0x79, + 0x41, 0x75, 0x74, 0x68, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x64, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x42, 0x08, 0x0a, + 0x06, 0x5f, 0x63, 0x72, 0x61, 0x6e, 0x65, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x72, 0x65, 0x67, 0x69, + 0x73, 0x74, 0x72, 0x79, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x72, 0x65, 0x67, 0x69, 0x73, 0x74, 0x72, + 0x79, 0x41, 0x75, 0x74, 0x68, 0x22, 0x6a, 0x0a, 0x15, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1b, + 0x0a, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, + 0x52, 0x06, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x07, 0x6f, + 0x6e, 0x65, 0x53, 0x68, 0x6f, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x48, 0x01, 0x52, 0x07, + 0x6f, 0x6e, 0x65, 0x53, 0x68, 0x6f, 0x74, 0x88, 0x01, 0x01, 0x42, 0x09, 0x0a, 0x07, 0x5f, 0x70, + 0x72, 0x65, 0x66, 0x69, 0x78, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x6f, 0x6e, 0x65, 0x53, 0x68, 0x6f, + 0x74, 0x22, 0x44, 0x0a, 0x16, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x44, 0x65, + 0x6c, 0x65, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x70, + 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x72, 0x65, + 0x66, 0x69, 0x78, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x47, 0x0a, 0x13, 0x44, 0x65, 0x70, 0x6c, 0x6f, + 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4c, 0x65, 0x67, 0x61, 0x63, 0x79, 0x12, 0x1c, + 0x0a, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, 0x64, 0x12, 0x12, 0x0a, 0x04, + 0x6a, 0x73, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6a, 0x73, 0x6f, 0x6e, + 0x22, 0x64, 0x0a, 0x12, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x26, 0x0a, 0x0e, 0x74, 0x69, 0x6d, 0x65, + 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, + 0x52, 0x0e, 0x74, 0x69, 0x6d, 0x65, 0x6f, 0x75, 0x74, 0x53, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, + 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x22, 0x2b, 0x0a, 0x13, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, + 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x14, 0x0a, + 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x22, 0x28, 0x0a, 0x10, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x62, 0x6f, 0x72, + 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x22, 0x82, 0x01, + 0x0a, 0x13, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, + 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x12, 0x12, + 0x0a, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x74, 0x61, + 0x69, 0x6c, 0x22, 0x54, 0x0a, 0x17, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, + 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, + 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, + 0x6e, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x09, 0x63, + 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x44, 0x0a, 0x16, 0x43, 0x6c, 0x6f, 0x73, + 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, + 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x2a, 0x69, + 0x0a, 0x0b, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, + 0x18, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x5f, 0x52, 0x45, 0x41, 0x53, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, + 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x43, + 0x4c, 0x4f, 0x53, 0x45, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, 0x53, 0x45, 0x4c, 0x46, 0x5f, 0x44, + 0x45, 0x53, 0x54, 0x52, 0x55, 0x43, 0x54, 0x10, 0x02, 0x12, 0x0c, 0x0a, 0x08, 0x53, 0x48, 0x55, + 0x54, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x12, 0x10, 0x0a, 0x0c, 0x52, 0x45, 0x56, 0x4f, 0x4b, + 0x45, 0x5f, 0x54, 0x4f, 0x4b, 0x45, 0x4e, 0x10, 0x04, 0x32, 0x9c, 0x05, 0x0a, 0x05, 0x41, 0x67, + 0x65, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x07, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x12, 0x10, + 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, + 0x1a, 0x13, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, + 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x30, 0x01, 0x12, 0x37, 0x0a, 0x0c, 0x43, 0x6f, 0x6d, 0x6d, 0x61, + 0x6e, 0x64, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, + 0x41, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x45, 0x72, 0x72, 0x6f, + 0x72, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x12, 0x35, 0x0a, 0x0b, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, + 0x17, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x41, 0x62, 0x6f, + 0x72, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x2d, 0x0a, 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, + 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x64, 0x12, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x44, 0x0a, 0x10, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x1f, 0x2e, 0x63, 0x6f, 0x6d, + 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, + 0x61, 0x74, 0x75, 0x73, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, + 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x28, 0x01, 0x12, 0x44, 0x0a, 0x0e, + 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x21, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, - 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x09, 0x63, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, - 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x73, 0x74, 0x72, 0x65, 0x61, - 0x6d, 0x69, 0x6e, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0d, 0x52, 0x04, 0x74, 0x61, 0x69, 0x6c, 0x22, 0x54, 0x0a, 0x17, 0x43, 0x6f, 0x6e, 0x74, - 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x39, 0x0a, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, - 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, - 0x69, 0x65, 0x72, 0x52, 0x09, 0x63, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x22, 0x44, - 0x0a, 0x16, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, - 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x12, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, - 0x2e, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x52, 0x06, 0x72, 0x65, - 0x61, 0x73, 0x6f, 0x6e, 0x2a, 0x69, 0x0a, 0x0b, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x52, 0x65, 0x61, - 0x73, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x18, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x5f, 0x52, 0x45, 0x41, - 0x53, 0x4f, 0x4e, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, - 0x00, 0x12, 0x09, 0x0a, 0x05, 0x43, 0x4c, 0x4f, 0x53, 0x45, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d, - 0x53, 0x45, 0x4c, 0x46, 0x5f, 0x44, 0x45, 0x53, 0x54, 0x52, 0x55, 0x43, 0x54, 0x10, 0x02, 0x12, - 0x0c, 0x0a, 0x08, 0x53, 0x48, 0x55, 0x54, 0x44, 0x4f, 0x57, 0x4e, 0x10, 0x03, 0x12, 0x10, 0x0a, - 0x0c, 0x52, 0x45, 0x56, 0x4f, 0x4b, 0x45, 0x5f, 0x54, 0x4f, 0x4b, 0x45, 0x4e, 0x10, 0x04, 0x32, - 0x9c, 0x05, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x32, 0x0a, 0x07, 0x43, 0x6f, 0x6e, - 0x6e, 0x65, 0x63, 0x74, 0x12, 0x10, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, - 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x1a, 0x13, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, - 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x30, 0x01, 0x12, 0x37, 0x0a, - 0x0c, 0x43, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x18, 0x2e, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x43, 0x6f, 0x6d, 0x6d, 0x61, - 0x6e, 0x64, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x35, 0x0a, 0x0b, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x55, - 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x17, 0x2e, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x41, 0x67, - 0x65, 0x6e, 0x74, 0x41, 0x62, 0x6f, 0x72, 0x74, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x1a, 0x0d, - 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x2d, 0x0a, - 0x0d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x63, 0x65, 0x64, 0x12, 0x0d, - 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, - 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x44, 0x0a, 0x10, - 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, - 0x12, 0x1f, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x44, 0x65, 0x70, 0x6c, 0x6f, 0x79, - 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, + 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, - 0x28, 0x01, 0x12, 0x44, 0x0a, 0x0e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, - 0x74, 0x61, 0x74, 0x65, 0x12, 0x21, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, - 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x65, 0x4c, 0x69, 0x73, 0x74, - 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x28, 0x01, 0x12, 0x42, 0x0a, 0x12, 0x43, 0x6f, 0x6e, 0x74, - 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x1b, - 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, - 0x72, 0x4c, 0x6f, 0x67, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, - 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x28, 0x01, 0x12, 0x38, 0x0a, 0x0a, - 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1b, 0x2e, 0x63, 0x6f, 0x6d, - 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, - 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x30, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x73, 0x12, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, - 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, - 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x0c, 0x43, 0x6f, 0x6e, 0x74, - 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x12, 0x20, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, - 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x4c, 0x69, - 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, - 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x43, 0x0a, 0x10, 0x43, 0x6f, 0x6e, - 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x12, 0x20, 0x2e, - 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, - 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, - 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x35, - 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x79, 0x72, - 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2d, 0x69, 0x6f, 0x2f, 0x64, 0x79, 0x72, 0x65, 0x63, 0x74, 0x6f, - 0x72, 0x69, 0x6f, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x67, 0x6f, 0x2f, - 0x61, 0x67, 0x65, 0x6e, 0x74, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x28, 0x01, 0x12, 0x42, 0x0a, 0x12, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, + 0x6f, 0x67, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x1b, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, + 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x4d, 0x65, + 0x73, 0x73, 0x61, 0x67, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, + 0x6d, 0x70, 0x74, 0x79, 0x28, 0x01, 0x12, 0x38, 0x0a, 0x0a, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, + 0x4c, 0x69, 0x73, 0x74, 0x12, 0x1b, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, + 0x12, 0x30, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, + 0x6e, 0x65, 0x72, 0x73, 0x12, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, + 0x74, 0x79, 0x12, 0x3f, 0x0a, 0x0c, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, + 0x6f, 0x67, 0x12, 0x20, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6e, 0x74, + 0x61, 0x69, 0x6e, 0x65, 0x72, 0x4c, 0x6f, 0x67, 0x4c, 0x69, 0x73, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x45, 0x6d, + 0x70, 0x74, 0x79, 0x12, 0x43, 0x0a, 0x10, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, + 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, 0x74, 0x12, 0x20, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, + 0x2e, 0x43, 0x6f, 0x6e, 0x74, 0x61, 0x69, 0x6e, 0x65, 0x72, 0x49, 0x6e, 0x73, 0x70, 0x65, 0x63, + 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x1a, 0x0d, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, + 0x6f, 0x6e, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x69, 0x74, 0x68, + 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x64, 0x79, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x2d, + 0x69, 0x6f, 0x2f, 0x64, 0x79, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x69, 0x6f, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x67, 0x6f, 0x2f, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/protobuf/proto/agent.proto b/protobuf/proto/agent.proto index 8b6e43eb40..57d949f538 100644 --- a/protobuf/proto/agent.proto +++ b/protobuf/proto/agent.proto @@ -231,18 +231,17 @@ message CommonContainerConfig { message DeployWorkloadRequest { string id = 1; - string containerName = 2; /* ContainerConfigs */ - optional CommonContainerConfig common = 3; - optional DagentContainerConfig dagent = 4; - optional CraneContainerConfig crane = 5; + optional CommonContainerConfig common = 2; + optional DagentContainerConfig dagent = 3; + optional CraneContainerConfig crane = 4; - optional string registry = 6; - string imageName = 7; - string tag = 8; + optional string registry = 5; + string imageName = 6; + string tag = 7; - optional RegistryAuth registryAuth = 9; + optional RegistryAuth registryAuth = 8; } message ContainerStateRequest { diff --git a/web/crux/proto/agent.proto b/web/crux/proto/agent.proto index 8b6e43eb40..57d949f538 100644 --- a/web/crux/proto/agent.proto +++ b/web/crux/proto/agent.proto @@ -231,18 +231,17 @@ message CommonContainerConfig { message DeployWorkloadRequest { string id = 1; - string containerName = 2; /* ContainerConfigs */ - optional CommonContainerConfig common = 3; - optional DagentContainerConfig dagent = 4; - optional CraneContainerConfig crane = 5; + optional CommonContainerConfig common = 2; + optional DagentContainerConfig dagent = 3; + optional CraneContainerConfig crane = 4; - optional string registry = 6; - string imageName = 7; - string tag = 8; + optional string registry = 5; + string imageName = 6; + string tag = 7; - optional RegistryAuth registryAuth = 9; + optional RegistryAuth registryAuth = 8; } message ContainerStateRequest { diff --git a/web/crux/src/app/deploy/deploy.service.ts b/web/crux/src/app/deploy/deploy.service.ts index 9203068624..b629101955 100644 --- a/web/crux/src/app/deploy/deploy.service.ts +++ b/web/crux/src/app/deploy/deploy.service.ts @@ -585,7 +585,6 @@ export default class DeployService { crane: this.mapper.craneConfigToAgentProto(config), dagent: this.mapper.dagentConfigToAgentProto(config), id: it.id, - containerName: it.image.config.name, imageName: it.image.name, tag: it.image.tag, registry: registryUrl, diff --git a/web/crux/src/grpc/protobuf/proto/agent.ts b/web/crux/src/grpc/protobuf/proto/agent.ts index b3ce694c6a..30e29af90c 100644 --- a/web/crux/src/grpc/protobuf/proto/agent.ts +++ b/web/crux/src/grpc/protobuf/proto/agent.ts @@ -328,7 +328,6 @@ export interface CommonContainerConfig_SecretsEntry { export interface DeployWorkloadRequest { id: string - containerName: string /** ContainerConfigs */ common?: CommonContainerConfig | undefined dagent?: DagentContainerConfig | undefined @@ -1336,14 +1335,13 @@ export const CommonContainerConfig_SecretsEntry = { } function createBaseDeployWorkloadRequest(): DeployWorkloadRequest { - return { id: '', containerName: '', imageName: '', tag: '' } + return { id: '', imageName: '', tag: '' } } export const DeployWorkloadRequest = { fromJSON(object: any): DeployWorkloadRequest { return { id: isSet(object.id) ? String(object.id) : '', - containerName: isSet(object.containerName) ? String(object.containerName) : '', common: isSet(object.common) ? CommonContainerConfig.fromJSON(object.common) : undefined, dagent: isSet(object.dagent) ? DagentContainerConfig.fromJSON(object.dagent) : undefined, crane: isSet(object.crane) ? CraneContainerConfig.fromJSON(object.crane) : undefined, @@ -1357,7 +1355,6 @@ export const DeployWorkloadRequest = { toJSON(message: DeployWorkloadRequest): unknown { const obj: any = {} message.id !== undefined && (obj.id = message.id) - message.containerName !== undefined && (obj.containerName = message.containerName) message.common !== undefined && (obj.common = message.common ? CommonContainerConfig.toJSON(message.common) : undefined) message.dagent !== undefined && From 04f27d345d2009c97b60b90b8ffb5614d20a1462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bence=20Nagygy=C3=B6rgy?= Date: Tue, 3 Dec 2024 15:34:31 +0100 Subject: [PATCH 24/32] fix(web): branch default value --- web/crux-ui/src/components/pipelines/edit-pipeline-card.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/crux-ui/src/components/pipelines/edit-pipeline-card.tsx b/web/crux-ui/src/components/pipelines/edit-pipeline-card.tsx index 5e1ec11a37..29ec274a51 100644 --- a/web/crux-ui/src/components/pipelines/edit-pipeline-card.tsx +++ b/web/crux-ui/src/components/pipelines/edit-pipeline-card.tsx @@ -33,7 +33,7 @@ const DEFAULT_EDITABLE_PIPELINE: EditablePipeline = { repository: { organization: '', project: '', - branch: null, + branch: '', }, trigger: { name: '', From 885cbd1b80e6664dfdec12208f0eb91fe1b669b2 Mon Sep 17 00:00:00 2001 From: Mate Vago Date: Tue, 3 Dec 2024 15:37:37 +0100 Subject: [PATCH 25/32] fix: setting prefix and container name --- golang/pkg/dagent/utils/docker.go | 5 +++++ web/crux/src/domain/start-deployment.ts | 10 +++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/golang/pkg/dagent/utils/docker.go b/golang/pkg/dagent/utils/docker.go index d4c7743fbc..a52e1a89d8 100644 --- a/golang/pkg/dagent/utils/docker.go +++ b/golang/pkg/dagent/utils/docker.go @@ -561,6 +561,11 @@ func getContainerPrefix(deployImageRequest *v1.DeployImageRequest) string { containerPrefix = deployImageRequest.InstanceConfig.ContainerPreName } } + + if containerPrefix == "" { + containerPrefix = deployImageRequest.ContainerConfig.ContainerPreName + } + return containerPrefix } diff --git a/web/crux/src/domain/start-deployment.ts b/web/crux/src/domain/start-deployment.ts index af52b93f3d..3334bc63c7 100644 --- a/web/crux/src/domain/start-deployment.ts +++ b/web/crux/src/domain/start-deployment.ts @@ -97,6 +97,7 @@ export const deploymentConfigOf = (deployment: DeployableDeployment): ConcreteCo type DeployableInstance = { image: { config: ContainerConfig + name: string } config: ContainerConfig } @@ -121,7 +122,14 @@ export const instanceConfigOf = ( // then we merge and override the rest with the instance config const instanceConfig = instance.config as any as ConcreteContainerConfigData - return mergeInstanceConfigWithDeploymentConfig(mergedDeploymentConfig, instanceConfig) + const result = mergeInstanceConfigWithDeploymentConfig(mergedDeploymentConfig, instanceConfig) + + // set defaults + if (!result.name) { + result.name = instance.image.name + } + + return result } type SecretCandidate = { From 353b16458345f446dc502dfe714b6a8b65262a70 Mon Sep 17 00:00:00 2001 From: Mate Vago Date: Tue, 3 Dec 2024 16:01:06 +0100 Subject: [PATCH 26/32] fix: duplicated images --- web/crux/src/app/version/version.message.ts | 4 +++ .../src/app/version/version.ws.gateway.ts | 26 +++++++------------ 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/web/crux/src/app/version/version.message.ts b/web/crux/src/app/version/version.message.ts index f66390e72c..9dd8d1b0b0 100644 --- a/web/crux/src/app/version/version.message.ts +++ b/web/crux/src/app/version/version.message.ts @@ -1,5 +1,6 @@ import { AddImagesDto, ImageDetailsDto } from '../image/image.dto' +export const WS_TYPE_GET_IMAGE = 'get-image' export type GetImageMessage = { id: string } @@ -7,6 +8,7 @@ export type GetImageMessage = { export const WS_TYPE_IMAGE = 'image' export type ImageMessage = ImageDetailsDto +export const WS_TYPE_ADD_IMAGES = 'add-images' export type AddImagesMessage = { registryImages: AddImagesDto[] } @@ -23,6 +25,7 @@ export type ImagesAddedMessage = { images: ImageDetailsDto[] } +export const WS_TYPE_DELETE_IMAGE = 'delete-image' export type DeleteImageMessage = { imageId: string } @@ -33,4 +36,5 @@ export type ImageDeletedMessage = { } export const WS_TYPE_IMAGES_WERE_REORDERED = 'images-were-reordered' +export const WS_TYPE_ORDER_IMAGES = 'order-images' export type OrderImagesMessage = string[] diff --git a/web/crux/src/app/version/version.ws.gateway.ts b/web/crux/src/app/version/version.ws.gateway.ts index 8cd1c31765..9af90dc479 100644 --- a/web/crux/src/app/version/version.ws.gateway.ts +++ b/web/crux/src/app/version/version.ws.gateway.ts @@ -36,13 +36,15 @@ import { ImageDeletedMessage, ImageMessage, ImageTagMessage, - ImagesAddedMessage, OrderImagesMessage, + WS_TYPE_ADD_IMAGES, + WS_TYPE_DELETE_IMAGE, + WS_TYPE_GET_IMAGE, WS_TYPE_IMAGE, - WS_TYPE_IMAGES_ADDED, WS_TYPE_IMAGES_WERE_REORDERED, WS_TYPE_IMAGE_DELETED, WS_TYPE_IMAGE_TAG_UPDATED, + WS_TYPE_ORDER_IMAGES, WS_TYPE_SET_IMAGE_TAG, } from './version.message' import VersionService from './version.service' @@ -114,7 +116,7 @@ export default class VersionWebSocketGateway { } @AuditLogLevel('disabled') - @SubscribeMessage('get-image') + @SubscribeMessage(WS_TYPE_GET_IMAGE) async getImage(@SocketMessage() message: GetImageMessage): Promise> { const data = await this.imageService.getImageDetails(message.id) @@ -124,27 +126,17 @@ export default class VersionWebSocketGateway { } as WsMessage } - @SubscribeMessage('add-images') + @SubscribeMessage(WS_TYPE_ADD_IMAGES) async addImages( @TeamSlug() teamSlug: string, @VersionId() versionId: string, @SocketMessage() message: AddImagesMessage, @IdentityFromSocket() identity: Identity, - @SocketSubscription() subscription: WsSubscription, ): Promise { - const images = await this.imageService.addImagesToVersion(teamSlug, versionId, message.registryImages, identity) - - const res: WsMessage = { - type: WS_TYPE_IMAGES_ADDED, - data: { - images, - }, - } - - subscription.sendToAll(res) + await this.imageService.addImagesToVersion(teamSlug, versionId, message.registryImages, identity) } - @SubscribeMessage('delete-image') + @SubscribeMessage(WS_TYPE_DELETE_IMAGE) async deleteImage( @SocketMessage() message: DeleteImageMessage, @SocketSubscription() subscription: WsSubscription, @@ -189,7 +181,7 @@ export default class VersionWebSocketGateway { } } - @SubscribeMessage('order-images') + @SubscribeMessage(WS_TYPE_ORDER_IMAGES) async orderImages( @SocketClient() client: WsClient, @SocketMessage() message: OrderImagesMessage, From 038495dd95aa3851ae9be0b0ce4ef6cff704f2d4 Mon Sep 17 00:00:00 2001 From: Mate Vago Date: Tue, 3 Dec 2024 16:19:39 +0100 Subject: [PATCH 27/32] fix: container name defaults --- web/crux-ui/src/models/image.ts | 11 ++++++++++- web/crux/src/domain/container.ts | 3 ++- web/crux/src/domain/image.ts | 8 ++++++++ web/crux/src/domain/start-deployment.ts | 6 +++--- 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/web/crux-ui/src/models/image.ts b/web/crux-ui/src/models/image.ts index 4f366a9fe7..52854517fb 100644 --- a/web/crux-ui/src/models/image.ts +++ b/web/crux-ui/src/models/image.ts @@ -78,4 +78,13 @@ export type ImageMessage = VersionImage export const imageNameOf = (image: VersionImage): string => imageName(image.name, image.tag) -export const containerNameOfImage = (image: VersionImage) => image.config.name ?? image.name +export const registryImageNameToContainerName = (name: string) => { + if (name.includes('/')) { + return name.split('/').pop() + } + + return name +} + +export const containerNameOfImage = (image: VersionImage) => + image.config.name ?? registryImageNameToContainerName(image.name) diff --git a/web/crux/src/domain/container.ts b/web/crux/src/domain/container.ts index 599cc7f63b..bf371f100c 100644 --- a/web/crux/src/domain/container.ts +++ b/web/crux/src/domain/container.ts @@ -1,4 +1,5 @@ import { NetworkMode } from '@prisma/client' +import { registryImageNameToContainerName } from './image' export const PORT_MIN = 0 export const PORT_MAX = 65535 @@ -278,4 +279,4 @@ type InstanceWithConfigAndImageConfig = { } export const nameOfInstance = (instance: InstanceWithConfigAndImageConfig) => - instance.config.name ?? instance.image.config.name ?? instance.image.name + instance.config.name ?? instance.image.config.name ?? registryImageNameToContainerName(instance.image.name) diff --git a/web/crux/src/domain/image.ts b/web/crux/src/domain/image.ts index bd0253f61b..7ac642a4ae 100644 --- a/web/crux/src/domain/image.ts +++ b/web/crux/src/domain/image.ts @@ -76,3 +76,11 @@ export const parseDyrectorioEnvRules = (labels: Record): Record< } }, {}) } + +export const registryImageNameToContainerName = (name: string) => { + if (name.includes('/')) { + return name.split('/').pop() + } + + return name +} diff --git a/web/crux/src/domain/start-deployment.ts b/web/crux/src/domain/start-deployment.ts index 3334bc63c7..4114a672b6 100644 --- a/web/crux/src/domain/start-deployment.ts +++ b/web/crux/src/domain/start-deployment.ts @@ -1,5 +1,5 @@ import { ContainerConfig, DeploymentStatusEnum, VersionTypeEnum } from '@prisma/client' -import { ConcreteContainerConfigData, ContainerConfigData, UniqueSecretKeyValue } from './container' +import { ConcreteContainerConfigData, ContainerConfigData, UniqueSecretKeyValue, nameOfInstance } from './container' import { mergeConfigsWithConcreteConfig, mergeInstanceConfigWithDeploymentConfig } from './container-merge' import { DeploymentWithConfig } from './deployment' @@ -122,11 +122,11 @@ export const instanceConfigOf = ( // then we merge and override the rest with the instance config const instanceConfig = instance.config as any as ConcreteContainerConfigData - const result = mergeInstanceConfigWithDeploymentConfig(mergedDeploymentConfig, instanceConfig) + const result = mergeInstanceConfigWithDeploymentConfig(mergedDeploymentConfig, instanceConfig) // set defaults if (!result.name) { - result.name = instance.image.name + result.name = nameOfInstance(instance) } return result From 8b21704b35106eb60e3294eb49e3929869a5e7aa Mon Sep 17 00:00:00 2001 From: Mate Vago Date: Wed, 4 Dec 2024 12:11:11 +0100 Subject: [PATCH 28/32] fix: storage saving --- .../container-config/common-editor.spec.ts | 106 +++++------ .../container-config/common-json.spec.ts | 177 +++++++++--------- .../container-config-filters.spec.ts | 20 +- .../container-config/docker-editor.spec.ts | 36 ++-- .../container-config/docker-json.spec.ts | 36 ++-- .../image-config-view-state.spec.ts | 12 +- .../kubernetes-editor.spec.ts | 64 +++---- .../container-config/kubernetes-json.spec.ts | 64 +++---- .../deployment/deployment-copy.spec.ts | 10 +- .../deployment-copyability-versioned.spec.ts | 16 +- .../deployment-deletability.spec.ts | 8 +- .../e2e/with-login/image-config.spec.ts | 48 ++--- .../e2e/with-login/resource-copy.spec.ts | 8 +- .../app/container/container-config.service.ts | 12 +- 14 files changed, 311 insertions(+), 306 deletions(-) diff --git a/web/crux-ui/e2e/with-login/container-config/common-editor.spec.ts b/web/crux-ui/e2e/with-login/container-config/common-editor.spec.ts index 239b71fea1..fd56c19a65 100644 --- a/web/crux-ui/e2e/with-login/container-config/common-editor.spec.ts +++ b/web/crux-ui/e2e/with-login/container-config/common-editor.spec.ts @@ -21,7 +21,7 @@ import { } from 'e2e/utils/websocket-match' import { createImage, createProject, createVersion } from '../../utils/projects' import { waitSocketRef, wsPatchSent } from '../../utils/websocket' -import { WS_TYPE_PATCH_IMAGE } from '@app/models' +import { WS_TYPE_PATCH_CONFIG } from '@app/models' const setup = async ( page: Page, @@ -31,26 +31,26 @@ const setup = async ( ): Promise<{ projectId: string; versionId: string; imageId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') - const imageId = await createImage(page, projectId, versionId, imageName) + const imageConfigId = await createImage(page, projectId, versionId, imageName) - return { projectId, versionId, imageId } + return { projectId, versionId, imageConfigId } } test.describe.configure({ mode: 'parallel' }) test.describe('Image common config from editor', () => { test('Container name should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'name-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'name-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) const name = 'new-container-name' - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchContainerName(name)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchContainerName(name)) await page.locator('input[placeholder="Container name"]').fill(name) await wsSent @@ -60,15 +60,15 @@ test.describe('Image common config from editor', () => { }) test('Expose strategy should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'expose-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'expose-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchExpose('exposeWithTls')) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchExpose('exposeWithTls')) await page.getByRole('button', { name: 'HTTPS', exact: true }).click() await wsSent @@ -78,17 +78,17 @@ test.describe('Image common config from editor', () => { }) test('User should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'user-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'user-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) const user = 23 - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchUser(user)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchUser(user)) await page.locator('input[placeholder="Container default"]').fill(user.toString()) await wsSent @@ -98,17 +98,17 @@ test.describe('Image common config from editor', () => { }) test('TTY should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'tty-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'tty-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) await page.locator('button:has-text("TTY")').click() - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchTTY(true)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchTTY(true)) await page.locator('button[aria-checked="false"]:right-of(label:has-text("TTY"))').click() await wsSent @@ -118,10 +118,10 @@ test.describe('Image common config from editor', () => { }) test('Port should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'port-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'port-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -136,7 +136,7 @@ test.describe('Image common config from editor', () => { const internalInput = page.locator('input[placeholder="Internal"]') const externalInput = page.locator('input[placeholder="External"]') - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchPorts(internal, external)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchPorts(internal, external)) await internalInput.fill(internal) await externalInput.fill(external) await wsSent @@ -148,10 +148,10 @@ test.describe('Image common config from editor', () => { }) test('Port ranges should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'port-range-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'port-range-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -173,7 +173,7 @@ test.describe('Image common config from editor', () => { const wsSent = wsPatchSent( ws, wsRoute, - WS_TYPE_PATCH_IMAGE, + WS_TYPE_PATCH_CONFIG, wsPatchMatchPortRange(internalFrom, externalFrom, internalTo, externalTo), ) await internalInputFrom.fill(internalFrom) @@ -191,10 +191,10 @@ test.describe('Image common config from editor', () => { }) test('Secrets should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'secrets-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'secrets-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -204,7 +204,7 @@ test.describe('Image common config from editor', () => { const secret = 'secretName' const secretInput = page.locator('input[placeholder="SECRETS"] >> visible=true').nth(0) - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchSecret(secret, true)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchSecret(secret, true)) await secretInput.fill(secret) await page.getByRole('switch', { checked: false }).locator(':right-of(:text("Required"))').click() @@ -217,10 +217,10 @@ test.describe('Image common config from editor', () => { }) test('Commands should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'commands-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'commands-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -230,7 +230,7 @@ test.describe('Image common config from editor', () => { const command = 'sleep' const commandInput = page.locator('input[placeholder="Commands"] >> visible=true').nth(0) - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchCommand(command)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchCommand(command)) await commandInput.fill(command) await wsSent @@ -240,10 +240,10 @@ test.describe('Image common config from editor', () => { }) test('Arguments should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'arguments-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'arguments-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -253,7 +253,7 @@ test.describe('Image common config from editor', () => { const argument = '1234' const argumentInput = page.locator('input[placeholder="Arguments"] >> visible=true').nth(0) - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchArgument(argument)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchArgument(argument)) await argumentInput.fill(argument) await wsSent @@ -263,9 +263,9 @@ test.describe('Image common config from editor', () => { }) test('Routing should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'routing-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'routing-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -278,7 +278,7 @@ test.describe('Image common config from editor', () => { const internalInput = page.locator('input[placeholder="Internal"]') - let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchPorts(internal)) + let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchPorts(internal)) await internalInput.fill(internal) await wsSent @@ -293,7 +293,7 @@ test.describe('Image common config from editor', () => { wsSent = wsPatchSent( ws, wsRoute, - WS_TYPE_PATCH_IMAGE, + WS_TYPE_PATCH_CONFIG, wsPatchMatchRouting(domain, path, uploadLimit, stripPath, routedPort), ) await page.locator('input[placeholder="Domain"]').fill(domain) @@ -314,14 +314,14 @@ test.describe('Image common config from editor', () => { }) test('Environment should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( + const { projectId, versionId, imageConfigId } = await setup( page, 'environment-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG, ) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -331,7 +331,7 @@ test.describe('Image common config from editor', () => { const key = 'env-key' const value = 'env-value' - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchEnvironment(key, value)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchEnvironment(key, value)) await page.locator('input[placeholder="Key"]').first().fill(key) await page.locator('input[placeholder="Value"]').first().fill(value) await wsSent @@ -343,14 +343,14 @@ test.describe('Image common config from editor', () => { }) test('Config container should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( + const { projectId, versionId, imageConfigId } = await setup( page, 'config-container-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG, ) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -362,7 +362,7 @@ test.describe('Image common config from editor', () => { const volume = 'volume' const path = 'test/path/' - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchConfigContainer(img, volume, path, true)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchConfigContainer(img, volume, path, true)) await confDiv.getByLabel('Image').fill(img) await confDiv.getByLabel('Volume').fill(volume) await confDiv.getByLabel('Path').fill(path) @@ -378,14 +378,14 @@ test.describe('Image common config from editor', () => { }) test('Init containers should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( + const { projectId, versionId, imageConfigId } = await setup( page, 'init-container-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG, ) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -401,7 +401,7 @@ test.describe('Image common config from editor', () => { await page.locator('button:has-text("Init containers")').click() - let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE) + let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG) await page.locator(`[src="/plus.svg"]:right-of(label:has-text("Init containers"))`).first().click() await wsSent @@ -410,7 +410,7 @@ test.describe('Image common config from editor', () => { wsSent = wsPatchSent( ws, wsRoute, - WS_TYPE_PATCH_IMAGE, + WS_TYPE_PATCH_CONFIG, wsPatchMatchInitContainer(name, image, volName, volPath, arg, cmd, envKey, envVal), ) await confDiv.getByLabel('NAME').fill(name) @@ -436,16 +436,16 @@ test.describe('Image common config from editor', () => { }) test('Volume should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'volume-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'volume-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) await page.locator('button:has-text("Volume")').click() - let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE) + let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG) await page.locator(`[src="/plus.svg"]:right-of(label:has-text("Volume"))`).first().click() await wsSent @@ -454,7 +454,7 @@ test.describe('Image common config from editor', () => { const path = '/test/volume' const volumeClass = 'class' - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchVolume(name, size, path, volumeClass)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchVolume(name, size, path, volumeClass)) await page.getByLabel('Name').fill(name) await page.getByLabel('Size').fill(size) await page.getByLabel('Path').fill(path) @@ -470,20 +470,20 @@ test.describe('Image common config from editor', () => { }) test('Storage should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'storage-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'storage-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const storageName = 'image-editor-storage' const storageId = await createStorage(page, storageName, 'storage.com', '1234', '12345') const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) await page.locator('button:has-text("Volume")').click() - let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE) + let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG) await page.locator(`[src="/plus.svg"]:right-of(label:has-text("Volume"))`).first().click() await wsSent @@ -492,7 +492,7 @@ test.describe('Image common config from editor', () => { const path = '/storage/volume' const volumeClass = 'class' - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchVolume(volumeName, size, path, volumeClass)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchVolume(volumeName, size, path, volumeClass)) await page.getByLabel('Name').fill(volumeName) await page.getByLabel('Size').fill(size) await page.getByLabel('Path').fill(path) @@ -503,7 +503,7 @@ test.describe('Image common config from editor', () => { const bucketPath = '/storage/' await page.locator('button:has-text("Storage")').click() - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchStorage(storageId, bucketPath, volumeName)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchStorage(storageId, bucketPath, volumeName)) await storageDiv.locator(`button:has-text("${storageName}")`).click() await storageDiv.locator('input[placeholder="Bucket path"]').fill(bucketPath) await storageDiv.locator(`button:has-text("${volumeName}")`).click() diff --git a/web/crux-ui/e2e/with-login/container-config/common-json.spec.ts b/web/crux-ui/e2e/with-login/container-config/common-json.spec.ts index a3a91d5f2c..5432be017b 100644 --- a/web/crux-ui/e2e/with-login/container-config/common-json.spec.ts +++ b/web/crux-ui/e2e/with-login/container-config/common-json.spec.ts @@ -21,32 +21,32 @@ import { } from 'e2e/utils/websocket-match' import { createImage, createProject, createVersion } from '../../utils/projects' import { waitSocketRef, wsPatchSent } from '../../utils/websocket' -import { WS_TYPE_PATCH_IMAGE } from '@app/models' +import { WS_TYPE_PATCH_CONFIG } from '@app/models' const setup = async ( page: Page, projectName: string, versionName: string, imageName: string, -): Promise<{ projectId: string; versionId: string; imageId: string }> => { +): Promise<{ imageConfigId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') - const imageId = await createImage(page, projectId, versionId, imageName) + const imageConfigId = await createImage(page, projectId, versionId, imageName) - return { projectId, versionId, imageId } + return { imageConfigId } } test.describe.configure({ mode: 'parallel' }) test.describe('Image common config from JSON', () => { test('Container name should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'name-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'name-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const name = 'new-container-name' @@ -57,7 +57,7 @@ test.describe('Image common config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.name = name - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchContainerName(name)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchContainerName(name)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -67,13 +67,13 @@ test.describe('Image common config from JSON', () => { }) test('Expose strategy should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'expose-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'expose-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') await jsonEditorButton.click() @@ -82,7 +82,7 @@ test.describe('Image common config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.expose = 'exposeWithTls' - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchExpose('exposeWithTls')) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchExpose('exposeWithTls')) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -92,13 +92,13 @@ test.describe('Image common config from JSON', () => { }) test('User should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'user-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'user-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const user = 23 @@ -109,7 +109,7 @@ test.describe('Image common config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.user = user - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchUser(user)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchUser(user)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -119,13 +119,13 @@ test.describe('Image common config from JSON', () => { }) test('TTY should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'tty-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'tty-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') await jsonEditorButton.click() @@ -134,7 +134,7 @@ test.describe('Image common config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.tty = true - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchTTY(true)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchTTY(true)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -144,13 +144,13 @@ test.describe('Image common config from JSON', () => { }) test('Port should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'port-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'port-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') await jsonEditorButton.click() @@ -164,7 +164,7 @@ test.describe('Image common config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.ports = [{ internal: internalAsNumber, external: externalAsNumber }] - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchPorts(internal, external)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchPorts(internal, external)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -178,13 +178,13 @@ test.describe('Image common config from JSON', () => { }) test('Port ranges should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'port-range-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'port-range-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') await jsonEditorButton.click() @@ -210,7 +210,7 @@ test.describe('Image common config from JSON', () => { const wsSent = wsPatchSent( ws, wsRoute, - WS_TYPE_PATCH_IMAGE, + WS_TYPE_PATCH_CONFIG, wsPatchMatchPortRange(internalFrom, externalFrom, internalTo, externalTo), ) await jsonEditor.fill(JSON.stringify(json)) @@ -230,13 +230,13 @@ test.describe('Image common config from JSON', () => { }) test('Secrets should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'secrets-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'secrets-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const secret = 'secretName' const secretInput = page.locator('input[placeholder="SECRETS"] >> visible=true').nth(0) @@ -248,7 +248,7 @@ test.describe('Image common config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.secrets = [{ key: secret, required: true }] - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchSecret(secret, true)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchSecret(secret, true)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -259,13 +259,13 @@ test.describe('Image common config from JSON', () => { }) test('Commands should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'commands-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'commands-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const command = 'sleep' @@ -276,7 +276,7 @@ test.describe('Image common config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.commands = [command] - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchCommand(command)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchCommand(command)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -286,13 +286,13 @@ test.describe('Image common config from JSON', () => { }) test('Arguments should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'arguments-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'arguments-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const argument = '1234' @@ -303,7 +303,7 @@ test.describe('Image common config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.args = [argument] - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchArgument(argument)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchArgument(argument)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -313,12 +313,12 @@ test.describe('Image common config from JSON', () => { }) test('Routing should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'routing-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'routing-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') await jsonEditorButton.click() @@ -337,7 +337,7 @@ test.describe('Image common config from JSON', () => { const wsSent = wsPatchSent( ws, wsRoute, - WS_TYPE_PATCH_IMAGE, + WS_TYPE_PATCH_CONFIG, wsPatchMatchRouting(domain, path, uploadLimit, stripPath, port), ) await jsonEditor.fill(JSON.stringify(json)) @@ -352,12 +352,12 @@ test.describe('Image common config from JSON', () => { }) test('Environment should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'environment-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'environment-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const key = 'env-key' const value = 'env-value' @@ -369,7 +369,7 @@ test.describe('Image common config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.environment = { [key]: value } - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchEnvironment(key, value)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchEnvironment(key, value)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -380,17 +380,12 @@ test.describe('Image common config from JSON', () => { }) test('Config container should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( - page, - 'config-container-json', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'config-container-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const img = 'image' const volume = 'volume' @@ -403,7 +398,7 @@ test.describe('Image common config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.configContainer = { image: img, volume, path, keepFiles: true } - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchConfigContainer(img, volume, path, true)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchConfigContainer(img, volume, path, true)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -417,17 +412,12 @@ test.describe('Image common config from JSON', () => { }) test('Init containers should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( - page, - 'init-container-json', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'init-container-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const name = 'container-name' const image = 'image' @@ -458,7 +448,7 @@ test.describe('Image common config from JSON', () => { const wsSent = wsPatchSent( ws, wsRoute, - WS_TYPE_PATCH_IMAGE, + WS_TYPE_PATCH_CONFIG, wsPatchMatchInitContainer(name, image, volName, volPath, arg, cmd, envKey, envVal), ) await jsonEditor.fill(JSON.stringify(json)) @@ -478,12 +468,12 @@ test.describe('Image common config from JSON', () => { }) test('Volume should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'volume-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'volume-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') await jsonEditorButton.click() @@ -497,7 +487,7 @@ test.describe('Image common config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.volumes = [{ name, path, type: 'rwo', class: volumeClass, size }] - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchVolume(name, size, path, volumeClass)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchVolume(name, size, path, volumeClass)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -510,9 +500,9 @@ test.describe('Image common config from JSON', () => { }) test('Storage should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'storage-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + const { imageConfigId } = await setup(page, 'storage-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const volumeName = 'volume-name' const size = '1024' @@ -523,10 +513,10 @@ test.describe('Image common config from JSON', () => { const storageId = await createStorage(page, storageName, 'storage.com', '1234', '12345') const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) - await page.waitForSelector('h2:text-is("Image")') + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.waitForSelector('h2:text-is("Image config")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') await jsonEditorButton.click() @@ -536,7 +526,12 @@ test.describe('Image common config from JSON', () => { json.volumes = [{ name: volumeName, path, type: 'rwo', class: volumeClass, size }] json.storage = { storageId, bucket: bucketPath, path: volumeName } - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchStorage(storageId, bucketPath, volumeName)) + const wsSent = wsPatchSent( + ws, + wsRoute, + WS_TYPE_PATCH_CONFIG, + wsPatchMatchStorage(storageId, bucketPath, volumeName), + ) await jsonEditor.fill(JSON.stringify(json)) await wsSent diff --git a/web/crux-ui/e2e/with-login/container-config/container-config-filters.spec.ts b/web/crux-ui/e2e/with-login/container-config/container-config-filters.spec.ts index df1cae48d3..12d4e5598a 100644 --- a/web/crux-ui/e2e/with-login/container-config/container-config-filters.spec.ts +++ b/web/crux-ui/e2e/with-login/container-config/container-config-filters.spec.ts @@ -11,20 +11,20 @@ const setup = async ( ): Promise<{ projectId: string; versionId: string; imageId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') - const imageId = await createImage(page, projectId, versionId, imageName) + const imageConfigId = await createImage(page, projectId, versionId, imageName) return { projectId, versionId, - imageId, + imageConfigId, } } test.describe('Filters', () => { test('None should be selected by default', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'filter-all', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'filter-all', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const allButton = await page.locator('button:has-text("All")') @@ -34,9 +34,9 @@ test.describe('Filters', () => { }) test('All should not be selected if one of the main filters are not selected', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'filter-select', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'filter-select', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') await page.locator(`button:has-text("Common")`).first().click() @@ -47,9 +47,9 @@ test.describe('Filters', () => { }) test('Main filter should not be selected if one of its sub filters are not selected', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'sub-filter', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'sub-filter', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const subFilter = await page.locator(`button:has-text("Network mode")`) @@ -62,9 +62,9 @@ test.describe('Filters', () => { }) test('Config field should be invisible if its sub filter is not selected', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'sub-deselect', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'sub-deselect', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const subFilter = await page.locator(`button:has-text("Deployment strategy")`) diff --git a/web/crux-ui/e2e/with-login/container-config/docker-editor.spec.ts b/web/crux-ui/e2e/with-login/container-config/docker-editor.spec.ts index 810c454908..04b04e1698 100644 --- a/web/crux-ui/e2e/with-login/container-config/docker-editor.spec.ts +++ b/web/crux-ui/e2e/with-login/container-config/docker-editor.spec.ts @@ -10,7 +10,7 @@ import { wsPatchMatchRestartPolicy, } from 'e2e/utils/websocket-match' import { createImage, createProject, createVersion } from '../../utils/projects' -import { WS_TYPE_PATCH_IMAGE } from '@app/models' +import { WS_TYPE_PATCH_CONFIG } from '@app/models' const setup = async ( page: Page, @@ -20,28 +20,28 @@ const setup = async ( ): Promise<{ projectId: string; versionId: string; imageId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') - const imageId = await createImage(page, projectId, versionId, imageName) + const imageConfigId = await createImage(page, projectId, versionId, imageName) - return { projectId, versionId, imageId } + return { projectId, versionId, imageConfigId: imageConfigId } } test.describe('Image docker config from editor', () => { test('Network mode should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( + const { projectId, versionId, imageConfigId } = await setup( page, 'networkmode-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG, ) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) const mode = 'host' - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchNetworkMode(mode)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchNetworkMode(mode)) await page.locator(`div.grid:has(label:has-text("NETWORK MODE")) button:has-text("${mode}")`).click() await wsSent @@ -53,14 +53,14 @@ test.describe('Image docker config from editor', () => { }) test('Docker labels should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( + const { projectId, versionId, imageConfigId } = await setup( page, 'dockerlabel-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG, ) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -72,7 +72,7 @@ test.describe('Image docker config from editor', () => { await page.locator('button:has-text("Docker labels")').click() - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchDockerLabel(key, value)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchDockerLabel(key, value)) await keyInput.fill(key) await valueInput.fill(value) await wsSent @@ -84,19 +84,19 @@ test.describe('Image docker config from editor', () => { }) test('Restart policy should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( + const { projectId, versionId, imageConfigId } = await setup( page, 'restartpolicy-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG, ) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchRestartPolicy('always')) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchRestartPolicy('always')) await page.locator('div.grid:has(label:has-text("RESTART POLICY")) button:has-text("Always")').click() await wsSent @@ -108,9 +108,9 @@ test.describe('Image docker config from editor', () => { }) test('Log config should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'logconfig-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'logconfig-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -123,7 +123,7 @@ test.describe('Image docker config from editor', () => { const loggerConf = page.locator('div.grid:has(label:has-text("LOG CONFIG"))') - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchLogConfig(type, key, value)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchLogConfig(type, key, value)) await loggerConf.locator('input[placeholder="Key"]').first().fill(key) await loggerConf.locator('input[placeholder="Value"]').first().fill(value) await loggerConf.locator(`button:has-text("${type}")`).click() @@ -137,9 +137,9 @@ test.describe('Image docker config from editor', () => { }) test('Networks should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'networks-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'networks-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -148,7 +148,7 @@ test.describe('Image docker config from editor', () => { const network = '10.16.128.196' - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchNetwork(network)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchNetwork(network)) await page.locator('div.grid:has(label:has-text("NETWORKS")) input[placeholder="Network"]').first().fill(network) await wsSent diff --git a/web/crux-ui/e2e/with-login/container-config/docker-json.spec.ts b/web/crux-ui/e2e/with-login/container-config/docker-json.spec.ts index 9ba93f576f..507b3d2bc0 100644 --- a/web/crux-ui/e2e/with-login/container-config/docker-json.spec.ts +++ b/web/crux-ui/e2e/with-login/container-config/docker-json.spec.ts @@ -10,7 +10,7 @@ import { wsPatchMatchRestartPolicy, } from 'e2e/utils/websocket-match' import { createImage, createProject, createVersion } from '../../utils/projects' -import { WS_TYPE_PATCH_IMAGE } from '@app/models' +import { WS_TYPE_PATCH_CONFIG } from '@app/models' const setup = async ( page: Page, @@ -20,18 +20,18 @@ const setup = async ( ): Promise<{ projectId: string; versionId: string; imageId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') - const imageId = await createImage(page, projectId, versionId, imageName) + const imageConfigId = await createImage(page, projectId, versionId, imageName) - return { projectId, versionId, imageId } + return { projectId, versionId, imageConfigId: imageConfigId } } test.describe.configure({ mode: 'parallel' }) test.describe('Image docker config from JSON', () => { test('Network mode should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'networkmode-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'networkmode-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -45,7 +45,7 @@ test.describe('Image docker config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.networkMode = mode - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchNetworkMode(mode)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchNetworkMode(mode)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -57,9 +57,9 @@ test.describe('Image docker config from JSON', () => { }) test('Docker labels should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'dockerlabel-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'dockerlabel-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -74,7 +74,7 @@ test.describe('Image docker config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.dockerLabels = { [key]: value } - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchDockerLabel(key, value)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchDockerLabel(key, value)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -89,14 +89,14 @@ test.describe('Image docker config from JSON', () => { }) test('Restart policy should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( + const { projectId, versionId, imageConfigId } = await setup( page, 'restartpolicy-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG, ) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -108,7 +108,7 @@ test.describe('Image docker config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.restartPolicy = 'always' - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchRestartPolicy('always')) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchRestartPolicy('always')) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -120,9 +120,9 @@ test.describe('Image docker config from JSON', () => { }) test('Log config should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'logconfig-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'logconfig-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -138,7 +138,7 @@ test.describe('Image docker config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.logConfig = { driver: type, options: { [key]: value } } - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchLogConfig(type, key, value)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchLogConfig(type, key, value)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -151,9 +151,9 @@ test.describe('Image docker config from JSON', () => { }) test('Networks should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'networks-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'networks-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -169,7 +169,7 @@ test.describe('Image docker config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.networks = [network] - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchNetwork(network)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchNetwork(network)) await jsonEditor.fill(JSON.stringify(json)) await wsSent diff --git a/web/crux-ui/e2e/with-login/container-config/image-config-view-state.spec.ts b/web/crux-ui/e2e/with-login/container-config/image-config-view-state.spec.ts index 1a9c3e35d8..fde1249dc6 100644 --- a/web/crux-ui/e2e/with-login/container-config/image-config-view-state.spec.ts +++ b/web/crux-ui/e2e/with-login/container-config/image-config-view-state.spec.ts @@ -11,20 +11,20 @@ const setup = async ( ): Promise<{ projectId: string; versionId: string; imageId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') - const imageId = await createImage(page, projectId, versionId, imageName) + const imageConfigId = await createImage(page, projectId, versionId, imageName) return { projectId, versionId, - imageId, + imageId: imageConfigId, } } test.describe('View state', () => { test('Editor state should show the configuration fields', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'editor-state-conf', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'editor-state-conf', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const editorButton = await page.waitForSelector('button:has-text("Editor")') @@ -39,9 +39,9 @@ test.describe('View state', () => { }) test('JSON state should show the json editor', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'editor-state-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'editor-state-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') diff --git a/web/crux-ui/e2e/with-login/container-config/kubernetes-editor.spec.ts b/web/crux-ui/e2e/with-login/container-config/kubernetes-editor.spec.ts index 5a6cd1a3ff..ef3d1b0e1c 100644 --- a/web/crux-ui/e2e/with-login/container-config/kubernetes-editor.spec.ts +++ b/web/crux-ui/e2e/with-login/container-config/kubernetes-editor.spec.ts @@ -18,7 +18,7 @@ import { } from 'e2e/utils/websocket-match' import { createImage, createProject, createVersion } from '../../utils/projects' import { waitSocketRef, wsPatchSent } from '../../utils/websocket' -import { WS_TYPE_PATCH_IMAGE } from '@app/models' +import { WS_TYPE_PATCH_CONFIG } from '@app/models' const setup = async ( page: Page, @@ -28,14 +28,14 @@ const setup = async ( ): Promise<{ projectId: string; versionId: string; imageId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') - const imageId = await createImage(page, projectId, versionId, imageName) + const imageConfigId = await createImage(page, projectId, versionId, imageName) - return { projectId, versionId, imageId } + return { projectId, versionId, imageConfigId: imageConfigId } } test.describe('Image kubernetes config from editor', () => { test('Deployment strategy should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( + const { projectId, versionId, imageConfigId } = await setup( page, 'deployment-strategy-editor', '1.0.0', @@ -43,14 +43,14 @@ test.describe('Image kubernetes config from editor', () => { ) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) const strategy = 'rolling' - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchDeploymentStrategy(strategy)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchDeploymentStrategy(strategy)) await page.locator(`button:has-text("${strategy}")`).click() await wsSent @@ -60,7 +60,7 @@ test.describe('Image kubernetes config from editor', () => { }) test('Custom headers should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( + const { projectId, versionId, imageConfigId } = await setup( page, 'custom-headers-editor', '1.0.0', @@ -68,7 +68,7 @@ test.describe('Image kubernetes config from editor', () => { ) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -80,7 +80,7 @@ test.describe('Image kubernetes config from editor', () => { .locator('div.grid:has(label:has-text("CUSTOM HEADERS")) input[placeholder="Header name"]') .first() - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchCustomHeader(header)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchCustomHeader(header)) await input.fill(header) await wsSent @@ -90,7 +90,7 @@ test.describe('Image kubernetes config from editor', () => { }) test('Proxy headers should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( + const { projectId, versionId, imageConfigId } = await setup( page, 'proxy-headers-editor', '1.0.0', @@ -98,14 +98,14 @@ test.describe('Image kubernetes config from editor', () => { ) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) await page.locator('button:has-text("Proxy headers")').click() - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchProxyHeader(true)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchProxyHeader(true)) await page.locator('button[aria-checked="false"]:right-of(label:has-text("PROXY HEADERS"))').click() await wsSent @@ -115,7 +115,7 @@ test.describe('Image kubernetes config from editor', () => { }) test('Load balancer should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( + const { projectId, versionId, imageConfigId } = await setup( page, 'load-balancer-editor', '1.0.0', @@ -123,7 +123,7 @@ test.describe('Image kubernetes config from editor', () => { ) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -133,11 +133,11 @@ test.describe('Image kubernetes config from editor', () => { const key = 'balancer-key' const value = 'balancer-value' - let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchLoadBalancer(true)) + let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchLoadBalancer(true)) await page.locator('button[aria-checked="false"]:right-of(label:has-text("USE LOAD BALANCER"))').click() await wsSent - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchLBAnnotations(key, value)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchLBAnnotations(key, value)) await page.locator('div.grid:has(label:has-text("USE LOAD BALANCER")) input[placeholder="Key"]').first().fill(key) await page .locator('div.grid:has(label:has-text("USE LOAD BALANCER")) input[placeholder="Value"]') @@ -159,7 +159,7 @@ test.describe('Image kubernetes config from editor', () => { }) test('Health check config should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( + const { projectId, versionId, imageConfigId } = await setup( page, 'health-check-editor', '1.0.0', @@ -167,7 +167,7 @@ test.describe('Image kubernetes config from editor', () => { ) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -184,7 +184,7 @@ test.describe('Image kubernetes config from editor', () => { const wsSent = wsPatchSent( ws, wsRoute, - WS_TYPE_PATCH_IMAGE, + WS_TYPE_PATCH_CONFIG, wsPatchMatchHealthCheck(port, liveness, readiness, startup), ) await hcConf.locator('input[placeholder="Port"]').fill(port.toString()) @@ -202,14 +202,14 @@ test.describe('Image kubernetes config from editor', () => { }) test('Resource config should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( + const { projectId, versionId, imageConfigId } = await setup( page, 'resource-config-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG, ) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -226,7 +226,7 @@ test.describe('Image kubernetes config from editor', () => { const wsSent = wsPatchSent( ws, wsRoute, - WS_TYPE_PATCH_IMAGE, + WS_TYPE_PATCH_CONFIG, wsPatchMatchResourceConfig(cpuLimits, cpuRequests, memoryLimits, memoryRequests), ) await rsConf.locator('input').nth(0).fill(cpuLimits) @@ -247,10 +247,10 @@ test.describe('Image kubernetes config from editor', () => { page.locator(`div.max-h-128 > div:nth-child(2):near(label:has-text("${category}"))`) test('Labels should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'labels-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'labels-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -264,15 +264,15 @@ test.describe('Image kubernetes config from editor', () => { const serviceDiv = await getCategoryDiv('Service', page) const ingressDiv = await getCategoryDiv('Ingress', page) - let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchDeploymentLabel(key, value)) + let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchDeploymentLabel(key, value)) await deploymentDiv.locator('input[placeholder="Key"]').first().fill(key) await deploymentDiv.locator('input[placeholder="Value"]').first().fill(value) await wsSent - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchServiceLabel(key, value)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchServiceLabel(key, value)) await serviceDiv.locator('input[placeholder="Key"]').first().fill(key) await serviceDiv.locator('input[placeholder="Value"]').first().fill(value) await wsSent - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchIngressLabel(key, value)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchIngressLabel(key, value)) await ingressDiv.locator('input[placeholder="Key"]').first().fill(key) await ingressDiv.locator('input[placeholder="Value"]').first().fill(value) await wsSent @@ -288,7 +288,7 @@ test.describe('Image kubernetes config from editor', () => { }) test('Annotations should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( + const { projectId, versionId, imageConfigId } = await setup( page, 'annotations-editor', '1.0.0', @@ -296,7 +296,7 @@ test.describe('Image kubernetes config from editor', () => { ) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -310,15 +310,15 @@ test.describe('Image kubernetes config from editor', () => { const serviceDiv = await getCategoryDiv('Service', page) const ingressDiv = await getCategoryDiv('Ingress', page) - let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchDeploymentAnnotations(key, value)) + let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchDeploymentAnnotations(key, value)) await deploymentDiv.locator('input[placeholder="Key"]').first().fill(key) await deploymentDiv.locator('input[placeholder="Value"]').first().fill(value) await wsSent - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchServiceAnnotations(key, value)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchServiceAnnotations(key, value)) await serviceDiv.locator('input[placeholder="Key"]').first().fill(key) await serviceDiv.locator('input[placeholder="Value"]').first().fill(value) await wsSent - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchIngressAnnotations(key, value)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchIngressAnnotations(key, value)) await ingressDiv.locator('input[placeholder="Key"]').first().fill(key) await ingressDiv.locator('input[placeholder="Value"]').first().fill(value) await wsSent diff --git a/web/crux-ui/e2e/with-login/container-config/kubernetes-json.spec.ts b/web/crux-ui/e2e/with-login/container-config/kubernetes-json.spec.ts index 9fa54071af..166bf48c6f 100644 --- a/web/crux-ui/e2e/with-login/container-config/kubernetes-json.spec.ts +++ b/web/crux-ui/e2e/with-login/container-config/kubernetes-json.spec.ts @@ -18,7 +18,7 @@ import { } from 'e2e/utils/websocket-match' import { createImage, createProject, createVersion } from '../../utils/projects' import { waitSocketRef, wsPatchSent } from '../../utils/websocket' -import { WS_TYPE_PATCH_IMAGE } from '@app/models' +import { WS_TYPE_PATCH_CONFIG } from '@app/models' const setup = async ( page: Page, @@ -28,14 +28,14 @@ const setup = async ( ): Promise<{ projectId: string; versionId: string; imageId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') - const imageId = await createImage(page, projectId, versionId, imageName) + const imageConfigId = await createImage(page, projectId, versionId, imageName) - return { projectId, versionId, imageId } + return { projectId, versionId, imageConfigId: imageConfigId } } test.describe('Image kubernetes config from JSON', () => { test('Deployment strategy should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( + const { projectId, versionId, imageConfigId } = await setup( page, 'deployment-strategy-json', '1.0.0', @@ -43,7 +43,7 @@ test.describe('Image kubernetes config from JSON', () => { ) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -57,7 +57,7 @@ test.describe('Image kubernetes config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.deploymentStrategy = strategy - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchDeploymentStrategy(strategy)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchDeploymentStrategy(strategy)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -67,7 +67,7 @@ test.describe('Image kubernetes config from JSON', () => { }) test('Custom headers should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( + const { projectId, versionId, imageConfigId } = await setup( page, 'custom-headers-json', '1.0.0', @@ -75,7 +75,7 @@ test.describe('Image kubernetes config from JSON', () => { ) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -89,7 +89,7 @@ test.describe('Image kubernetes config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.customHeaders = [header] - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchCustomHeader(header)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchCustomHeader(header)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -102,7 +102,7 @@ test.describe('Image kubernetes config from JSON', () => { }) test('Proxy headers should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( + const { projectId, versionId, imageConfigId } = await setup( page, 'proxy-headers-json', '1.0.0', @@ -110,7 +110,7 @@ test.describe('Image kubernetes config from JSON', () => { ) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -122,7 +122,7 @@ test.describe('Image kubernetes config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.proxyHeaders = true - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchProxyHeader(true)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchProxyHeader(true)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -132,7 +132,7 @@ test.describe('Image kubernetes config from JSON', () => { }) test('Load balancer should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( + const { projectId, versionId, imageConfigId } = await setup( page, 'load-balancer-json', '1.0.0', @@ -140,7 +140,7 @@ test.describe('Image kubernetes config from JSON', () => { ) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -155,11 +155,11 @@ test.describe('Image kubernetes config from JSON', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.useLoadBalancer = true - let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchLoadBalancer(true)) + let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchLoadBalancer(true)) await jsonEditor.fill(JSON.stringify(json)) await wsSent json.extraLBAnnotations = { [key]: value } - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchLBAnnotations(key, value)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchLBAnnotations(key, value)) await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -177,10 +177,10 @@ test.describe('Image kubernetes config from JSON', () => { }) test('Health check config should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'health-check-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'health-check-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -200,7 +200,7 @@ test.describe('Image kubernetes config from JSON', () => { const wsSent = wsPatchSent( ws, wsRoute, - WS_TYPE_PATCH_IMAGE, + WS_TYPE_PATCH_CONFIG, wsPatchMatchHealthCheck(port, liveness, readiness, startup), ) await jsonEditor.fill(JSON.stringify(json)) @@ -216,7 +216,7 @@ test.describe('Image kubernetes config from JSON', () => { }) test('Resource config should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup( + const { projectId, versionId, imageConfigId } = await setup( page, 'resource-config-json', '1.0.0', @@ -224,7 +224,7 @@ test.describe('Image kubernetes config from JSON', () => { ) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -247,7 +247,7 @@ test.describe('Image kubernetes config from JSON', () => { const wsSent = wsPatchSent( ws, wsRoute, - WS_TYPE_PATCH_IMAGE, + WS_TYPE_PATCH_CONFIG, wsPatchMatchResourceConfig(cpuLimits, cpuRequests, memoryLimits, memoryRequests), ) await jsonEditor.fill(JSON.stringify(json)) @@ -266,10 +266,10 @@ test.describe('Image kubernetes config from JSON', () => { page.locator(`div.max-h-128 > div:nth-child(2):near(label:has-text("${category}"))`) test('Labels should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'labels-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'labels-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -283,15 +283,15 @@ test.describe('Image kubernetes config from JSON', () => { const jsonEditor = await page.locator('textarea') const json = JSON.parse(await jsonEditor.inputValue()) - let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchDeploymentLabel(key, value)) + let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchDeploymentLabel(key, value)) json.labels = { deployment: { [key]: value } } await jsonEditor.fill(JSON.stringify(json)) await wsSent - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchServiceLabel(key, value)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchServiceLabel(key, value)) json.labels = { deployment: { [key]: value }, service: { [key]: value } } await jsonEditor.fill(JSON.stringify(json)) await wsSent - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchIngressLabel(key, value)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchIngressLabel(key, value)) json.labels = { deployment: { [key]: value }, service: { [key]: value }, ingress: { [key]: value } } await jsonEditor.fill(JSON.stringify(json)) await wsSent @@ -310,10 +310,10 @@ test.describe('Image kubernetes config from JSON', () => { }) test('Annotations should be saved', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'annotations-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'annotations-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -327,15 +327,15 @@ test.describe('Image kubernetes config from JSON', () => { const jsonEditor = await page.locator('textarea') const json = JSON.parse(await jsonEditor.inputValue()) - let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchDeploymentAnnotations(key, value)) + let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchDeploymentAnnotations(key, value)) json.annotations = { deployment: { [key]: value } } await jsonEditor.fill(JSON.stringify(json)) await wsSent - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchServiceAnnotations(key, value)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchServiceAnnotations(key, value)) json.annotations = { deployment: { [key]: value }, service: { [key]: value } } await jsonEditor.fill(JSON.stringify(json)) await wsSent - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchIngressAnnotations(key, value)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchIngressAnnotations(key, value)) json.annotations = { deployment: { [key]: value }, service: { [key]: value }, ingress: { [key]: value } } await jsonEditor.fill(JSON.stringify(json)) await wsSent diff --git a/web/crux-ui/e2e/with-login/deployment/deployment-copy.spec.ts b/web/crux-ui/e2e/with-login/deployment/deployment-copy.spec.ts index 72b356a475..76e8509138 100644 --- a/web/crux-ui/e2e/with-login/deployment/deployment-copy.spec.ts +++ b/web/crux-ui/e2e/with-login/deployment/deployment-copy.spec.ts @@ -1,4 +1,4 @@ -import { WS_TYPE_PATCH_IMAGE, WS_TYPE_PATCH_INSTANCE } from '@app/models' +import { WS_TYPE_PATCH_CONFIG, WS_TYPE_PATCH_INSTANCE } from '@app/models' import { Page, expect } from '@playwright/test' import { wsPatchMatchEverySecret, wsPatchMatchNonNullSecretValues } from 'e2e/utils/websocket-match' import { DAGENT_NODE, NGINX_TEST_IMAGE_WITH_TAG, TEAM_ROUTES, waitForURLExcept } from '../../utils/common' @@ -21,7 +21,7 @@ const addSecretToImage = async ( secretKeys: string[], ): Promise => { const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -33,7 +33,7 @@ const addSecretToImage = async ( const json = JSON.parse(await jsonEditor.inputValue()) json.secrets = secretKeys.map(key => ({ key, required: false })) - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchEverySecret(secretKeys)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchEverySecret(secretKeys)) await jsonEditor.fill(JSON.stringify(json)) await wsSent } @@ -73,9 +73,9 @@ test.describe('Deployment Copy', () => { await createNode(page, newNodeName) const versionId = await createVersion(page, projectId, '0.1.0', 'Incremental') - const imageId = await createImage(page, projectId, versionId, NGINX_TEST_IMAGE_WITH_TAG) + const imageConfigId = await createImage(page, projectId, versionId, NGINX_TEST_IMAGE_WITH_TAG) - await addSecretToImage(page, projectId, versionId, imageId, secretKeys) + await addSecretToImage(page, projectId, versionId, imageConfigId, secretKeys) const { id: deploymentId } = await addDeploymentToVersion(page, projectId, versionId, DAGENT_NODE, { prefix: originalPrefix, diff --git a/web/crux-ui/e2e/with-login/deployment/deployment-copyability-versioned.spec.ts b/web/crux-ui/e2e/with-login/deployment/deployment-copyability-versioned.spec.ts index 39b799f2d2..7a78d13bd4 100644 --- a/web/crux-ui/e2e/with-login/deployment/deployment-copyability-versioned.spec.ts +++ b/web/crux-ui/e2e/with-login/deployment/deployment-copyability-versioned.spec.ts @@ -1,11 +1,11 @@ -import { ProjectType, WS_TYPE_PATCH_IMAGE } from '@app/models' +import { ProjectType, WS_TYPE_PATCH_CONFIG } from '@app/models' import { Page, expect } from '@playwright/test' import { NGINX_TEST_IMAGE_WITH_TAG, TEAM_ROUTES, waitForURLExcept } from '../../utils/common' import { deployWithDagent } from '../../utils/node-helper' import { createNode } from '../../utils/nodes' import { addDeploymentToVersion, - createImage, + createImage as imageConfigId, createProject, createVersion, fillDeploymentPrefix, @@ -39,7 +39,7 @@ test.describe('Versioned Project', () => { const { projectId } = await setup(page, nodeName, projectName, 'versioned') const versionId = await createVersion(page, projectId, '0.1.0', 'Incremental') - await createImage(page, projectId, versionId, NGINX_TEST_IMAGE_WITH_TAG) + await imageConfigId(page, projectId, versionId, NGINX_TEST_IMAGE_WITH_TAG) const { id: deploymentId } = await addDeploymentToVersion(page, projectId, versionId, nodeName, { prefix }) await page.goto(TEAM_ROUTES.deployment.details(deploymentId)) @@ -66,7 +66,7 @@ test.describe('Versioned Project', () => { const { projectId } = await setup(page, nodeName, projectName, 'versioned') const versionId = await createVersion(page, projectId, '0.1.0', 'Incremental') - await createImage(page, projectId, versionId, NGINX_TEST_IMAGE_WITH_TAG) + await imageConfigId(page, projectId, versionId, NGINX_TEST_IMAGE_WITH_TAG) const { id: deploymentId } = await addDeploymentToVersion(page, projectId, versionId, nodeName, { prefix }) await page.goto(TEAM_ROUTES.deployment.details(deploymentId)) @@ -93,7 +93,7 @@ test.describe('Versioned Project', () => { const { projectId } = await setup(page, nodeName, projectName, 'versioned') const versionId = await createVersion(page, projectId, '1.0.0', 'Incremental') - await createImage(page, projectId, versionId, NGINX_TEST_IMAGE_WITH_TAG) + await imageConfigId(page, projectId, versionId, NGINX_TEST_IMAGE_WITH_TAG) await addDeploymentToVersion(page, projectId, versionId, nodeName, { prefix }) await page.goto(TEAM_ROUTES.deployment.list()) @@ -118,10 +118,10 @@ test.describe('Versioned Project', () => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, '0.1.0', 'Incremental') - const imageId = await createImage(page, projectId, versionId, NGINX_TEST_IMAGE_WITH_TAG) + const imageId = await imageConfigId(page, projectId, versionId, NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -145,7 +145,7 @@ test.describe('Versioned Project', () => { useParentConfig: false, }, ] - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG) await jsonContainer.fill(JSON.stringify(configObject)) await wsSent diff --git a/web/crux-ui/e2e/with-login/deployment/deployment-deletability.spec.ts b/web/crux-ui/e2e/with-login/deployment/deployment-deletability.spec.ts index 818ac7305d..62ba1da135 100644 --- a/web/crux-ui/e2e/with-login/deployment/deployment-deletability.spec.ts +++ b/web/crux-ui/e2e/with-login/deployment/deployment-deletability.spec.ts @@ -4,17 +4,17 @@ import { NGINX_TEST_IMAGE_WITH_TAG, TEAM_ROUTES } from 'e2e/utils/common' import { deployWithDagent } from '../../utils/node-helper' import { addImageToVersion, createImage, createProject, createVersion } from '../../utils/projects' import { waitSocketRef, wsPatchSent } from '../../utils/websocket' -import { WS_TYPE_PATCH_IMAGE } from '@app/models' +import { WS_TYPE_PATCH_CONFIG } from '@app/models' test('In progress deployment should be not deletable', async ({ page }) => { const projectName = 'project-delete-test-1' const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, '0.1.0', 'Incremental') - const imageId = await createImage(page, projectId, versionId, 'nginx') + const imageConfigId = await createImage(page, projectId, versionId, 'nginx') const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -38,7 +38,7 @@ test('In progress deployment should be not deletable', async ({ page }) => { useParentConfig: false, }, ] - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG) await jsonContainer.fill(JSON.stringify(configObject)) await wsSent diff --git a/web/crux-ui/e2e/with-login/image-config.spec.ts b/web/crux-ui/e2e/with-login/image-config.spec.ts index 88d17dcd28..a03b93299d 100644 --- a/web/crux-ui/e2e/with-login/image-config.spec.ts +++ b/web/crux-ui/e2e/with-login/image-config.spec.ts @@ -1,32 +1,32 @@ +import { WS_TYPE_PATCH_CONFIG } from '@app/models' import { expect, Page } from '@playwright/test' -import { test } from '../utils/test.fixture' import { NGINX_TEST_IMAGE_WITH_TAG, screenshotPath, TEAM_ROUTES } from '../utils/common' import { createImage, createProject, createVersion } from '../utils/projects' +import { test } from '../utils/test.fixture' import { waitSocketRef, wsPatchSent } from '../utils/websocket' -import { WS_TYPE_PATCH_IMAGE } from '@app/models' const setup = async ( page: Page, projectName: string, versionName: string, imageName: string, -): Promise<{ projectId: string; versionId: string; imageId: string }> => { +): Promise<{ projectId: string; versionId: string; imageConfigId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') - const imageId = await createImage(page, projectId, versionId, imageName) + const imageConfigId = await createImage(page, projectId, versionId, imageName) return { projectId, versionId, - imageId, + imageConfigId, } } test.describe('View state', () => { test('Editor state should show the configuration fields', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'editor-state-conf', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'editor-state-conf', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const editorButton = await page.waitForSelector('button:has-text("Editor")') @@ -40,9 +40,9 @@ test.describe('View state', () => { }) test('JSON state should show the json editor', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'editor-state-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'editor-state-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') @@ -57,9 +57,9 @@ test.describe('View state', () => { test.describe('Filters', () => { test('None should be selected by default', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'filter-all', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'filter-all', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const allButton = await page.locator('button:has-text("All")') @@ -69,9 +69,9 @@ test.describe('Filters', () => { }) test('All should not be selected if one of the main filters are not selected', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'filter-select', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'filter-select', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') await page.locator(`button:has-text("Common")`).first().click() @@ -82,9 +82,9 @@ test.describe('Filters', () => { }) test('Main filter should not be selected if one of its sub filters are not selected', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'sub-filter', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'sub-filter', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const subFilter = await page.locator(`button:has-text("Network mode")`) @@ -96,9 +96,9 @@ test.describe('Filters', () => { }) test('Config field should be invisible if its sub filter is not selected', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'sub-deselect', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'sub-deselect', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const subFilter = await page.locator(`button:has-text("Deployment strategy")`) @@ -119,17 +119,17 @@ const wsPatchMatchPorts = (internalPort: string, externalPort?: string) => (payl test.describe('Image configurations', () => { test('Port should be saved after adding it from the config field', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'port-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'port-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) await page.locator('button:has-text("Ports")').click() - let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE) + let wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG) const addPortsButton = await page.locator(`[src="/plus.svg"]:right-of(label:has-text("Ports"))`).first() await addPortsButton.click() await wsSent @@ -140,7 +140,7 @@ test.describe('Image configurations', () => { const internalInput = page.locator('input[placeholder="Internal"]') const externalInput = page.locator('input[placeholder="External"]') - wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchPorts(internal, external)) + wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchPorts(internal, external)) await internalInput.type(internal) await externalInput.type(external) await wsSent @@ -152,10 +152,10 @@ test.describe('Image configurations', () => { }) test('Port should be saved after adding it from the json editor', async ({ page }) => { - const { projectId, versionId, imageId } = await setup(page, 'port-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { projectId, versionId, imageConfigId } = await setup(page, 'port-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) + await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) @@ -172,7 +172,7 @@ test.describe('Image configurations', () => { const json = JSON.parse(await jsonEditor.inputValue()) json.ports = [{ internal: internalAsNumber, external: externalAsNumber }] - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE, wsPatchMatchPorts(internal, external)) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchPorts(internal, external)) await jsonEditor.fill(JSON.stringify(json)) await wsSent diff --git a/web/crux-ui/e2e/with-login/resource-copy.spec.ts b/web/crux-ui/e2e/with-login/resource-copy.spec.ts index 86dd95955b..947ec6be40 100644 --- a/web/crux-ui/e2e/with-login/resource-copy.spec.ts +++ b/web/crux-ui/e2e/with-login/resource-copy.spec.ts @@ -1,4 +1,4 @@ -import { WS_TYPE_PATCH_IMAGE, WS_TYPE_PATCH_INSTANCE } from '@app/models' +import { WS_TYPE_PATCH_CONFIG, WS_TYPE_PATCH_INSTANCE } from '@app/models' import { expect } from '@playwright/test' import { DAGENT_NODE, NGINX_TEST_IMAGE_WITH_TAG, TEAM_ROUTES, waitForURLExcept } from 'e2e/utils/common' import { addPortsToContainerConfig } from 'e2e/utils/container-config' @@ -51,17 +51,17 @@ test.describe('Deleting default version', () => { const projectId = await createProject(page, projectName, 'versioned') const defaultVersionName = 'default-version' const defaultVersionId = await createVersion(page, projectId, defaultVersionName, 'Rolling') - const defaultVersionImageId = await createImage(page, projectId, defaultVersionId, NGINX_TEST_IMAGE_WITH_TAG) + const imageConfigId = await createImage(page, projectId, defaultVersionId, NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(defaultVersionId, defaultVersionImageId)) + await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(defaultVersionId, imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(defaultVersionId) const internal = '1000' const external = '2000' - await addPortsToContainerConfig(page, ws, wsRoute, WS_TYPE_PATCH_IMAGE, internal, external) + await addPortsToContainerConfig(page, ws, wsRoute, WS_TYPE_PATCH_CONFIG, internal, external) const newVersionId = await createVersion(page, projectId, 'new-version', 'Rolling') diff --git a/web/crux/src/app/container/container-config.service.ts b/web/crux/src/app/container/container-config.service.ts index f0c05a9bde..0c736e400e 100644 --- a/web/crux/src/app/container/container-config.service.ts +++ b/web/crux/src/app/container/container-config.service.ts @@ -259,7 +259,17 @@ export default class ContainerConfigService { }) } - const data: ContainerConfigData = req.config ?? {} + const config = await this.prisma.containerConfig.findUnique({ + where: { + id: configId, + }, + }) + + const data: ContainerConfigData = this.mapper.configDtoToConfigData( + config as any as ContainerConfigData, + req.config ?? {}, + ) + if (req.resetSection) { data[req.resetSection] = null } From f42e87fa5a14ccbc5ae3fceb5cb16bd17c443f23 Mon Sep 17 00:00:00 2001 From: Mate Vago Date: Wed, 4 Dec 2024 12:20:03 +0100 Subject: [PATCH 29/32] build: disable e2e --- .github/workflows/deploy_external.yaml | 2 +- .github/workflows/product_builder.yaml | 32 +++++++++++++------------- web/crux-ui/package.json | 2 +- web/crux/package.json | 2 +- 4 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/deploy_external.yaml b/.github/workflows/deploy_external.yaml index 1702f0c27c..cd8bca6661 100644 --- a/.github/workflows/deploy_external.yaml +++ b/.github/workflows/deploy_external.yaml @@ -5,7 +5,7 @@ on: workflows: - 'product_builder' branches: - - 'develop' + - 'feat/release-candidate' types: - completed permissions: diff --git a/.github/workflows/product_builder.yaml b/.github/workflows/product_builder.yaml index 0046302227..562aa2054d 100644 --- a/.github/workflows/product_builder.yaml +++ b/.github/workflows/product_builder.yaml @@ -69,9 +69,9 @@ jobs: crux: ${{ steps.filter.outputs.crux }} cruxui: ${{ steps.filter.outputs.cruxui }} kratos: ${{ steps.filter.outputs.kratos }} - tag: ${{ steps.settag.outputs.tag }} + tag: "0.15.0-rc" # ${{ steps.settag.outputs.tag }} extratag: ${{ steps.settag.outputs.extratag }} - version: ${{ steps.settag.outputs.version }} + version: "0.15.0-rc" # ${{ steps.settag.outputs.version }} minorversion: ${{ steps.settag.outputs.minorversion }} release: ${{ steps.release.outputs.release }} steps: @@ -669,7 +669,7 @@ jobs: defaults: run: working-directory: ${{ env.GOLANG_WORKING_DIRECTORY }} - needs: [gather_changes, e2e] + needs: [gather_changes] if: | always() && (github.ref_name == 'develop' || github.ref_name == 'main' || github.ref_type == 'tag') && @@ -735,16 +735,16 @@ jobs: container: image: ghcr.io/dyrector-io/dyrectorio/builder-images/signer:2 needs: [gather_changes, go_push] - if: | - always() && - (github.ref_name == 'develop' || github.ref_name == 'main' || github.ref_type == 'tag') && - needs.e2e.result == 'success' && - needs.go_build.result == 'success' && - (needs.crux_build.result == 'success' || needs.crux_build.result == 'skipped') && - (needs.crux-ui_build.result == 'success' || needs.crux-ui_build.result == 'skipped') && - (needs.kratos_build.result == 'success' || needs.kratos_build.result == 'skipped') && - needs.conventional_commits.result == 'success' && - needs.gather_changes.result == 'success' && needs.go_push.result == 'success' + # if: | + # always() && + # (github.ref_name == 'develop' || github.ref_name == 'main' || github.ref_type == 'tag') && + # needs.e2e.result == 'success' && + # needs.go_build.result == 'success' && + # (needs.crux_build.result == 'success' || needs.crux_build.result == 'skipped') && + # (needs.crux-ui_build.result == 'success' || needs.crux-ui_build.result == 'skipped') && + # (needs.kratos_build.result == 'success' || needs.kratos_build.result == 'skipped') && + # needs.conventional_commits.result == 'success' && + # needs.gather_changes.result == 'success' && needs.go_push.result == 'success' environment: Workflow - Protected steps: - name: Login to GHCR @@ -800,7 +800,7 @@ jobs: runs-on: ubuntu-22.04 container: image: ghcr.io/dyrector-io/dyrectorio/builder-images/signer:2 - needs: [crux_build, e2e, gather_changes] + needs: [crux_build, gather_changes] if: | always() && (github.ref_name == 'develop' || github.ref_name == 'main' || github.ref_type == 'tag') && @@ -863,7 +863,7 @@ jobs: runs-on: ubuntu-22.04 container: image: ghcr.io/dyrector-io/dyrectorio/builder-images/signer:2 - needs: [crux-ui_build, e2e, gather_changes] + needs: [crux-ui_build, gather_changes] if: | always() && (github.ref_name == 'develop' || github.ref_name == 'main' || github.ref_type == 'tag') && @@ -926,7 +926,7 @@ jobs: runs-on: ubuntu-22.04 container: image: ghcr.io/dyrector-io/dyrectorio/builder-images/signer:2 - needs: [kratos_build, e2e, gather_changes] + needs: [kratos_build, gather_changes] if: | always() && (github.ref_name == 'develop' || github.ref_name == 'main' || github.ref_type == 'tag') && diff --git a/web/crux-ui/package.json b/web/crux-ui/package.json index 2ae23b4549..83f8ca63d4 100644 --- a/web/crux-ui/package.json +++ b/web/crux-ui/package.json @@ -1,6 +1,6 @@ { "name": "crux-ui", - "version": "0.14.1", + "version": "0.15.0-rc", "description": "Open-source delivery platform that helps developers to deliver applications efficiently by simplifying software releases and operations in any environment.", "author": "dyrector.io", "private": true, diff --git a/web/crux/package.json b/web/crux/package.json index 4c92c568b9..936f801814 100644 --- a/web/crux/package.json +++ b/web/crux/package.json @@ -1,6 +1,6 @@ { "name": "crux", - "version": "0.14.1", + "version": "0.15.0-rc", "description": "Open-source delivery platform that helps developers to deliver applications efficiently by simplifying software releases and operations in any environment.", "author": "dyrector.io", "private": true, From 44d73cade90212288fde7984a41ea1e8629cb132 Mon Sep 17 00:00:00 2001 From: Mate Vago Date: Wed, 4 Dec 2024 12:24:51 +0100 Subject: [PATCH 30/32] fix: remove e2e need --- .github/workflows/product_builder.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/product_builder.yaml b/.github/workflows/product_builder.yaml index 562aa2054d..9df64e9f4f 100644 --- a/.github/workflows/product_builder.yaml +++ b/.github/workflows/product_builder.yaml @@ -673,7 +673,6 @@ jobs: if: | always() && (github.ref_name == 'develop' || github.ref_name == 'main' || github.ref_type == 'tag') && - needs.e2e.result == 'success' && needs.go_build.result == 'success' && (needs.crux_build.result == 'success' || needs.crux_build.result == 'skipped') && (needs.crux-ui_build.result == 'success' || needs.crux-ui_build.result == 'skipped') && @@ -804,7 +803,6 @@ jobs: if: | always() && (github.ref_name == 'develop' || github.ref_name == 'main' || github.ref_type == 'tag') && - needs.e2e.result == 'success' && (needs.go_build.result == 'success' || needs.go_build.result == 'skipped') && needs.crux_build.result == 'success' && (needs.crux-ui_build.result == 'success' || needs.crux-ui_build.result == 'skipped') && @@ -867,7 +865,6 @@ jobs: if: | always() && (github.ref_name == 'develop' || github.ref_name == 'main' || github.ref_type == 'tag') && - needs.e2e.result == 'success' && (needs.go_build.result == 'success' || needs.go_build.result == 'skipped') && (needs.crux_build.result == 'success' || needs.crux_build.result == 'skipped') && needs.crux-ui_build.result == 'success' && @@ -930,7 +927,6 @@ jobs: if: | always() && (github.ref_name == 'develop' || github.ref_name == 'main' || github.ref_type == 'tag') && - needs.e2e.result == 'success' && (needs.go_build.result == 'success' || needs.go_build.result == 'skipped') && (needs.crux_build.result == 'success' || needs.crux_build.result == 'skipped') && (needs.crux-ui_build.result == 'success' || needs.crux-ui_build.result == 'skipped') && From e6b85801d9a807c16c0f484a1fbe756f8e882ec1 Mon Sep 17 00:00:00 2001 From: Mate Vago Date: Wed, 4 Dec 2024 12:34:10 +0100 Subject: [PATCH 31/32] fix: lint --- .../container-config/common-editor.spec.ts | 83 ++++++++----------- .../container-config-filters.spec.ts | 18 ++-- .../container-config/docker-editor.spec.ts | 39 +++------ .../container-config/docker-json.spec.ts | 29 +++---- .../image-config-view-state.spec.ts | 12 +-- .../kubernetes-editor.spec.ts | 71 ++++------------ .../container-config/kubernetes-json.spec.ts | 65 +++++---------- .../deployment/deployment-copy.spec.ts | 2 +- .../deployment-copyability-versioned.spec.ts | 18 ++-- .../deployment-deletability.spec.ts | 2 +- .../e2e/with-login/image-config.spec.ts | 8 +- 11 files changed, 122 insertions(+), 225 deletions(-) diff --git a/web/crux-ui/e2e/with-login/container-config/common-editor.spec.ts b/web/crux-ui/e2e/with-login/container-config/common-editor.spec.ts index fd56c19a65..6fa73895e5 100644 --- a/web/crux-ui/e2e/with-login/container-config/common-editor.spec.ts +++ b/web/crux-ui/e2e/with-login/container-config/common-editor.spec.ts @@ -1,5 +1,5 @@ +import { WS_TYPE_PATCH_CONFIG } from '@app/models' import { expect, Page } from '@playwright/test' -import { test } from '../../utils/test.fixture' import { NGINX_TEST_IMAGE_WITH_TAG, TEAM_ROUTES } from 'e2e/utils/common' import { createStorage } from 'e2e/utils/storages' import { @@ -20,33 +20,33 @@ import { wsPatchMatchVolume, } from 'e2e/utils/websocket-match' import { createImage, createProject, createVersion } from '../../utils/projects' +import { test } from '../../utils/test.fixture' import { waitSocketRef, wsPatchSent } from '../../utils/websocket' -import { WS_TYPE_PATCH_CONFIG } from '@app/models' const setup = async ( page: Page, projectName: string, versionName: string, imageName: string, -): Promise<{ projectId: string; versionId: string; imageId: string }> => { +): Promise<{ imageConfigId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') const imageConfigId = await createImage(page, projectId, versionId, imageName) - return { projectId, versionId, imageConfigId } + return { imageConfigId } } test.describe.configure({ mode: 'parallel' }) test.describe('Image common config from editor', () => { test('Container name should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'name-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'name-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const name = 'new-container-name' @@ -60,13 +60,13 @@ test.describe('Image common config from editor', () => { }) test('Expose strategy should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'expose-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'expose-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchExpose('exposeWithTls')) await page.getByRole('button', { name: 'HTTPS', exact: true }).click() @@ -78,13 +78,13 @@ test.describe('Image common config from editor', () => { }) test('User should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'user-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'user-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const user = 23 @@ -98,13 +98,13 @@ test.describe('Image common config from editor', () => { }) test('TTY should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'tty-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'tty-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("TTY")').click() @@ -118,13 +118,13 @@ test.describe('Image common config from editor', () => { }) test('Port should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'port-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'port-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Ports")').click() @@ -148,13 +148,13 @@ test.describe('Image common config from editor', () => { }) test('Port ranges should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'port-range-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'port-range-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Port ranges")').click() @@ -191,13 +191,13 @@ test.describe('Image common config from editor', () => { }) test('Secrets should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'secrets-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'secrets-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Secrets")').click() @@ -217,13 +217,13 @@ test.describe('Image common config from editor', () => { }) test('Commands should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'commands-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'commands-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Commands")').click() @@ -240,13 +240,13 @@ test.describe('Image common config from editor', () => { }) test('Arguments should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'arguments-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'arguments-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Arguments")').click() @@ -263,12 +263,12 @@ test.describe('Image common config from editor', () => { }) test('Routing should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'routing-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'routing-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Ports")').click() @@ -314,17 +314,12 @@ test.describe('Image common config from editor', () => { }) test('Environment should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup( - page, - 'environment-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'environment-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Environment")').click() @@ -343,17 +338,12 @@ test.describe('Image common config from editor', () => { }) test('Config container should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup( - page, - 'config-container-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'config-container-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Config container")').click() const confDiv = page.locator('div.grid:has(label:has-text("CONFIG CONTAINER"))') @@ -378,17 +368,12 @@ test.describe('Image common config from editor', () => { }) test('Init containers should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup( - page, - 'init-container-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'init-container-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const name = 'container-name' const image = 'image' @@ -436,12 +421,12 @@ test.describe('Image common config from editor', () => { }) test('Volume should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'volume-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'volume-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Volume")').click() @@ -470,7 +455,7 @@ test.describe('Image common config from editor', () => { }) test('Storage should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'storage-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'storage-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const storageName = 'image-editor-storage' const storageId = await createStorage(page, storageName, 'storage.com', '1234', '12345') @@ -479,7 +464,7 @@ test.describe('Image common config from editor', () => { await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Volume")').click() diff --git a/web/crux-ui/e2e/with-login/container-config/container-config-filters.spec.ts b/web/crux-ui/e2e/with-login/container-config/container-config-filters.spec.ts index 12d4e5598a..fbf3249a50 100644 --- a/web/crux-ui/e2e/with-login/container-config/container-config-filters.spec.ts +++ b/web/crux-ui/e2e/with-login/container-config/container-config-filters.spec.ts @@ -1,28 +1,24 @@ import { expect, Page } from '@playwright/test' -import { test } from '../../utils/test.fixture' import { NGINX_TEST_IMAGE_WITH_TAG, TEAM_ROUTES } from 'e2e/utils/common' import { createImage, createProject, createVersion } from '../../utils/projects' +import { test } from '../../utils/test.fixture' const setup = async ( page: Page, projectName: string, versionName: string, imageName: string, -): Promise<{ projectId: string; versionId: string; imageId: string }> => { +): Promise<{ imageConfigId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') const imageConfigId = await createImage(page, projectId, versionId, imageName) - return { - projectId, - versionId, - imageConfigId, - } + return { imageConfigId } } test.describe('Filters', () => { test('None should be selected by default', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'filter-all', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'filter-all', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') @@ -34,7 +30,7 @@ test.describe('Filters', () => { }) test('All should not be selected if one of the main filters are not selected', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'filter-select', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'filter-select', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') @@ -47,7 +43,7 @@ test.describe('Filters', () => { }) test('Main filter should not be selected if one of its sub filters are not selected', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'sub-filter', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'sub-filter', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') @@ -62,7 +58,7 @@ test.describe('Filters', () => { }) test('Config field should be invisible if its sub filter is not selected', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'sub-deselect', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'sub-deselect', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') diff --git a/web/crux-ui/e2e/with-login/container-config/docker-editor.spec.ts b/web/crux-ui/e2e/with-login/container-config/docker-editor.spec.ts index 04b04e1698..7a68cc2e84 100644 --- a/web/crux-ui/e2e/with-login/container-config/docker-editor.spec.ts +++ b/web/crux-ui/e2e/with-login/container-config/docker-editor.spec.ts @@ -17,27 +17,22 @@ const setup = async ( projectName: string, versionName: string, imageName: string, -): Promise<{ projectId: string; versionId: string; imageId: string }> => { +): Promise<{ imageConfigId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') const imageConfigId = await createImage(page, projectId, versionId, imageName) - return { projectId, versionId, imageConfigId: imageConfigId } + return { imageConfigId } } test.describe('Image docker config from editor', () => { test('Network mode should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup( - page, - 'networkmode-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'networkmode-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const mode = 'host' @@ -53,17 +48,12 @@ test.describe('Image docker config from editor', () => { }) test('Docker labels should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup( - page, - 'dockerlabel-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'dockerlabel-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const key = 'docker-key' const value = 'docker-value' @@ -84,17 +74,12 @@ test.describe('Image docker config from editor', () => { }) test('Restart policy should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup( - page, - 'restartpolicy-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'restartpolicy-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG, wsPatchMatchRestartPolicy('always')) await page.locator('div.grid:has(label:has-text("RESTART POLICY")) button:has-text("Always")').click() @@ -108,12 +93,12 @@ test.describe('Image docker config from editor', () => { }) test('Log config should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'logconfig-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'logconfig-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Log config")').click() @@ -137,12 +122,12 @@ test.describe('Image docker config from editor', () => { }) test('Networks should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'networks-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'networks-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Networks")').click() diff --git a/web/crux-ui/e2e/with-login/container-config/docker-json.spec.ts b/web/crux-ui/e2e/with-login/container-config/docker-json.spec.ts index 507b3d2bc0..ab69528e8b 100644 --- a/web/crux-ui/e2e/with-login/container-config/docker-json.spec.ts +++ b/web/crux-ui/e2e/with-login/container-config/docker-json.spec.ts @@ -17,24 +17,24 @@ const setup = async ( projectName: string, versionName: string, imageName: string, -): Promise<{ projectId: string; versionId: string; imageId: string }> => { +): Promise<{ imageConfigId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') const imageConfigId = await createImage(page, projectId, versionId, imageName) - return { projectId, versionId, imageConfigId: imageConfigId } + return { imageConfigId } } test.describe.configure({ mode: 'parallel' }) test.describe('Image docker config from JSON', () => { test('Network mode should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'networkmode-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'networkmode-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const mode = 'host' @@ -57,12 +57,12 @@ test.describe('Image docker config from JSON', () => { }) test('Docker labels should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'dockerlabel-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'dockerlabel-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const key = 'docker-key' const value = 'docker-value' @@ -89,17 +89,12 @@ test.describe('Image docker config from JSON', () => { }) test('Restart policy should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup( - page, - 'restartpolicy-json', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'restartpolicy-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') await jsonEditorButton.click() @@ -120,12 +115,12 @@ test.describe('Image docker config from JSON', () => { }) test('Log config should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'logconfig-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'logconfig-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const type = 'json-file' const key = 'logger-key' @@ -151,12 +146,12 @@ test.describe('Image docker config from JSON', () => { }) test('Networks should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'networks-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'networks-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Networks")').click() diff --git a/web/crux-ui/e2e/with-login/container-config/image-config-view-state.spec.ts b/web/crux-ui/e2e/with-login/container-config/image-config-view-state.spec.ts index fde1249dc6..414acbf567 100644 --- a/web/crux-ui/e2e/with-login/container-config/image-config-view-state.spec.ts +++ b/web/crux-ui/e2e/with-login/container-config/image-config-view-state.spec.ts @@ -8,21 +8,17 @@ const setup = async ( projectName: string, versionName: string, imageName: string, -): Promise<{ projectId: string; versionId: string; imageId: string }> => { +): Promise<{ imageConfigId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') const imageConfigId = await createImage(page, projectId, versionId, imageName) - return { - projectId, - versionId, - imageId: imageConfigId, - } + return { imageConfigId } } test.describe('View state', () => { test('Editor state should show the configuration fields', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'editor-state-conf', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'editor-state-conf', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') @@ -39,7 +35,7 @@ test.describe('View state', () => { }) test('JSON state should show the json editor', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'editor-state-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'editor-state-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') diff --git a/web/crux-ui/e2e/with-login/container-config/kubernetes-editor.spec.ts b/web/crux-ui/e2e/with-login/container-config/kubernetes-editor.spec.ts index ef3d1b0e1c..972d7a16f7 100644 --- a/web/crux-ui/e2e/with-login/container-config/kubernetes-editor.spec.ts +++ b/web/crux-ui/e2e/with-login/container-config/kubernetes-editor.spec.ts @@ -25,28 +25,23 @@ const setup = async ( projectName: string, versionName: string, imageName: string, -): Promise<{ projectId: string; versionId: string; imageId: string }> => { +): Promise<{ imageConfigId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') const imageConfigId = await createImage(page, projectId, versionId, imageName) - return { projectId, versionId, imageConfigId: imageConfigId } + return { imageConfigId } } test.describe('Image kubernetes config from editor', () => { test('Deployment strategy should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup( - page, - 'deployment-strategy-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'deployment-strategy-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const strategy = 'rolling' @@ -60,18 +55,13 @@ test.describe('Image kubernetes config from editor', () => { }) test('Custom headers should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup( - page, - 'custom-headers-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'custom-headers-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Custom headers")').click() @@ -90,18 +80,13 @@ test.describe('Image kubernetes config from editor', () => { }) test('Proxy headers should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup( - page, - 'proxy-headers-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'proxy-headers-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Proxy headers")').click() @@ -115,18 +100,13 @@ test.describe('Image kubernetes config from editor', () => { }) test('Load balancer should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup( - page, - 'load-balancer-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'load-balancer-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Use load balancer")').click() @@ -159,18 +139,13 @@ test.describe('Image kubernetes config from editor', () => { }) test('Health check config should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup( - page, - 'health-check-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'health-check-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Health check config")').click() @@ -202,17 +177,12 @@ test.describe('Image kubernetes config from editor', () => { }) test('Resource config should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup( - page, - 'resource-config-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'resource-config-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Resource config")').click() @@ -247,13 +217,13 @@ test.describe('Image kubernetes config from editor', () => { page.locator(`div.max-h-128 > div:nth-child(2):near(label:has-text("${category}"))`) test('Labels should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'labels-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'labels-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.getByRole('button', { name: 'Labels', exact: true }).click() @@ -288,18 +258,13 @@ test.describe('Image kubernetes config from editor', () => { }) test('Annotations should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup( - page, - 'annotations-editor', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'annotations-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.getByRole('button', { name: 'Annotations', exact: true }).click() diff --git a/web/crux-ui/e2e/with-login/container-config/kubernetes-json.spec.ts b/web/crux-ui/e2e/with-login/container-config/kubernetes-json.spec.ts index 166bf48c6f..3e372bf41b 100644 --- a/web/crux-ui/e2e/with-login/container-config/kubernetes-json.spec.ts +++ b/web/crux-ui/e2e/with-login/container-config/kubernetes-json.spec.ts @@ -1,5 +1,5 @@ +import { WS_TYPE_PATCH_CONFIG } from '@app/models' import { expect, Page } from '@playwright/test' -import { test } from '../../utils/test.fixture' import { NGINX_TEST_IMAGE_WITH_TAG, TEAM_ROUTES } from 'e2e/utils/common' import { wsPatchMatchCustomHeader, @@ -17,36 +17,31 @@ import { wsPatchMatchServiceLabel, } from 'e2e/utils/websocket-match' import { createImage, createProject, createVersion } from '../../utils/projects' +import { test } from '../../utils/test.fixture' import { waitSocketRef, wsPatchSent } from '../../utils/websocket' -import { WS_TYPE_PATCH_CONFIG } from '@app/models' const setup = async ( page: Page, projectName: string, versionName: string, imageName: string, -): Promise<{ projectId: string; versionId: string; imageId: string }> => { +): Promise<{ imageConfigId: string }> => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, versionName, 'Incremental') const imageConfigId = await createImage(page, projectId, versionId, imageName) - return { projectId, versionId, imageConfigId: imageConfigId } + return { imageConfigId } } test.describe('Image kubernetes config from JSON', () => { test('Deployment strategy should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup( - page, - 'deployment-strategy-json', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'deployment-strategy-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const strategy = 'rolling' @@ -67,18 +62,13 @@ test.describe('Image kubernetes config from JSON', () => { }) test('Custom headers should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup( - page, - 'custom-headers-json', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'custom-headers-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const header = 'test-header' @@ -102,18 +92,13 @@ test.describe('Image kubernetes config from JSON', () => { }) test('Proxy headers should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup( - page, - 'proxy-headers-json', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'proxy-headers-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') await jsonEditorButton.click() @@ -132,18 +117,13 @@ test.describe('Image kubernetes config from JSON', () => { }) test('Load balancer should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup( - page, - 'load-balancer-json', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'load-balancer-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const key = 'balancer-key' const value = 'balancer-value' @@ -177,13 +157,13 @@ test.describe('Image kubernetes config from JSON', () => { }) test('Health check config should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'health-check-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'health-check-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const port = 12560 const liveness = 'test/liveness/' @@ -216,18 +196,13 @@ test.describe('Image kubernetes config from JSON', () => { }) test('Resource config should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup( - page, - 'resource-config-json', - '1.0.0', - NGINX_TEST_IMAGE_WITH_TAG, - ) + const { imageConfigId } = await setup(page, 'resource-config-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const cpuLimits = '50' const cpuRequests = '25' @@ -266,13 +241,13 @@ test.describe('Image kubernetes config from JSON', () => { page.locator(`div.max-h-128 > div:nth-child(2):near(label:has-text("${category}"))`) test('Labels should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'labels-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'labels-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const key = 'label-key' const value = 'label-value' @@ -310,13 +285,13 @@ test.describe('Image kubernetes config from JSON', () => { }) test('Annotations should be saved', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'annotations-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'annotations-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const key = 'annotation-key' const value = 'annotation-value' diff --git a/web/crux-ui/e2e/with-login/deployment/deployment-copy.spec.ts b/web/crux-ui/e2e/with-login/deployment/deployment-copy.spec.ts index 76e8509138..99bd90b953 100644 --- a/web/crux-ui/e2e/with-login/deployment/deployment-copy.spec.ts +++ b/web/crux-ui/e2e/with-login/deployment/deployment-copy.spec.ts @@ -24,7 +24,7 @@ const addSecretToImage = async ( await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') await jsonEditorButton.click() diff --git a/web/crux-ui/e2e/with-login/deployment/deployment-copyability-versioned.spec.ts b/web/crux-ui/e2e/with-login/deployment/deployment-copyability-versioned.spec.ts index 7a78d13bd4..5837f5cb0b 100644 --- a/web/crux-ui/e2e/with-login/deployment/deployment-copyability-versioned.spec.ts +++ b/web/crux-ui/e2e/with-login/deployment/deployment-copyability-versioned.spec.ts @@ -1,11 +1,11 @@ -import { ProjectType, WS_TYPE_PATCH_CONFIG } from '@app/models' +import { ProjectType, WS_TYPE_PATCH_IMAGE } from '@app/models' import { Page, expect } from '@playwright/test' import { NGINX_TEST_IMAGE_WITH_TAG, TEAM_ROUTES, waitForURLExcept } from '../../utils/common' import { deployWithDagent } from '../../utils/node-helper' import { createNode } from '../../utils/nodes' import { addDeploymentToVersion, - createImage as imageConfigId, + createImage, createProject, createVersion, fillDeploymentPrefix, @@ -39,7 +39,7 @@ test.describe('Versioned Project', () => { const { projectId } = await setup(page, nodeName, projectName, 'versioned') const versionId = await createVersion(page, projectId, '0.1.0', 'Incremental') - await imageConfigId(page, projectId, versionId, NGINX_TEST_IMAGE_WITH_TAG) + await createImage(page, projectId, versionId, NGINX_TEST_IMAGE_WITH_TAG) const { id: deploymentId } = await addDeploymentToVersion(page, projectId, versionId, nodeName, { prefix }) await page.goto(TEAM_ROUTES.deployment.details(deploymentId)) @@ -66,7 +66,7 @@ test.describe('Versioned Project', () => { const { projectId } = await setup(page, nodeName, projectName, 'versioned') const versionId = await createVersion(page, projectId, '0.1.0', 'Incremental') - await imageConfigId(page, projectId, versionId, NGINX_TEST_IMAGE_WITH_TAG) + await createImage(page, projectId, versionId, NGINX_TEST_IMAGE_WITH_TAG) const { id: deploymentId } = await addDeploymentToVersion(page, projectId, versionId, nodeName, { prefix }) await page.goto(TEAM_ROUTES.deployment.details(deploymentId)) @@ -93,7 +93,7 @@ test.describe('Versioned Project', () => { const { projectId } = await setup(page, nodeName, projectName, 'versioned') const versionId = await createVersion(page, projectId, '1.0.0', 'Incremental') - await imageConfigId(page, projectId, versionId, NGINX_TEST_IMAGE_WITH_TAG) + await createImage(page, projectId, versionId, NGINX_TEST_IMAGE_WITH_TAG) await addDeploymentToVersion(page, projectId, versionId, nodeName, { prefix }) await page.goto(TEAM_ROUTES.deployment.list()) @@ -118,13 +118,13 @@ test.describe('Versioned Project', () => { const projectId = await createProject(page, projectName, 'versioned') const versionId = await createVersion(page, projectId, '0.1.0', 'Incremental') - const imageId = await imageConfigId(page, projectId, versionId, NGINX_TEST_IMAGE_WITH_TAG) + const imageId = await createImage(page, projectId, versionId, NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) - await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) + await page.goto(TEAM_ROUTES.project.versions(projectId).imageDetails(versionId, imageId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const editorButton = await page.waitForSelector('button:has-text("JSON")') await editorButton.click() @@ -145,7 +145,7 @@ test.describe('Versioned Project', () => { useParentConfig: false, }, ] - const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_CONFIG) + const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_IMAGE) await jsonContainer.fill(JSON.stringify(configObject)) await wsSent diff --git a/web/crux-ui/e2e/with-login/deployment/deployment-deletability.spec.ts b/web/crux-ui/e2e/with-login/deployment/deployment-deletability.spec.ts index 62ba1da135..7d605877c3 100644 --- a/web/crux-ui/e2e/with-login/deployment/deployment-deletability.spec.ts +++ b/web/crux-ui/e2e/with-login/deployment/deployment-deletability.spec.ts @@ -17,7 +17,7 @@ test('In progress deployment should be not deletable', async ({ page }) => { await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const editorButton = await page.waitForSelector('button:has-text("JSON")') await editorButton.click() diff --git a/web/crux-ui/e2e/with-login/image-config.spec.ts b/web/crux-ui/e2e/with-login/image-config.spec.ts index a03b93299d..eb009625d5 100644 --- a/web/crux-ui/e2e/with-login/image-config.spec.ts +++ b/web/crux-ui/e2e/with-login/image-config.spec.ts @@ -119,13 +119,13 @@ const wsPatchMatchPorts = (internalPort: string, externalPort?: string) => (payl test.describe('Image configurations', () => { test('Port should be saved after adding it from the config field', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'port-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'port-editor', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) await page.locator('button:has-text("Ports")').click() @@ -152,13 +152,13 @@ test.describe('Image configurations', () => { }) test('Port should be saved after adding it from the json editor', async ({ page }) => { - const { projectId, versionId, imageConfigId } = await setup(page, 'port-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) + const { imageConfigId } = await setup(page, 'port-json', '1.0.0', NGINX_TEST_IMAGE_WITH_TAG) const sock = waitSocketRef(page) await page.goto(TEAM_ROUTES.containerConfig.details(imageConfigId)) await page.waitForSelector('h2:text-is("Image")') const ws = await sock - const wsRoute = TEAM_ROUTES.project.versions(projectId).detailsSocket(versionId) + const wsRoute = TEAM_ROUTES.containerConfig.detailsSocket(imageConfigId) const jsonEditorButton = await page.waitForSelector('button:has-text("JSON")') await jsonEditorButton.click() From 3d3afac4259500062056fc972ad58671b6be6314 Mon Sep 17 00:00:00 2001 From: Mate Vago Date: Wed, 4 Dec 2024 12:36:04 +0100 Subject: [PATCH 32/32] fix: go_push needs --- .github/workflows/product_builder.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/product_builder.yaml b/.github/workflows/product_builder.yaml index 9df64e9f4f..32a2373691 100644 --- a/.github/workflows/product_builder.yaml +++ b/.github/workflows/product_builder.yaml @@ -669,7 +669,7 @@ jobs: defaults: run: working-directory: ${{ env.GOLANG_WORKING_DIRECTORY }} - needs: [gather_changes] + needs: [gather_changes, go_build] if: | always() && (github.ref_name == 'develop' || github.ref_name == 'main' || github.ref_type == 'tag') &&