Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: copying a deployment now does not copies the secret values to a different node #828

Merged
merged 3 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion web/crux-ui/e2e/utils/test.fixture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ test.beforeEach(async ({ page }, testInfo) => {
return
}

console.log(`[${testInfo.title}] ${type.toUpperCase()} ${it.text()}`)
console.info(`[${testInfo.title}] ${type.toUpperCase()} ${it.text()}`)
})

if (CPU_THROTTLE) {
Expand Down
20 changes: 20 additions & 0 deletions web/crux-ui/e2e/utils/websocket-match.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { UniqueSecretKeyValue } from '@app/models'

export const wsPatchMatchPorts = (internalPort: string, externalPort?: string) => (payload: any) => {
const internal = Number.parseInt(internalPort, 10)
const external = externalPort ? Number.parseInt(externalPort, 10) : null
Expand All @@ -22,6 +24,24 @@ export const wsPatchMatchPortRange =
)
}

export const wsPatchMatchEverySecret =
(secretKeys: string[]) =>
(payload: any): boolean => {
const payloadSecretKeys: string[] = payload.config?.secrets?.map(it => it?.key) ?? []
return secretKeys.every(it => payloadSecretKeys.includes(it))
}

export const wsPatchMatchNonNullSecretValues =
(secretKeys: string[]) =>
(payload: any): boolean => {
const payloadSecrets: UniqueSecretKeyValue[] = payload.config?.secrets ?? []

return (
secretKeys.every(secKey => payloadSecrets.find(it => it.key === secKey)) &&
payloadSecrets.every(it => typeof it.value === 'string')
)
}

export const wsPatchMatchSecret = (secret: string, required: boolean) => (payload: any) =>
payload.config?.secrets?.some(it => it.key === secret && it.required === required)

Expand Down
159 changes: 159 additions & 0 deletions web/crux-ui/e2e/with-login/deployment/deployment-copy.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { UniqueSecretKeyValue, WS_TYPE_PATCH_IMAGE, 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'
import { createNode } from '../../utils/nodes'
import {
addDeploymentToVersion,
createImage,
createProject,
createVersion,
fillDeploymentPrefix,
} from '../../utils/projects'
import { test } from '../../utils/test.fixture'
import { waitSocketRef, wsPatchSent } from '../../utils/websocket'

const addSecretToImage = async (
page: Page,
projectId: string,
versionId: string,
imageId: string,
secretKeys: string[],
): Promise<void> => {
const sock = waitSocketRef(page)
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 jsonEditorButton = await page.waitForSelector('button:has-text("JSON")')
await jsonEditorButton.click()

const jsonEditor = await page.locator('textarea')
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))
await jsonEditor.fill(JSON.stringify(json))
await wsSent
}

const openContainerConfigByDeploymentTable = async (page: Page, containerName: string): Promise<void> => {
const instancesTabelBody = await page.locator('.table-row-group')
const instanceRows = await instancesTabelBody.locator('.table-row')
await expect(instanceRows).toHaveCount(1)

await expect(page.locator(`div.table-cell:has-text("${containerName}")`).first()).toBeVisible()
const containerSettingsButton = await page.waitForSelector(
`[src="/instance_config_icon.svg"]:right-of(:text("${containerName}"))`,
)
await containerSettingsButton.click()

await page.waitForSelector(`h2:has-text("Container")`)
}

