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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions golang/internal/crypt/pgp.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ func DecryptSecrets(arr map[string]string, appConfig *config.CommonConfiguration
out := map[string][]byte{}

for key, sec := range arr {
if sec == "" {
out[key] = []byte{}
continue
}

decrypted, err := helper.DecryptMessageArmored(appConfig.SecretPrivateKey, nil, sec)
if err != nil {
return out, fmt.Errorf("could not process secret: %v %w", key, err)
Expand Down
3 changes: 2 additions & 1 deletion web/crux-ui/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@

"errors": {
"registryRateLimit": "Rate limit reached, please try again later",
"registryUnauthorized": "Unauthorized"
"registryUnauthorized": "Unauthorized",
"deployRequiredSecrets": "The following instances have missing required secrets: {{instances}}"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { DyoHeading } from '@app/elements/dyo-heading'
import DyoIcon from '@app/elements/dyo-icon'
import { DyoList } from '@app/elements/dyo-list'
import DyoModal, { DyoConfirmationModal } from '@app/elements/dyo-modal'
import { defaultApiErrorHandler } from '@app/errors'
import { apiErrorHandler, defaultApiErrorHandler, defaultTranslator } from '@app/errors'
import useConfirmation from '@app/hooks/use-confirmation'
import { EnumFilter, enumFilterFor, TextFilter, textFilterFor, useFilters } from '@app/hooks/use-filters'
import {
Expand All @@ -25,6 +25,7 @@ import {
deploymentIsDeployable,
DeploymentStatus,
DEPLOYMENT_STATUS_VALUES,
DyoErrorDto,
NodeEventMessage,
NodeStatus,
StartDeployment,
Expand All @@ -34,20 +35,37 @@ import {
import { deploymentApiUrl, deploymentDeployUrl, deploymentStartApiUrl, deploymentUrl, WS_NODES } from '@app/routes'
import { sendForm, utcDateToLocale } from '@app/utils'
import clsx from 'clsx'
import { Translate } from 'next-translate'
import useTranslation from 'next-translate/useTranslation'
import { NextRouter, useRouter } from 'next/dist/client/router'
import { useRouter } from 'next/dist/client/router'
import Image from 'next/image'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import DeploymentStatusTag from './deployments/deployment-status-tag'
import { VersionActions } from './use-version-state'

export const startDeployment = async (
router: NextRouter,
onApiError: (response: Response) => void,
deploymentId: string,
deployInstances?: string[],
) => {
type ErrorInstance = {
id: string
name: string
}

export const deployStartErrorHandler = (t: Translate) =>
apiErrorHandler((stringId: string, status: number, dto: DyoErrorDto) => {
if (status === 412) {
const instances: ErrorInstance[] = dto.value
return {
toast: t('common:errors.deployRequiredSecrets', {
instances: instances.reduce((message, it) => `${message}\n${it.name}`, ''),
}),
toastOptions: {
className: '!bg-error-red text-center min-w-[32rem]',
},
}
}
return defaultTranslator(t)(stringId, status, dto)
})

export const startDeployment = async (deploymentId: string, deployInstances?: string[]): Promise<Response | null> => {
const res = await sendForm(
'POST',
deploymentStartApiUrl(deploymentId),
Expand All @@ -59,12 +77,9 @@ export const startDeployment = async (
)

if (!res.ok) {
onApiError(res)
return null
return res
}

await router.push(deploymentDeployUrl(deploymentId))

return null
}

Expand Down Expand Up @@ -93,6 +108,7 @@ const VersionDeploymentsSection = (props: VersionDeploymentsSectionProps) => {
const router = useRouter()

const handleApiError = defaultApiErrorHandler(t)
const handleDeployStartError = deployStartErrorHandler(t)

const [showInfo, setShowInfo] = useState<DeploymentByVersion>(null)
const [confirmModalConfig, confirm] = useConfirmation()
Expand All @@ -111,7 +127,12 @@ const VersionDeploymentsSection = (props: VersionDeploymentsSectionProps) => {
return
}

await startDeployment(router, handleApiError, deployment.id)
const res = await startDeployment(deployment.id)
if (res) {
handleDeployStartError(res)
} else {
await router.push(deploymentDeployUrl(deployment.id))
}
}

const onDeleteDeployment = async (deployment: DeploymentByVersion) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ const SecretKeyValueInput = (props: SecretKeyValueInputProps) => {
className="basis-12 flex-initial cursor-pointer ml-2 w-12 ring-2 rounded-md focus:outline-none focus:dark text-bright-muted ring-light-grey-muted flex justify-center"
>
<DyoIcon
className="text-bright-muted"
className="text-bright-muted self-center"
src={required ? '/clear.svg' : '/trash-can.svg'}
alt={t(required ? 'clear' : 'common:delete')}
size="md"
Expand Down
7 changes: 4 additions & 3 deletions web/crux-ui/src/errors.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { Translate } from 'next-translate'
import toast from 'react-hot-toast'
import toast, { ToastOptions } from 'react-hot-toast'
import { fromApiError } from './error-responses'
import { DyoErrorDto, WsErrorMessage } from './models'

export type DyoApiErrorHandler = (res: Response, setErrorValue?: FormikSetErrorValue) => Promise<void>

type Translation = {
input?: string
toastOptions?: ToastOptions
toast: string
}

Expand Down Expand Up @@ -38,7 +39,7 @@ export const defaultTranslator: (t: Translate) => Translator = t => (stringId, s

export const apiErrorHandler =
(translator: Translator) => async (res: Response, setErrorValue?: FormikSetErrorValue) => {
const toaster = message => toast.error(message)
const toaster = (message, options) => toast.error(message, options)

const { status } = res
let translation = null
Expand All @@ -56,7 +57,7 @@ export const apiErrorHandler =
translation = translator(null, 500, null)
}

toaster(translation.toast)
toaster(translation.toast, translation.toastOptions)
}

export const defaultApiErrorHandler = (t: Translate) => apiErrorHandler(defaultTranslator(t))
Expand Down
14 changes: 7 additions & 7 deletions web/crux-ui/src/pages/deployments/[deploymentId].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import DeploymentTokenCard from '@app/components/projects/versions/deployments/d
import EditDeploymentCard from '@app/components/projects/versions/deployments/edit-deployment-card'
import EditDeploymentInstances from '@app/components/projects/versions/deployments/edit-deployment-instances'
import useDeploymentState from '@app/components/projects/versions/deployments/use-deployment-state'
import { startDeployment } from '@app/components/projects/versions/version-deployments-section'
import { deployStartErrorHandler, startDeployment } from '@app/components/projects/versions/version-deployments-section'
import { BreadcrumbLink } from '@app/components/shared/breadcrumb'
import PageHeading from '@app/components/shared/page-heading'
import { DetailsPageMenu } from '@app/components/shared/page-menu'
Expand All @@ -19,7 +19,6 @@ import { defaultApiErrorHandler } from '@app/errors'
import useWebsocketTranslate from '@app/hooks/use-websocket-translation'
import {
DeploymentDetails,
DeploymentInvalidatedSecrets,
DeploymentRoot,
mergeConfigs,
NodeDetails,
Expand Down Expand Up @@ -60,6 +59,7 @@ const DeploymentDetailsPage = (props: DeploymentDetailsPageProps) => {
const submitRef = useRef<() => Promise<any>>()

const handleApiError = defaultApiErrorHandler(t)
const handleDeployStartError = deployStartErrorHandler(t)

const onWsError = (error: Error) => {
// eslint-disable-next-line
Expand Down Expand Up @@ -108,11 +108,11 @@ const DeploymentDetailsPage = (props: DeploymentDetailsPageProps) => {
return
}

const result = await startDeployment(router, handleApiError, deployment.id, state.deployInstances)
if (result?.property === 'secrets') {
const invalidSecrets = result.value as DeploymentInvalidatedSecrets[]

actions.onInvalidateSecrets(invalidSecrets)
const res = await startDeployment(deployment.id)
if (res) {
handleDeployStartError(res)
} else {
await router.push(deploymentDeployUrl(deployment.id))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export default class DeployStartValidationInterceptor implements NestInterceptor
})
}

const secretsHaveValue = deployment.instances.every(it => {
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)

Expand All @@ -74,14 +74,17 @@ export default class DeployStartValidationInterceptor implements NestInterceptor
return instanceSecret.encrypted && instanceSecret.value.length > 0
})

return hasSecrets
return !hasSecrets
})

if (!secretsHaveValue) {
if (missingSecrets.length > 0) {
throw new CruxPreconditionFailedException({
message: 'Required secrets must have values!',
property: 'deploymentId',
value: deployment.id,
property: 'instanceIds',
value: missingSecrets.map(it => ({
id: it.id,
name: it.config?.name ?? it.image.name,
})),
})
}

Expand Down