diff --git a/README.md b/README.md index 530fa6b..763d160 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,12 @@ OpenSearch Continuous Integration is an open source CI system for OpenSearch and `npm run cdk deploy OpenSearch-CI-Config-Dev -- -c useSsl=false -c runWithOidc=false` +1. Locate the secret manager arns in the ci-config-stack outputs for `CASC_RELOAD_TOKEN` and update the secret value ([see docs](https://docs.aws.amazon.com/cli/latest/reference/secretsmanager/put-secret-value.html)) with the password you want to use to reload jenkins configuration. _Do not enclose it in quotes_ +``` +$aws secretsmanager put-secret-value \ +--secret-id MyCASCreloadTokenSecretARN \ +--secret-string CascReloadToken +``` 1. [Optional](#ssl-configuration) Configure the elements of the config stack for SSL configuration 1. [Optional](#setup-openid-connect-oidc-via-federate) Configure the elements setting up oidc via federate 1. Deploy the ci-stack, takes ~10 minutes to deploy (parameter values depend on step 2 and step 3) diff --git a/lib/ci-config-stack.ts b/lib/ci-config-stack.ts index 07805d5..5a08024 100644 --- a/lib/ci-config-stack.ts +++ b/lib/ci-config-stack.ts @@ -25,6 +25,8 @@ export class CIConfigStack extends Stack { static readonly OIDC_CONFIGURATION_VALUE_SECRET_EXPORT_VALUE: string = 'OIDCConfigValueSecret'; + static readonly CASC_RELOAD_TOKEN_SECRET_EXPORT_VALUE: string = 'casc'; + constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); @@ -46,6 +48,9 @@ export class CIConfigStack extends Stack { const OIDCConfigValuesSecret = new Secret(this, 'OIDCConfigValues', { description: 'OIDC params in JSON format', }); + const CascReloadTokenValuesSecret = new Secret(this, 'CascReloadTokenValue', { + description: 'Reload token (password) required for configuration as code plugin', + }); new CfnOutput(this, 'certificateArnSecret', { value: arnSecret.secretArn, @@ -76,5 +81,10 @@ export class CIConfigStack extends Stack { value: OIDCConfigValuesSecret.secretArn, exportName: CIConfigStack.OIDC_CONFIGURATION_VALUE_SECRET_EXPORT_VALUE, }); + + new CfnOutput(this, 'cascSecretValue', { + value: CascReloadTokenValuesSecret.secretArn, + exportName: CIConfigStack.CASC_RELOAD_TOKEN_SECRET_EXPORT_VALUE, + }); } } diff --git a/lib/ci-stack.ts b/lib/ci-stack.ts index 212332d..5d1afce 100644 --- a/lib/ci-stack.ts +++ b/lib/ci-stack.ts @@ -98,6 +98,7 @@ export class CIStack extends Stack { const importedRedirectUrlSecretBucketValue = Fn.importValue(`${CIConfigStack.REDIRECT_URL_SECRET_EXPORT_VALUE}`); const importedOidcConfigValuesSecretBucketValue = Fn.importValue(`${CIConfigStack.OIDC_CONFIGURATION_VALUE_SECRET_EXPORT_VALUE}`); const certificateArn = Secret.fromSecretCompleteArn(this, 'certificateArn', importedArnSecretBucketValue.toString()); + const importedReloadPasswordSecretsArn = Fn.importValue(`${CIConfigStack.CASC_RELOAD_TOKEN_SECRET_EXPORT_VALUE}`); const listenerCertificate = ListenerCertificate.fromArn(certificateArn.secretValue.toString()); const agentNode = new AgentNodes(this); const agentNodes: AgentNodeProps[] = [agentNode.AL2_X64, agentNode.AL2_X64_DOCKER_1, agentNode.AL2_ARM64, agentNode.AL2_ARM64_DOCKER_1, @@ -109,6 +110,7 @@ export class CIStack extends Stack { efsSG: securityGroups.efsSG, dataRetention: props.dataRetention ?? false, envVarsFilePath: props.envVarsFilePath ?? '', + reloadPasswordSecretsArn: importedReloadPasswordSecretsArn.toString(), sslCertContentsArn: importedContentsSecretBucketValue.toString(), sslCertChainArn: importedContentsChainBucketValue.toString(), sslCertPrivateKeyContentsArn: importedCertSecretBucketValue.toString(), diff --git a/lib/compute/jenkins-main-node.ts b/lib/compute/jenkins-main-node.ts index 5bc1a9a..7179faa 100644 --- a/lib/compute/jenkins-main-node.ts +++ b/lib/compute/jenkins-main-node.ts @@ -60,6 +60,7 @@ export interface JenkinsMainNodeProps extends HttpConfigProps, OidcFederateProps readonly vpc: Vpc; readonly sg: SecurityGroup; readonly envVarsFilePath: string; + readonly reloadPasswordSecretsArn: string; readonly failOnCloudInitError?: boolean; } @@ -144,6 +145,7 @@ export class JenkinsMainNode { props, props, jenkinsyaml, + props.reloadPasswordSecretsArn, this.EFS_ID, )), blockDevices: [{ @@ -222,7 +224,8 @@ export class JenkinsMainNode { } public static configElements(stackName: string, stackRegion: string, httpConfigProps: HttpConfigProps, - oidcFederateProps: OidcFederateProps, dataRetentionProps : DataRetentionProps, jenkinsyaml: string, efsId?: string): InitElement[] { + oidcFederateProps: OidcFederateProps, dataRetentionProps : DataRetentionProps, jenkinsyaml: string, + reloadPasswordSecretsArn: string, efsId?: string): InitElement[] { return [ InitPackage.yum('wget'), InitPackage.yum('openssl'), @@ -365,15 +368,15 @@ export class JenkinsMainNode { : 'echo Data rentention is disabled, not mounting efs'), InitFile.fromFileInline('/docker-compose.yml', join(__dirname, '../../resources/docker-compose.yml')), - InitCommand.shellCommand('systemctl start docker && docker-compose up -d'), + + InitCommand.shellCommand('systemctl start docker &&' + + ` var=\`aws --region ${stackRegion} secretsmanager get-secret-value --secret-id ${reloadPasswordSecretsArn} --query SecretString --output text\` &&` + + ' yq -i \'.services.jenkins.environment[1] = "CASC_RELOAD_TOKEN=\'$var\'"\' docker-compose.yml &&' + + ' docker-compose up -d'), // Commands are fired one after the other but it does not wait for the command to complete. // Therefore, sleep 90 seconds to wait for jenkins to start - InitCommand.shellCommand('sleep 90'), - - // Download jenkins-cli from the local machine - InitCommand.shellCommand('until $(curl --output /dev/null --silent --head --fail http://localhost:8080); do sleep 5; done &&' - + ' wget -O "jenkins-cli.jar" http://localhost:8080/jnlpJars/jenkins-cli.jar'), + InitCommand.shellCommand('sleep 60'), InitFile.fromFileInline('/initial_jenkins.yaml', jenkinsyaml), @@ -390,8 +393,8 @@ export class JenkinsMainNode { // Reload configuration via Jenkins.yaml InitCommand.shellCommand('cp /initial_jenkins.yaml /var/lib/jenkins/jenkins.yaml &&' - + ' java -jar /jenkins-cli.jar -s http://localhost:8080 reload-jcasc-configuration'), - + + ` var=\`aws --region ${stackRegion} secretsmanager get-secret-value --secret-id ${reloadPasswordSecretsArn} --query SecretString --output text\` &&` + + ' curl -f -X POST "http://localhost:8080/reload-configuration-as-code/?casc-reload-token=$var"'), ]; } diff --git a/resources/docker-compose.yml b/resources/docker-compose.yml index e908900..3d526b6 100644 --- a/resources/docker-compose.yml +++ b/resources/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.8' services: jenkins: image: opensearchstaging/jenkins:2.332.3-lts-jdk8 - restart: on-failure + restart: on-failure privileged: true tty: true user: root @@ -12,6 +12,7 @@ services: container_name: jenkins environment: - JENKINS_JAVA_OPTS="-Xmx8g" + - CASC_RELOAD_TOKEN=reloadPasswordHere volumes: - /var/lib/jenkins:/var/jenkins_home deploy: diff --git a/test/compute/jenkins-main-node.test.ts b/test/compute/jenkins-main-node.test.ts index e7a286f..4f54668 100644 --- a/test/compute/jenkins-main-node.test.ts +++ b/test/compute/jenkins-main-node.test.ts @@ -21,11 +21,12 @@ describe('JenkinsMainNode Config Elements', () => { runWithOidc: true, }, { dataRetention: true, - }, 'test/data/jenkins.yaml'); + }, 'test/data/jenkins.yaml', + 'ARN:ABC'); // THEN test('Config elements expected counts', async () => { - expect(configElements.filter((e) => e.elementType === 'COMMAND')).toHaveLength(19); + expect(configElements.filter((e) => e.elementType === 'COMMAND')).toHaveLength(18); expect(configElements.filter((e) => e.elementType === 'PACKAGE')).toHaveLength(10); expect(configElements.filter((e) => e.elementType === 'FILE')).toHaveLength(4); });