diff --git a/src/components/CIPipelineN/AdvancedConfigOptions.tsx b/src/components/CIPipelineN/AdvancedConfigOptions.tsx index ee2160bd67..50e11d2c05 100644 --- a/src/components/CIPipelineN/AdvancedConfigOptions.tsx +++ b/src/components/CIPipelineN/AdvancedConfigOptions.tsx @@ -15,18 +15,32 @@ */ import { useContext, useEffect, useState } from 'react' -import { CIBuildConfigType, CIBuildType, KeyValueTableProps, noop, OptionType } from '@devtron-labs/devtron-fe-common-lib' + +import { + BuildSecretType, + CIBuildConfigType, + CIBuildType, + KeyValueTableProps, + noop, + OptionType, +} from '@devtron-labs/devtron-fe-common-lib' + +import { importComponentFromFELibrary } from '@Components/common' + import CIConfig from '../ciConfig/CIConfig' -import DockerArgs from './DockerArgs' -import CustomImageTags from './CustomImageTags' +import { getTargetPlatformMap } from '../ciConfig/CIConfig.utils' import TargetPlatformSelector from '../ciConfig/TargetPlatformSelector' import { AdvancedConfigOptionsProps, CIConfigParentState } from '../ciConfig/types' import { DockerConfigOverrideKeys } from '../ciPipeline/types' -import { getTargetPlatformMap } from '../ciConfig/CIConfig.utils' import { pipelineContext } from '../workflowEditor/workflowEditor' -import '../ciConfig/CIConfig.scss' +import CustomImageTags from './CustomImageTags' +import DockerArgs from './DockerArgs' import { ComponentStates } from './types' +import '../ciConfig/CIConfig.scss' + +const BuildSecrets = importComponentFromFELibrary('BuildSecrets', null, 'function') + export default function AdvancedConfigOptions({ ciPipeline, appId, isTemplateView }: AdvancedConfigOptionsProps) { const { formData, setFormData, loadingState, setLoadingState, formDataErrorObj, setFormDataErrorObj } = useContext(pipelineContext) @@ -53,6 +67,25 @@ export default function AdvancedConfigOptions({ ciPipeline, appId, isTemplateVie const hasParentLoaded = parentState?.loadingState === ComponentStates.loaded const showNonBuildpackOptions = hasParentLoaded && (isGlobalAndNotBuildpack || !isCurrentCITypeBuildpack) + // Shows the current override draft if present; else the pipeline's saved override (fetched via + // app/ci-pipeline/:appId/:ciPipelineId) if it already has one; else the global config as the starting + // point to override - same precedence populateCurrentPlatformsData uses for targetPlatform below + const currentSecrets: BuildSecretType[] = + (allowOverride + ? formData.dockerConfigOverride?.ciBuildConfig?.dockerBuildConfig?.secrets ?? + (parentState.selectedCIPipeline?.isDockerConfigOverridden + ? parentState.selectedCIPipeline?.dockerConfigOverride?.ciBuildConfig?.dockerBuildConfig?.secrets + : parentState.ciConfig?.ciBuildConfig?.dockerBuildConfig?.secrets) + : parentState.ciConfig?.ciBuildConfig?.dockerBuildConfig?.secrets) ?? [] + + const currentSSH: BuildSecretType[] = + (allowOverride + ? formData.dockerConfigOverride?.ciBuildConfig?.dockerBuildConfig?.ssh ?? + (parentState.selectedCIPipeline?.isDockerConfigOverridden + ? parentState.selectedCIPipeline?.dockerConfigOverride?.ciBuildConfig?.dockerBuildConfig?.ssh + : parentState.ciConfig?.ciBuildConfig?.dockerBuildConfig?.ssh) + : parentState.ciConfig?.ciBuildConfig?.dockerBuildConfig?.ssh) ?? [] + useEffect(() => { if (parentState.ciConfig) { populateCurrentPlatformsData() @@ -99,7 +132,7 @@ export default function AdvancedConfigOptions({ ciPipeline, appId, isTemplateVie const updateDockerConfigOverride = ( key: string, - value: CIBuildConfigType | OptionType[] | boolean | string, + value: CIBuildConfigType | OptionType[] | boolean | string | BuildSecretType[], ): void => { setFormData((prevFormData) => { const _form = structuredClone(prevFormData) @@ -143,6 +176,22 @@ export default function AdvancedConfigOptions({ ciPipeline, appId, isTemplateVie _form.dockerConfigOverride.ciBuildConfig.dockerBuildConfig.buildContext = value as string } else if (key === DockerConfigOverrideKeys.projectPath) { _form.dockerConfigOverride.ciBuildConfig.buildPackConfig.projectPath = value as string + } else if (key === DockerConfigOverrideKeys.secrets) { + _form.dockerConfigOverride.ciBuildConfig = { + ..._form.dockerConfigOverride.ciBuildConfig, + dockerBuildConfig: { + ..._form.dockerConfigOverride.ciBuildConfig.dockerBuildConfig, + secrets: value as BuildSecretType[], + }, + } + } else if (key === DockerConfigOverrideKeys.ssh) { + _form.dockerConfigOverride.ciBuildConfig = { + ..._form.dockerConfigOverride.ciBuildConfig, + dockerBuildConfig: { + ..._form.dockerConfigOverride.ciBuildConfig.dockerBuildConfig, + ssh: value as BuildSecretType[], + }, + } } else { _form.dockerConfigOverride.ciBuildConfig = value as CIBuildConfigType } @@ -156,6 +205,28 @@ export default function AdvancedConfigOptions({ ciPipeline, appId, isTemplateVie }) } + const handleBuildSecretsUpdate = (secrets: BuildSecretType[]): void => { + updateDockerConfigOverride(DockerConfigOverrideKeys.secrets, secrets) + } + + const handleSSHUpdate = (ssh: BuildSecretType[]): void => { + updateDockerConfigOverride(DockerConfigOverrideKeys.ssh, ssh) + } + + const handleBuildSecretsError = (hasError: boolean): void => { + setFormDataErrorObj((prev) => ({ + ...prev, + secretsError: { isValid: !hasError, message: 'Invalid build secrets' }, + })) + } + + const handleSSHError = (hasError: boolean): void => { + setFormDataErrorObj((prev) => ({ + ...prev, + sshError: { isValid: !hasError, message: 'Invalid SSH keys' }, + })) + } + const toggleAdvancedOptions = (): void => { setCollapsedSection(!collapsedSection) } @@ -232,6 +303,18 @@ export default function AdvancedConfigOptions({ ciPipeline, appId, isTemplateVie handleDockerArgsError={handleDockerArgsError} /> )} + + {showNonBuildpackOptions && BuildSecrets && ( + + )} ) diff --git a/src/components/CIPipelineN/CIPipeline.tsx b/src/components/CIPipelineN/CIPipeline.tsx index 97e3a72a38..65c2b66095 100644 --- a/src/components/CIPipelineN/CIPipeline.tsx +++ b/src/components/CIPipelineN/CIPipeline.tsx @@ -176,6 +176,14 @@ export default function CIPipeline({ message: '', isValid: true, }, + secretsError: { + message: '', + isValid: true, + }, + sshError: { + message: '', + isValid: true, + }, }) const [ciPipeline, setCIPipeline] = useState({ @@ -419,7 +427,11 @@ export default function CIPipeline({ } _formDataErrorObj[BuildStageVariable.Build].isValid = - _formDataErrorObj.name.isValid && valid && _formDataErrorObj.dockerArgsError.isValid + _formDataErrorObj.name.isValid && + valid && + _formDataErrorObj.dockerArgsError.isValid && + _formDataErrorObj.secretsError.isValid && + _formDataErrorObj.sshError.isValid } else { const stepsLength = _formData[stageName].steps.length let isStageValid = true @@ -639,7 +651,13 @@ export default function CIPipeline({ ) { setApiInProgress(false) const branchNameNotPresent = formData.materials.some((_mat) => !_mat.value) - if (formData.name === '' || branchNameNotPresent || !formDataErrorObj.dockerArgsError.isValid) { + if ( + formData.name === '' || + branchNameNotPresent || + !formDataErrorObj.dockerArgsError.isValid || + !formDataErrorObj.secretsError.isValid || + !formDataErrorObj.sshError.isValid + ) { ToastManager.showToast({ variant: ToastVariantType.error, description: 'Please ensure all fields are valid', diff --git a/src/components/ciConfig/CIAdvancedConfig.tsx b/src/components/ciConfig/CIAdvancedConfig.tsx index 1e7648fe1f..dcd79468e3 100644 --- a/src/components/ciConfig/CIAdvancedConfig.tsx +++ b/src/components/ciConfig/CIAdvancedConfig.tsx @@ -15,12 +15,17 @@ */ import { useState } from 'react' -import PluginIcon from '../../assets/icons/ic-plugin.svg?react' + +import { BuildSecretType, KeyValueTableProps } from '@devtron-labs/devtron-fe-common-lib' + import Dropdown from '../../assets/icons/ic-chevron-down.svg?react' -import { CIAdvancedConfigProps } from './types' -import TargetPlatformSelector from './TargetPlatformSelector' +import PluginIcon from '../../assets/icons/ic-plugin.svg?react' import DockerArgs from '../CIPipelineN/DockerArgs' -import { KeyValueTableProps } from '@devtron-labs/devtron-fe-common-lib' +import { importComponentFromFELibrary } from '../common' +import TargetPlatformSelector from './TargetPlatformSelector' +import { CIAdvancedConfigProps } from './types' + +const BuildSecrets = importComponentFromFELibrary('BuildSecrets', null, 'function') export default function CIAdvancedConfig({ configOverrideView, @@ -28,6 +33,13 @@ export default function CIAdvancedConfig({ args, setArgs, setArgsError, + appId, + secrets, + setSecrets, + ssh, + setSsh, + setSecretsError, + setSshError, isBuildpackType, selectedTargetPlatforms, setSelectedTargetPlatforms, @@ -50,21 +62,27 @@ export default function CIAdvancedConfig({ setArgsError((prev) => ({ ...prev, [isBuildpackType ? 'buildEnvArgs' : 'args']: errorState })) } - const renderTargetPlatform = () => { - return ( -
- -
- ) + const handleBuildSecretsChange = (updatedSecrets: BuildSecretType[]): void => { + setSecrets(updatedSecrets) } + const handleSSHChange = (updatedSSH: BuildSecretType[]): void => { + setSsh(updatedSSH) + } + + const renderTargetPlatform = () => ( +
+ +
+ ) + if (configOverrideView && !isBuildpackType) { return null } @@ -110,6 +128,17 @@ export default function CIAdvancedConfig({ handleDockerArgsError={handleDockerArgsError} readOnly={updateNotAllowed} /> + {BuildSecrets && ( + + )} )} diff --git a/src/components/ciConfig/CIConfigForm.tsx b/src/components/ciConfig/CIConfigForm.tsx index 129a7b2a70..c609760c43 100644 --- a/src/components/ciConfig/CIConfigForm.tsx +++ b/src/components/ciConfig/CIConfigForm.tsx @@ -16,6 +16,7 @@ import { useEffect, useState } from 'react' import { + BuildSecretType, CIBuildConfigType, CIBuildType, showError, @@ -134,6 +135,10 @@ export default function CIConfigForm({ args: false, buildEnvArgs: false, }) + const [secrets, setSecrets] = useState([]) + const [ssh, setSsh] = useState([]) + const [secretsError, setSecretsError] = useState(false) + const [sshError, setSshError] = useState(false) const [loadingDataState, setLoadingDataState] = useState({ loading: false, failed: false, @@ -182,6 +187,10 @@ export default function CIConfigForm({ // Buildpack - build env arguments setBuildEnvArgs(processBuildArgs(currentCIBuildConfig.buildPackConfig?.args)) + + // Build secrets and SSH keys + setSecrets(ciConfig?.ciBuildConfig?.dockerBuildConfig?.secrets ?? []) + setSsh(ciConfig?.ciBuildConfig?.dockerBuildConfig?.ssh ?? []) } const updateParentCIBuildTypeState = () => { @@ -203,6 +212,14 @@ export default function CIConfigForm({ return } + if (!isBuildpackType && (secretsError || sshError)) { + ToastManager.showToast({ + variant: ToastVariantType.error, + description: 'Please ensure build secrets and SSH keys are valid', + }) + return + } + let targetPlatforms = '' const targetPlatformsSet = new Set() for (let index = 0; index < selectedTargetPlatforms.length; index++) { @@ -247,6 +264,8 @@ export default function CIConfigForm({ dockerfileRepository: repository.value, targetPlatform: targetPlatforms, buildContext: buildContext.value, + secrets: secrets.filter((secret) => secret.type && secret.secretName && secret.id && secret.key), + ssh: ssh.filter((sshKey) => sshKey.type && sshKey.secretName && sshKey.id && sshKey.key), } } @@ -405,6 +424,13 @@ export default function CIConfigForm({ buildEnvArgs={buildEnvArgs} setBuildEnvArgs={setBuildEnvArgs} setArgsError={setArgsError} + appId={appId} + secrets={secrets} + setSecrets={setSecrets} + ssh={ssh} + setSsh={setSsh} + setSecretsError={setSecretsError} + setSshError={setSshError} handleOnChangeConfig={handleOnChangeConfig} selectedTargetPlatforms={selectedTargetPlatforms} setSelectedTargetPlatforms={setSelectedTargetPlatforms} diff --git a/src/components/ciConfig/CIDockerFileConfig.tsx b/src/components/ciConfig/CIDockerFileConfig.tsx index faa203f1f9..8a0d25f97f 100644 --- a/src/components/ciConfig/CIDockerFileConfig.tsx +++ b/src/components/ciConfig/CIDockerFileConfig.tsx @@ -56,6 +56,13 @@ export default function CIDockerFileConfig({ buildEnvArgs, setBuildEnvArgs, setArgsError, + appId, + secrets, + setSecrets, + ssh, + setSsh, + setSecretsError, + setSshError, handleOnChangeConfig, selectedTargetPlatforms, setSelectedTargetPlatforms, @@ -436,6 +443,13 @@ export default function CIDockerFileConfig({ args={isBuildpackType ? buildEnvArgs : args} setArgs={isBuildpackType ? setBuildEnvArgs : setArgs} setArgsError={setArgsError} + appId={appId} + secrets={secrets} + setSecrets={setSecrets} + ssh={ssh} + setSsh={setSsh} + setSecretsError={setSecretsError} + setSshError={setSshError} isBuildpackType={isBuildpackType} selectedTargetPlatforms={selectedTargetPlatforms} setSelectedTargetPlatforms={setSelectedTargetPlatforms} diff --git a/src/components/ciConfig/types.tsx b/src/components/ciConfig/types.tsx index 177f56af60..d44baebc69 100644 --- a/src/components/ciConfig/types.tsx +++ b/src/components/ciConfig/types.tsx @@ -18,6 +18,7 @@ import React from 'react' import { AppConfigProps, + BuildSecretType, CIBuildConfigType, CIBuildType, CiPipeline, @@ -285,6 +286,13 @@ export interface CIDockerFileConfigProps { buildEnvArgs: CIBuildArgType[] setBuildEnvArgs: React.Dispatch> setArgsError: React.Dispatch> + appId: string + secrets: BuildSecretType[] + setSecrets: React.Dispatch> + ssh: BuildSecretType[] + setSsh: React.Dispatch> + setSecretsError: React.Dispatch> + setSshError: React.Dispatch> handleOnChangeConfig: (e) => void selectedTargetPlatforms: any setSelectedTargetPlatforms: any @@ -336,7 +344,20 @@ export interface CIBuildpackBuildOptionsProps { readOnly?: boolean } -export interface CIAdvancedConfigProps extends Pick { +export interface CIAdvancedConfigProps + extends Pick< + CIDockerFileConfigProps, + | 'args' + | 'setArgs' + | 'setArgsError' + | 'appId' + | 'secrets' + | 'setSecrets' + | 'ssh' + | 'setSsh' + | 'setSecretsError' + | 'setSshError' + > { configOverrideView: boolean allowOverride: boolean isBuildpackType: boolean diff --git a/src/components/ciPipeline/types.ts b/src/components/ciPipeline/types.ts index 57ae140d23..d8ee7df78b 100644 --- a/src/components/ciPipeline/types.ts +++ b/src/components/ciPipeline/types.ts @@ -269,6 +269,8 @@ export const DockerConfigOverrideKeys = { dockerfileRelativePath: 'dockerfileRelativePath', targetPlatform: 'targetPlatform', buildContext: 'buildContext', + secrets: 'secrets', + ssh: 'ssh', } export interface CIPipelineType extends Required> { diff --git a/src/components/workflowEditor/types.ts b/src/components/workflowEditor/types.ts index ca532f4a98..348edffb87 100644 --- a/src/components/workflowEditor/types.ts +++ b/src/components/workflowEditor/types.ts @@ -222,6 +222,8 @@ export interface PipelineFormDataErrorType { containerRegistryError?: { isValid: boolean; message?: string } repositoryError?: { isValid: boolean; message?: string } dockerArgsError?: { isValid: boolean; message?: string } + secretsError?: { isValid: boolean; message?: string } + sshError?: { isValid: boolean; message?: string } preBuildStage: { steps: TaskErrorObj[] isValid: boolean