test.describe('Deployment Copy', () => {
const projectName = 'depl-cpy'
const newNodeName = projectName
const originalPrefix = `dcpy`

let originalDeploymentId: string
const secretKeys = ['secretOne', 'secretTwo']
const newSecretKey = 'new-secret'
const newSecretKeyList = [...secretKeys, newSecretKey]

test.beforeAll(async ({ browser }) => {
const ctx = await browser.newContext()
const page = await ctx.newPage()

const projectId = await createProject(page, projectName, 'versioned')
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)

await addSecretToImage(page, projectId, versionId, imageId, secretKeys)

const { id: deploymentId } = await addDeploymentToVersion(page, projectId, versionId, DAGENT_NODE, {
prefix: originalPrefix,
})
originalDeploymentId = deploymentId

const sock = waitSocketRef(page)
await page.goto(TEAM_ROUTES.deployment.details(originalDeploymentId))
await page.waitForSelector('h2:text-is("Deployments")')
const ws = await sock

await openContainerConfigByDeploymentTable(page, 'nginx')

const newSecretValue = 'new-secret-value'

const newSecertKeyInput = page.locator('input[placeholder="Key"][value=""]:below(label:has-text("SECRETS"))')
await newSecertKeyInput.fill(newSecretKey)

const newSecretValueInput = page.locator(
`input[placeholder="Value"][value=""]:near(input[placeholder="Key"][value="${newSecretKey}"], 10)`,
)
await newSecretValueInput.fill(newSecretValue)

const wsRoute = TEAM_ROUTES.deployment.detailsSocket(originalDeploymentId)
const wsSent = wsPatchSent(ws, wsRoute, WS_TYPE_PATCH_INSTANCE, wsPatchMatchNonNullSecretValues(newSecretKeyList))
await page.locator(`button:has-text("Save"):below(input[value="${newSecretValue}"])`).click()
await wsSent

await page.close()
await ctx.close()
})

test('should copy secret values to the same node', async ({ page }) => {
await page.goto(TEAM_ROUTES.deployment.details(originalDeploymentId))
await page.waitForSelector('h2:text-is("Deployments")')

const copyButton = page.locator('button:has-text("Copy")')
await copyButton.click()

const newPrefix = 'dcpy-second'
await page.locator(`button:has-text("${DAGENT_NODE}")`).click()
await fillDeploymentPrefix(page, newPrefix)

const currentUrl = page.url()
await page.locator('button:has-text("Copy")').click()
await waitForURLExcept(page, { startsWith: `${TEAM_ROUTES.deployment.list()}/`, except: currentUrl })
await page.waitForSelector('h2:text-is("Deployments")')

await expect(page.locator('.bg-dyo-turquoise:has-text("Preparing")')).toHaveCount(1)

await openContainerConfigByDeploymentTable(page, 'nginx')

const newSecretValueInput = page.locator(
`input[placeholder="Value"]:near(input[placeholder="Key"][value="${newSecretKey}"], 10)`,
)

await expect(newSecretValueInput).toBeDisabled()
await expect(newSecretValueInput).toHaveValue(/^(?!\s*$).+/) // match anything but an empty string
})

test('should delete secret values to a different node', async ({ page }) => {
await page.goto(TEAM_ROUTES.deployment.details(originalDeploymentId))
await page.waitForSelector('h2:text-is("Deployments")')

const copyButton = page.locator('button:has-text("Copy")')
await copyButton.click()

await page.locator(`button:has-text("${newNodeName}")`).click()
await fillDeploymentPrefix(page, originalPrefix)

const currentUrl = page.url()
await page.locator('button:has-text("Copy")').click()
await waitForURLExcept(page, { startsWith: `${TEAM_ROUTES.deployment.list()}/`, except: currentUrl })
await page.waitForSelector('h2:text-is("Deployments")')

await expect(page.locator('.bg-dyo-turquoise:has-text("Preparing")')).toHaveCount(1)

await openContainerConfigByDeploymentTable(page, 'nginx')

const newSecretKeyInput = page.locator(`input[placeholder="Key"][value="${newSecretKey}"]`)

await expect(await newSecretKeyInput.count()).toEqual(0)
})
})
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ProjectType, WS_TYPE_PATCH_IMAGE } from '@app/models'
import { expect, Page } from '@playwright/test'
import { test } from '../../utils/test.fixture'
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'
Expand All @@ -11,7 +10,8 @@ import {
createVersion,
fillDeploymentPrefix,
} from '../../utils/projects'
import { waitSocketRef as waitSocketRef, wsPatchSent } from '../../utils/websocket'
import { test } from '../../utils/test.fixture'
import { waitSocketRef, wsPatchSent } from '../../utils/websocket'

const setup = async (
page: Page,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ const AddDeploymentCard = (props: AddDeploymentCardProps) => {
) : !versions && formik.values.projectId ? (
<DyoLabel>{t('common:loading')}</DyoLabel>
) : versions.length === 0 ? (
<DyoLabel>{t('noVersions')}</DyoLabel>
<DyoMessage message={t('noVersions')} />
) : (
currentProject.type === 'versioned' && (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ import { useEffect } from 'react'
import toast from 'react-hot-toast'
import useSWR from 'swr'

interface AddDeploymentCardProps {
interface AddDeploymentToVersionCardProps {
className?: string
projectName: string
versionId: string
onAdd: (deploymentId: string) => void
onDiscard: VoidFunction
}

const AddDeploymentCard = (props: AddDeploymentCardProps) => {
const AddDeploymentToVersionCard = (props: AddDeploymentToVersionCardProps) => {
const { projectName, versionId, className, onAdd, onDiscard } = props

const { t } = useTranslation('deployments')
Expand Down Expand Up @@ -171,4 +171,4 @@ const AddDeploymentCard = (props: AddDeploymentCardProps) => {
)
}

export default AddDeploymentCard
export default AddDeploymentToVersionCard
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ProjectDetails, VERSION_SECTIONS_STATE_VALUES } from '@app/models'
import { parseStringUnionType } from '@app/utils'
import { useRouter } from 'next/dist/client/router'
import React, { useEffect, useRef } from 'react'
import AddDeploymentCard from './deployments/add-deployment-card'
import AddDeploymentToVersionCard from './deployments/add-deployment-to-version-card'
import CopyDeploymentCard from './deployments/copy-deployment-card'
import AddImagesCard from './images/add-images-card'
import { VerionState, VersionActions, VersionSection } from './use-version-state'
Expand Down Expand Up @@ -59,7 +59,7 @@ const VersionSections = (props: VersionSectionsProps) => {
) : state.addSection === 'image' ? (
<AddImagesCard onImagesSelected={actions.addImages} onDiscard={actions.discardAddSection} />
) : state.addSection === 'deployment' ? (
<AddDeploymentCard
<AddDeploymentToVersionCard
className="mb-4 p-8"
projectName={project.name}
versionId={state.version.id}
Expand Down
4 changes: 3 additions & 1 deletion web/crux/src/app/deploy/deploy.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,8 @@ export default class DeployService {
},
})

const differentNode = oldDeployment.nodeId !== newDeployment.nodeId

await this.prisma.$transaction(
oldDeployment.instances.map(it =>
this.prisma.instance.create({
Expand All @@ -962,7 +964,7 @@ export default class DeployService {
commands: toPrismaJson(it.config.commands),
args: toPrismaJson(it.config.args),
environment: toPrismaJson(it.config.environment),
secrets: toPrismaJson(it.config.secrets),
secrets: differentNode ? null : toPrismaJson(it.config.secrets),
initContainers: toPrismaJson(it.config.initContainers),
logConfig: toPrismaJson(it.config.logConfig),
restartPolicy: it.config.restartPolicy,
Expand Down
Loading