@@ -18,6 +18,18 @@ import {
1818} from '../../../utils' ;
1919import { DEFAULT_SPARK_IMAGE , SparkImage } from '../emr-releases' ;
2020
21+ const MISSING_ENVIRONMENTS_ERROR = 'MissingEnvironmentsError' ;
22+ const DUPLICATE_STAGE_NAME_ERROR = 'DuplicateStageNameError' ;
23+
24+ /**
25+ * User defined CI/CD environment stages
26+ */
27+ interface CICDEnvironment {
28+ stageName : string ;
29+ account : string ;
30+ region : string ;
31+ triggerIntegTest ?: boolean ;
32+ }
2133
2234/**
2335 * A CICD Pipeline to test and deploy a Spark application on Amazon EMR in cross-account environments using CDK Pipelines.
@@ -211,36 +223,63 @@ export class SparkEmrCICDPipeline extends TrackedConstruct {
211223 } ,
212224 } ) ;
213225
214- // Create the Staging stage of the CICD
215- const staging = new ApplicationStage ( this , 'Staging' , {
216- env : this . getAccountFromContext ( 'staging' ) ,
226+ try {
227+ const environments = this . getUserDefinedEnvironmentsFromContext ( ) ;
228+
229+ for ( const e of environments ) {
230+ this . integrationTestStage = this . attachStageToPipeline ( e . stageName . toUpperCase ( ) , {
231+ account : e . account ,
232+ region : e . region ,
233+ } , e . triggerIntegTest || false , buildStage , props ) ;
234+ }
235+ } catch ( e ) {
236+ const error = e as Error ;
237+ if ( error . name === DUPLICATE_STAGE_NAME_ERROR ) {
238+ throw e ;
239+ }
240+
241+ this . integrationTestStage = this . attachStageToPipeline ( 'Staging' , this . getAccountFromContext ( 'staging' ) , true , buildStage , props ) ;
242+ this . attachStageToPipeline ( 'Prod' , this . getAccountFromContext ( 'prod' ) , false , buildStage , props ) ;
243+ }
244+ }
245+
246+ /**
247+ * Attaches the given stage to the pipeline
248+ * @param stageName
249+ * @param resourceEnvironment
250+ * @param attachIntegTest
251+ * @param buildStage
252+ * @param props
253+ * @returns {CodeBuildStep|undefined } if integration step is configured, this returns the corresponding `CodeBuildStep` for the test
254+ */
255+ private attachStageToPipeline ( stageName : string , resourceEnvironment : ResourceEnvironment
256+ , attachIntegTest : boolean , buildStage : CodeBuildStep
257+ , props : SparkEmrCICDPipelineProps ) : CodeBuildStep | undefined {
258+ const applicationStage = new ApplicationStage ( this , stageName , {
259+ env : resourceEnvironment ,
217260 applicationStackFactory : props . applicationStackFactory ,
218- outputsEnv : props . integTestEnv ,
219- stage : CICDStage . STAGING ,
261+ outputsEnv : ( attachIntegTest && props . integTestScript ) ? props . integTestEnv : undefined ,
262+ stage : CICDStage . of ( stageName . toUpperCase ( ) ) ,
220263 } ) ;
221- const stagingDeployment = this . pipeline . addStage ( staging ) ;
264+ const stageDeployment = this . pipeline . addStage ( applicationStage ) ;
265+
266+ let integrationTestStage :CodeBuildStep | undefined = undefined ;
222267
223- if ( props . integTestScript ) {
268+ if ( attachIntegTest && props . integTestScript ) {
224269 // Extract the path and script name from the integration tests script path
225270 const [ integPath , integScript ] = SparkEmrCICDPipeline . extractPath ( props . integTestScript ) ;
226271
227- this . integrationTestStage = new CodeBuildStep ( ' IntegrationTests' , {
272+ integrationTestStage = new CodeBuildStep ( ` ${ stageName } IntegrationTests` , {
228273 input : buildStage . addOutputDirectory ( integPath ) ,
229274 commands : [ `chmod +x ${ integScript } && ./${ integScript } ` ] ,
230- envFromCfnOutputs : staging . stackOutputsEnv ,
275+ envFromCfnOutputs : applicationStage . stackOutputsEnv ,
231276 rolePolicyStatements : props . integTestPermissions ,
232277 } ) ;
233278 // Add a post step to run the integration tests
234- stagingDeployment . addPost ( this . integrationTestStage ) ;
279+ stageDeployment . addPost ( integrationTestStage ) ;
235280 }
236281
237- // Create the Production stage of the CICD
238- this . pipeline . addStage ( new ApplicationStage ( this , 'Production' , {
239- env : this . getAccountFromContext ( 'prod' ) ,
240- applicationStackFactory : props . applicationStackFactory ,
241- stage : CICDStage . PROD ,
242- } ) ) ;
243-
282+ return integrationTestStage ;
244283 }
245284
246285 /**
@@ -251,4 +290,34 @@ export class SparkEmrCICDPipeline extends TrackedConstruct {
251290 if ( ! account ) throw new Error ( `Missing context variable ${ name } ` ) ;
252291 return account ;
253292 }
293+
294+ /**
295+ * Retrieves the list of user defined environments from the context
296+ * @returns {CICDEnvironment[] } list of user defined environments
297+ */
298+ private getUserDefinedEnvironmentsFromContext ( ) : CICDEnvironment [ ] {
299+ const environments = this . node . tryGetContext ( 'environments' ) as CICDEnvironment [ ] ;
300+
301+ if ( ! environments ) {
302+ const missingContextError = new Error ( 'Missing context variable environments' ) ;
303+ missingContextError . name = MISSING_ENVIRONMENTS_ERROR ;
304+ throw missingContextError ;
305+ } else {
306+ //check for duplicates
307+
308+ const stageNameTracker = [ ] ;
309+
310+ for ( let e of environments ) {
311+ if ( stageNameTracker . indexOf ( e . stageName ) != - 1 ) {
312+ const duplicateStageError = new Error ( 'Duplicate stage name found' ) ;
313+ duplicateStageError . name = DUPLICATE_STAGE_NAME_ERROR ;
314+ throw duplicateStageError ;
315+ }
316+
317+ stageNameTracker . push ( e . stageName ) ;
318+ }
319+ }
320+
321+ return environments ;
322+ }
254323}
0 commit comments