├── .gitignore ├── Jenkinsfile ├── LICENSE ├── README.md ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── amazonaws │ │ └── codedeploy │ │ ├── AWSClients.java │ │ └── AWSCodeDeployPublisher.java └── resources │ ├── com │ └── amazonaws │ │ └── codedeploy │ │ └── AWSCodeDeployPublisher │ │ ├── config.jelly │ │ ├── global.jelly │ │ ├── help-applicationName.html │ │ ├── help-awsAccessKey.html │ │ ├── help-awsSecretKey.html │ │ ├── help-deploymentGroupAppspec.html │ │ ├── help-deploymentGroupName.html │ │ ├── help-excludes.html │ │ ├── help-externalId.html │ │ ├── help-githubCommitId.html │ │ ├── help-githubRepository.html │ │ ├── help-iamRoleArn.html │ │ ├── help-includes.html │ │ ├── help-pollingTimeout.html │ │ ├── help-proxyHost.html │ │ ├── help-proxyPort.html │ │ ├── help-s3bucket.html │ │ ├── help-s3prefix.html │ │ ├── help-subdirectory.html │ │ └── help-waitForCompletion.html │ └── index.jelly └── test └── java └── com └── amazonaws └── codedeploy └── CodeDeployTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | /.classpath 2 | /.project 3 | /.settings 4 | /pom.xml.releaseBackup 5 | /release.properties 6 | /target 7 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | buildPlugin() 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AWS CodeDeploy Jenkins Plugin 2 | ============================= 3 | 4 | The AWS CodeDeploy Jenkins plugin provides a post-build step for your Jenkins 5 | project. Upon a successful build, it will zip the workspace, upload to S3, and 6 | start a new deployment. Optionally, you can set it to wait for the deployment to 7 | finish, making the final success contingent on the success of the deployment. 8 | 9 | ### Build Status 10 | 11 | [![Build Status](https://ci.jenkins.io/buildStatus/icon?job=Plugins/aws-codedeploy-plugin/master)](https://ci.jenkins.io/job/Plugins/job/aws-codedeploy-plugin/job/master/) 12 | 13 | Setting up 14 | ---------- 15 | 16 | After building and installing the plugin, some simple configuration is needed 17 | for your project. 18 | 19 | **Freestyle** 20 | 21 | 1. Open up your project configuration 22 | 1. In the `Post-build Actions` section, select "Deploy an application to AWS 23 | CodeDeploy" 24 | 1. Application Name, Deployment Group, Deployment Config, and region are all 25 | required options. 26 | 1. For authentication, there are two options. Either option requires that the 27 | associated role has, at minimum, a policy that permits `codedeploy:*` and 28 | `s3:Put*`. 29 | 1. Access/Secret key pair. For example, the keys associated with a specific 30 | IAM user. If left blank, the default chain of credentials will be checked. 31 | 1. Temporary access keys. These will use the global keys from the Jenkins 32 | instance. 33 | 34 | **Pipeline** 35 | 36 | 1. Create a [Jenkins Pipeline](https://wiki.jenkins-ci.org/display/JENKINS/Pipeline+Plugin) project 37 | 1. Use the Pipeline Snippet Generator 38 | 1. For 'Sample Step', choose 'step: General Build Step' 39 | 1. For 'Build Step', choose 'Deploy an application to AWS CodeDeploy' 40 | 1. populate variables and then 'Generate Groovy' 41 | 42 | Here is a rather blank example: 43 | 44 | step([$class: 'AWSCodeDeployPublisher', applicationName: '', awsAccessKey: '', awsSecretKey: '', credentials: 'awsAccessKey', deploymentGroupAppspec: false, deploymentGroupName: '', deploymentMethod: 'deploy', excludes: '', iamRoleArn: '', includes: '**', proxyHost: '', proxyPort: 0, region: 'ap-northeast-1', s3bucket: '', s3prefix: '', subdirectory: '', versionFileName: '', waitForCompletion: false]) 45 | 46 | License 47 | ------- 48 | 49 | This plugin is licensed under Apache 2.0. See the LICENSE file for more information. 50 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4.0.0 3 | 4 | org.jenkins-ci.plugins 5 | plugin 6 | 2.19 7 | 8 | 9 | codedeploy 10 | 1.20-SNAPSHOT 11 | hpi 12 | com.amazonaws 13 | AWS CodeDeploy Plugin for Jenkins 14 | Adds a post-build step to integrate Jenkins with AWS CodeDeploy 15 | https://wiki.jenkins-ci.org/display/JENKINS/AWS+Codedeploy+plugin 16 | 17 | 18 | 19 | jmcfar 20 | Josh Mcfarlane 21 | jmcfar@amazon.com 22 | 23 | 24 | 25 | 26 | 27 | Apache License, Version 2.0 28 | http://www.apache.org/licenses/LICENSE-2.0.txt 29 | repo 30 | 31 | 32 | 33 | 34 | scm:git:ssh://github.com/jenkinsci/aws-codedeploy-plugin.git 35 | scm:git:ssh://git@github.com/jenkinsci/aws-codedeploy-plugin.git 36 | https://github.com/jenkinsci/aws-codedeploy-plugin 37 | HEAD 38 | 39 | 40 | 41 | 42 | repo.jenkins-ci.org 43 | http://repo.jenkins-ci.org/public/ 44 | 45 | 46 | 47 | 48 | UTF-8 49 | 1.612 50 | 7 51 | 52 | 53 | 54 | 55 | org.jenkins-ci.plugins 56 | aws-java-sdk 57 | 1.11.248 58 | 59 | 60 | 61 | 62 | 63 | repo.jenkins-ci.org 64 | http://repo.jenkins-ci.org/public/ 65 | 66 | 67 | 68 | 69 | 70 | 71 | maven-release-plugin 72 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/codedeploy/AWSClients.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazonaws.codedeploy; 16 | 17 | import static org.apache.commons.lang.StringUtils.isEmpty; 18 | 19 | import java.io.File; 20 | import java.io.FileOutputStream; 21 | import java.io.IOException; 22 | import java.io.OutputStreamWriter; 23 | import java.io.Writer; 24 | import java.util.UUID; 25 | 26 | import com.amazonaws.AmazonServiceException; 27 | import com.amazonaws.ClientConfiguration; 28 | import com.amazonaws.auth.AWSCredentials; 29 | import com.amazonaws.auth.BasicAWSCredentials; 30 | import com.amazonaws.auth.BasicSessionCredentials; 31 | import com.amazonaws.regions.Region; 32 | import com.amazonaws.regions.Regions; 33 | import com.amazonaws.services.codedeploy.AmazonCodeDeployClient; 34 | import com.amazonaws.services.codedeploy.model.GetApplicationRequest; 35 | import com.amazonaws.services.identitymanagement.AmazonIdentityManagementClient; 36 | import com.amazonaws.services.identitymanagement.model.GetUserResult; 37 | import com.amazonaws.services.s3.AmazonS3Client; 38 | import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClient; 39 | import com.amazonaws.services.securitytoken.model.AssumeRoleRequest; 40 | import com.amazonaws.services.securitytoken.model.AssumeRoleResult; 41 | import com.amazonaws.services.securitytoken.model.Credentials; 42 | 43 | /** 44 | * @author gibbon 45 | */ 46 | public class AWSClients { 47 | /** 48 | * Index in the colon-separated ARN that contains the account id 49 | * Sample ARN: arn:aws:iam::123456789012:user/David 50 | **/ 51 | private static final int ARN_ACCOUNT_ID_INDEX = 4; 52 | 53 | public final AmazonCodeDeployClient codedeploy; 54 | public final AmazonS3Client s3; 55 | 56 | private final String region; 57 | private final String proxyHost; 58 | private final int proxyPort; 59 | 60 | public AWSClients(String region, AWSCredentials credentials, String proxyHost, int proxyPort) { 61 | this.region = region; 62 | this.proxyHost = proxyHost; 63 | this.proxyPort = proxyPort; 64 | 65 | //setup proxy connection: 66 | ClientConfiguration clientCfg = new ClientConfiguration(); 67 | if (proxyHost != null && proxyPort > 0 ) { 68 | clientCfg.setProxyHost(proxyHost); 69 | clientCfg.setProxyPort(proxyPort); 70 | } 71 | 72 | this.s3 = credentials != null ? new AmazonS3Client(credentials, clientCfg) : new AmazonS3Client(clientCfg); 73 | this.codedeploy = credentials != null ? new AmazonCodeDeployClient(credentials, clientCfg) : new AmazonCodeDeployClient(clientCfg); 74 | codedeploy.setRegion(Region.getRegion(Regions.fromName(this.region))); 75 | s3.setRegion(Region.getRegion(Regions.fromName(this.region))); 76 | } 77 | 78 | public static AWSClients fromDefaultCredentialChain(String region, String proxyHost, int proxyPort) { 79 | return new AWSClients(region, null, proxyHost, proxyPort); 80 | } 81 | 82 | public static AWSClients fromIAMRole(String region, String iamRole, String externalId, String proxyHost, int proxyPort) { 83 | return new AWSClients(region, getCredentials(iamRole, externalId), proxyHost, proxyPort); 84 | } 85 | 86 | public static AWSClients fromBasicCredentials(String region, String awsAccessKey, String awsSecretKey, String proxyHost, int proxyPort) { 87 | return new AWSClients(region, new BasicAWSCredentials(awsAccessKey, awsSecretKey), proxyHost, proxyPort); 88 | } 89 | 90 | /** 91 | * Via the default provider chain (i.e., global keys for this Jenkins instance), return the account ID for the 92 | * currently authenticated user. 93 | * @param proxyHost hostname of the proxy to use (if any) 94 | * @param proxyPort port of the proxy to use (if any) 95 | * @return 12-digit account id 96 | */ 97 | public static String getAccountId(String proxyHost, int proxyPort) { 98 | 99 | String arn = ""; 100 | try { 101 | ClientConfiguration clientCfg = new ClientConfiguration(); 102 | if (proxyHost != null && proxyPort > 0 ) { 103 | clientCfg.setProxyHost(proxyHost); 104 | clientCfg.setProxyPort(proxyPort); 105 | } 106 | AmazonIdentityManagementClient iam = new AmazonIdentityManagementClient(clientCfg); 107 | GetUserResult user = iam.getUser(); 108 | arn = user.getUser().getArn(); 109 | } catch (AmazonServiceException e) { 110 | if (e.getErrorCode().compareTo("AccessDenied") == 0) { 111 | String msg = e.getMessage(); 112 | int arnIdx = msg.indexOf("arn:aws"); 113 | if (arnIdx != -1) { 114 | int arnSpace = msg.indexOf(" ", arnIdx); 115 | arn = msg.substring(arnIdx, arnSpace); 116 | } 117 | } 118 | } 119 | 120 | String accountId = arn.split(":")[ARN_ACCOUNT_ID_INDEX]; 121 | return accountId; 122 | } 123 | 124 | public void testConnection(String s3bucket, String codeDeployApplication) throws Exception { 125 | String testKey = "tmp-" + UUID.randomUUID() + ".txt"; 126 | s3.putObject(s3bucket, testKey, createTestFile()); 127 | 128 | codedeploy.getApplication(new GetApplicationRequest().withApplicationName(codeDeployApplication)); 129 | } 130 | 131 | private File createTestFile() throws IOException { 132 | File file = File.createTempFile("codedeploy-jenkins-plugin", ".txt"); 133 | file.deleteOnExit(); 134 | 135 | Writer writer = new OutputStreamWriter(new FileOutputStream(file), "UTF-8"); 136 | writer.write(""); 137 | writer.close(); 138 | 139 | return file; 140 | } 141 | 142 | private static AWSCredentials getCredentials(String iamRole, String externalId) { 143 | if (isEmpty(iamRole)) return null; 144 | 145 | AWSSecurityTokenServiceClient sts = new AWSSecurityTokenServiceClient(); 146 | 147 | int credsDuration = (int) (AWSCodeDeployPublisher.DEFAULT_TIMEOUT_SECONDS 148 | * AWSCodeDeployPublisher.DEFAULT_POLLING_FREQUENCY_SECONDS); 149 | 150 | if (credsDuration > 3600) { 151 | credsDuration = 3600; 152 | } 153 | 154 | AssumeRoleResult assumeRoleResult = sts.assumeRole(new AssumeRoleRequest() 155 | .withRoleArn(iamRole) 156 | .withExternalId(externalId) 157 | .withDurationSeconds(credsDuration) 158 | .withRoleSessionName(AWSCodeDeployPublisher.ROLE_SESSION_NAME) 159 | ); 160 | 161 | Credentials stsCredentials = assumeRoleResult.getCredentials(); 162 | BasicSessionCredentials credentials = new BasicSessionCredentials( 163 | stsCredentials.getAccessKeyId(), 164 | stsCredentials.getSecretAccessKey(), 165 | stsCredentials.getSessionToken() 166 | ); 167 | 168 | return credentials; 169 | } 170 | 171 | public int getProxyPort() { 172 | return proxyPort; 173 | } 174 | 175 | public String getProxyHost() { 176 | return proxyHost; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/main/java/com/amazonaws/codedeploy/AWSCodeDeployPublisher.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazonaws.codedeploy; 16 | 17 | import com.amazonaws.regions.Regions; 18 | import com.amazonaws.services.codedeploy.model.ListApplicationsResult; 19 | import com.amazonaws.services.codedeploy.model.ListDeploymentGroupsRequest; 20 | import com.amazonaws.services.codedeploy.model.ListDeploymentGroupsResult; 21 | import com.amazonaws.services.codedeploy.model.RevisionLocation; 22 | import com.amazonaws.services.codedeploy.model.RevisionLocationType; 23 | import com.amazonaws.services.s3.model.PutObjectResult; 24 | import com.amazonaws.services.codedeploy.model.BundleType; 25 | import com.amazonaws.services.codedeploy.model.CreateDeploymentRequest; 26 | import com.amazonaws.services.codedeploy.model.CreateDeploymentResult; 27 | import com.amazonaws.services.codedeploy.model.DeploymentInfo; 28 | import com.amazonaws.services.codedeploy.model.DeploymentOverview; 29 | import com.amazonaws.services.codedeploy.model.DeploymentStatus; 30 | import com.amazonaws.services.codedeploy.model.GetDeploymentRequest; 31 | import com.amazonaws.services.codedeploy.model.RegisterApplicationRevisionRequest; 32 | import com.amazonaws.services.codedeploy.model.S3Location; 33 | import com.amazonaws.services.codedeploy.model.GitHubLocation; 34 | 35 | import hudson.AbortException; 36 | import hudson.FilePath; 37 | import hudson.Launcher; 38 | import hudson.Util; 39 | import hudson.Extension; 40 | import hudson.model.AbstractProject; 41 | import hudson.model.Result; 42 | import hudson.model.Run; 43 | import hudson.model.TaskListener; 44 | import hudson.tasks.BuildStepMonitor; 45 | import hudson.tasks.BuildStepDescriptor; 46 | import hudson.tasks.Publisher; 47 | import hudson.util.DirScanner; 48 | import hudson.util.FormValidation; 49 | import hudson.util.ListBoxModel; 50 | import jenkins.tasks.SimpleBuildStep; 51 | import net.sf.json.JSONObject; 52 | 53 | import org.apache.commons.lang.StringUtils; 54 | import org.apache.commons.io.FileUtils; 55 | import org.kohsuke.stapler.DataBoundConstructor; 56 | import org.kohsuke.stapler.QueryParameter; 57 | import org.kohsuke.stapler.StaplerRequest; 58 | 59 | import java.io.File; 60 | import java.io.FileInputStream; 61 | import java.io.FileOutputStream; 62 | import java.io.InputStreamReader; 63 | import java.io.IOException; 64 | import java.io.PrintStream; 65 | import java.util.Date; 66 | import java.util.Map; 67 | import java.util.UUID; 68 | 69 | import javax.annotation.Nonnull; 70 | import javax.servlet.ServletException; 71 | 72 | /** 73 | * The AWS CodeDeploy Publisher is a post-build plugin that adds the ability to start a new CodeDeploy deployment 74 | * with the project's workspace as the application revision. 75 | * 76 | * To configure, users must create an IAM role that allows "S3" and "CodeDeploy" actions and must be assumable by 77 | * the globally configured keys. This allows the plugin to get temporary credentials instead of requiring permanent 78 | * credentials to be configured for each project. 79 | */ 80 | public class AWSCodeDeployPublisher extends Publisher implements SimpleBuildStep { 81 | public static final long DEFAULT_TIMEOUT_SECONDS = 900; 82 | public static final long DEFAULT_POLLING_FREQUENCY_SECONDS = 15; 83 | public static final String ROLE_SESSION_NAME = "jenkins-codedeploy-plugin"; 84 | private static final Regions[] AVAILABLE_REGIONS = {Regions.AP_NORTHEAST_1, Regions.AP_SOUTHEAST_1, Regions.AP_SOUTHEAST_2, Regions.EU_WEST_1, Regions.US_EAST_1, Regions.US_WEST_2, Regions.EU_CENTRAL_1, Regions.US_WEST_1, Regions.SA_EAST_1, Regions.AP_NORTHEAST_2, Regions.AP_SOUTH_1, Regions.US_EAST_2, Regions.CA_CENTRAL_1, Regions.EU_WEST_2, Regions.CN_NORTH_1}; 85 | 86 | private final String s3bucket; 87 | private final String s3prefix; 88 | private final String githubRepository; 89 | private final String githubCommitId; 90 | private final String applicationName; 91 | private final String deploymentGroupName; // TODO allow for deployment to multiple groups 92 | private final String deploymentConfig; 93 | private final Long pollingTimeoutSec; 94 | private final Long pollingFreqSec; 95 | private final boolean deploymentGroupAppspec; 96 | private final boolean waitForCompletion; 97 | private final String externalId; 98 | private final String iamRoleArn; 99 | private final String region; 100 | private final String includes; 101 | private final String excludes; 102 | private final String subdirectory; 103 | private final String proxyHost; 104 | private final int proxyPort; 105 | 106 | private final String awsAccessKey; 107 | private final String awsSecretKey; 108 | private final String credentials; 109 | private final String deploymentMethod; 110 | private final String versionFileName; 111 | 112 | private PrintStream logger; 113 | private Map envVars; 114 | // Fields in config.jelly must match the parameter names in the "DataBoundConstructor" 115 | @DataBoundConstructor 116 | public AWSCodeDeployPublisher( 117 | String s3bucket, 118 | String s3prefix, 119 | String githubRepository, 120 | String githubCommitId, 121 | String applicationName, 122 | String deploymentGroupName, 123 | String deploymentConfig, 124 | String region, 125 | Boolean deploymentGroupAppspec, 126 | Boolean waitForCompletion, 127 | Long pollingTimeoutSec, 128 | Long pollingFreqSec, 129 | String credentials, 130 | String versionFileName, 131 | String deploymentMethod, 132 | String awsAccessKey, 133 | String awsSecretKey, 134 | String iamRoleArn, 135 | String externalId, 136 | String includes, 137 | String proxyHost, 138 | int proxyPort, 139 | String excludes, 140 | String subdirectory) { 141 | 142 | this.externalId = externalId; 143 | this.applicationName = applicationName; 144 | this.deploymentGroupName = deploymentGroupName; 145 | if (deploymentConfig != null && deploymentConfig.length() == 0) { 146 | this.deploymentConfig = null; 147 | } else { 148 | this.deploymentConfig = deploymentConfig; 149 | } 150 | this.region = region; 151 | this.includes = includes; 152 | this.excludes = excludes; 153 | this.subdirectory = subdirectory; 154 | this.proxyHost = proxyHost; 155 | this.proxyPort = proxyPort; 156 | this.credentials = credentials; 157 | this.deploymentMethod = deploymentMethod; 158 | this.versionFileName = versionFileName; 159 | this.awsAccessKey = awsAccessKey; 160 | this.awsSecretKey = awsSecretKey; 161 | this.iamRoleArn = iamRoleArn; 162 | this.deploymentGroupAppspec = deploymentGroupAppspec; 163 | 164 | if (waitForCompletion != null && waitForCompletion) { 165 | this.waitForCompletion = waitForCompletion; 166 | if (pollingTimeoutSec == null) { 167 | this.pollingTimeoutSec = DEFAULT_TIMEOUT_SECONDS; 168 | } else { 169 | this.pollingTimeoutSec = pollingTimeoutSec; 170 | } 171 | if (pollingFreqSec == null) { 172 | this.pollingFreqSec = DEFAULT_POLLING_FREQUENCY_SECONDS; 173 | } else { 174 | this.pollingFreqSec = pollingFreqSec; 175 | } 176 | } else { 177 | this.waitForCompletion = false; 178 | this.pollingTimeoutSec = null; 179 | this.pollingFreqSec = null; 180 | } 181 | 182 | this.s3bucket = s3bucket; 183 | if (s3prefix == null || s3prefix.equals("/") || s3prefix.length() == 0) { 184 | this.s3prefix = ""; 185 | } else { 186 | this.s3prefix = s3prefix; 187 | } 188 | 189 | this.githubRepository = githubRepository; 190 | this.githubCommitId = githubCommitId; 191 | } 192 | 193 | @Override 194 | public void perform(@Nonnull Run build, @Nonnull FilePath workspace, @Nonnull Launcher launcher, @Nonnull TaskListener listener) throws IOException, InterruptedException { 195 | this.logger = listener.getLogger(); 196 | envVars = build.getEnvironment(listener); 197 | final boolean buildFailed = build.getResult() == Result.FAILURE; 198 | if (buildFailed) { 199 | logger.println("Skipping CodeDeploy publisher as build failed"); 200 | return; 201 | } 202 | 203 | final AWSClients aws; 204 | if ("awsAccessKey".equals(credentials)) { 205 | if (StringUtils.isEmpty(this.awsAccessKey) && StringUtils.isEmpty(this.awsSecretKey)) { 206 | aws = AWSClients.fromDefaultCredentialChain( 207 | this.region, 208 | this.proxyHost, 209 | this.proxyPort); 210 | } else { 211 | aws = AWSClients.fromBasicCredentials( 212 | this.region, 213 | this.awsAccessKey, 214 | this.awsSecretKey, 215 | this.proxyHost, 216 | this.proxyPort); 217 | } 218 | } else { 219 | aws = AWSClients.fromIAMRole( 220 | this.region, 221 | this.iamRoleArn, 222 | this.getDescriptor().getExternalId(), 223 | this.proxyHost, 224 | this.proxyPort); 225 | } 226 | 227 | boolean success = false; 228 | 229 | try { 230 | 231 | verifyCodeDeployApplication(aws); 232 | 233 | final String projectName = build.getDisplayName(); 234 | if (workspace == null) { 235 | throw new IllegalArgumentException("No workspace present for the build."); 236 | } 237 | 238 | RevisionLocation revisionLocation; 239 | 240 | if (!StringUtils.isEmpty(this.githubRepository) && !StringUtils.isEmpty(this.githubCommitId)) { 241 | revisionLocation = createFromGitHub(); 242 | } else { 243 | final FilePath sourceDirectory = getSourceDirectory(workspace); 244 | revisionLocation = zipAndUpload(aws, projectName, sourceDirectory); 245 | } 246 | 247 | registerRevision(aws, revisionLocation); 248 | if ("onlyRevision".equals(deploymentMethod)){ 249 | success = true; 250 | } else { 251 | 252 | String deploymentId = createDeployment(aws, revisionLocation); 253 | 254 | success = waitForDeployment(aws, deploymentId); 255 | } 256 | 257 | } catch (Exception e) { 258 | 259 | this.logger.println("Failed CodeDeploy post-build step; exception follows."); 260 | this.logger.println(e.getMessage()); 261 | e.printStackTrace(this.logger); 262 | } 263 | 264 | if (!success) { 265 | throw new AbortException(); 266 | } 267 | } 268 | 269 | private FilePath getSourceDirectory(FilePath basePath) throws IOException, InterruptedException { 270 | String subdirectory = StringUtils.trimToEmpty(getSubdirectoryFromEnv()); 271 | if (!subdirectory.isEmpty() && !subdirectory.startsWith("/")) { 272 | subdirectory = "/" + subdirectory; 273 | } 274 | FilePath sourcePath = basePath.withSuffix(subdirectory).absolutize(); 275 | if (!sourcePath.isDirectory() || !isSubDirectory(basePath, sourcePath)) { 276 | throw new IllegalArgumentException("Provided path (resolved as '" + sourcePath 277 | +"') is not a subdirectory of the workspace (resolved as '" + basePath + "')"); 278 | } 279 | return sourcePath; 280 | } 281 | 282 | private boolean isSubDirectory(FilePath parent, FilePath child) { 283 | FilePath parentFolder = child; 284 | while (parentFolder!=null) { 285 | if (parent.equals(parentFolder)) { 286 | return true; 287 | } 288 | parentFolder = parentFolder.getParent(); 289 | } 290 | return false; 291 | } 292 | 293 | private void verifyCodeDeployApplication(AWSClients aws) throws IllegalArgumentException { 294 | // Check that the application exists 295 | ListApplicationsResult applications = aws.codedeploy.listApplications(); 296 | String applicationName = getApplicationNameFromEnv(); 297 | String deploymentGroupName = getDeploymentGroupNameFromEnv(); 298 | 299 | if (!applications.getApplications().contains(applicationName)) { 300 | throw new IllegalArgumentException("Cannot find application named '" + applicationName + "'"); 301 | } 302 | 303 | // Check that the deployment group exists 304 | ListDeploymentGroupsResult deploymentGroups = aws.codedeploy.listDeploymentGroups( 305 | new ListDeploymentGroupsRequest() 306 | .withApplicationName(applicationName) 307 | ); 308 | 309 | if (!deploymentGroups.getDeploymentGroups().contains(deploymentGroupName)) { 310 | throw new IllegalArgumentException("Cannot find deployment group named '" + deploymentGroupName + "'"); 311 | } 312 | } 313 | 314 | private RevisionLocation zipAndUpload(AWSClients aws, String projectName, FilePath sourceDirectory) throws IOException, InterruptedException, IllegalArgumentException { 315 | 316 | File zipFile = null; 317 | File versionFile; 318 | versionFile = new File(sourceDirectory + "/" + versionFileName); 319 | 320 | InputStreamReader reader = null; 321 | String version = null; 322 | try { 323 | reader = new InputStreamReader(new FileInputStream(versionFile), "UTF-8"); 324 | char[] chars = new char[(int) versionFile.length() -1]; 325 | reader.read(chars); 326 | version = new String(chars); 327 | reader.close(); 328 | } catch (IOException e) { 329 | e.printStackTrace(); 330 | } finally { 331 | if(reader !=null){reader.close();} 332 | } 333 | 334 | if (version != null){ 335 | zipFile = new File("/tmp/" + projectName + "-" + version + ".zip"); 336 | final boolean fileCreated = zipFile.createNewFile(); 337 | if (!fileCreated) { 338 | logger.println("File already exists, overwriting: " + zipFile.getPath()); 339 | } 340 | } else { 341 | zipFile = File.createTempFile(projectName + "-", ".zip"); 342 | } 343 | 344 | String key; 345 | File appspec; 346 | File dest; 347 | String deploymentGroupName = getDeploymentGroupNameFromEnv(); 348 | String prefix = getS3PrefixFromEnv(); 349 | String bucket = getS3BucketFromEnv(); 350 | 351 | if(bucket.indexOf("/") > 0){ 352 | throw new IllegalArgumentException("S3 Bucket field cannot contain any subdirectories. Bucket name only!"); 353 | } 354 | 355 | try { 356 | if (this.deploymentGroupAppspec) { 357 | appspec = new File(sourceDirectory + "/appspec." + deploymentGroupName + ".yml"); 358 | if (appspec.exists()) { 359 | dest = new File(sourceDirectory + "/appspec.yml"); 360 | FileUtils.copyFile(appspec, dest); 361 | logger.println("Use appspec." + deploymentGroupName + ".yml"); 362 | } 363 | if (!appspec.exists()) { 364 | throw new IllegalArgumentException("/appspec." + deploymentGroupName + ".yml file does not exist" ); 365 | } 366 | 367 | } 368 | 369 | logger.println("Zipping files into " + zipFile.getAbsolutePath()); 370 | 371 | FileOutputStream outputStream = new FileOutputStream(zipFile); 372 | try { 373 | sourceDirectory.zip( 374 | outputStream, 375 | new DirScanner.Glob(this.includes, this.excludes) 376 | ); 377 | } finally { 378 | outputStream.close(); 379 | } 380 | 381 | if (prefix.isEmpty()) { 382 | key = zipFile.getName(); 383 | } else { 384 | key = Util.replaceMacro(prefix, envVars); 385 | if (prefix.endsWith("/")) { 386 | key += zipFile.getName(); 387 | } else { 388 | key += "/" + zipFile.getName(); 389 | } 390 | } 391 | logger.println("Uploading zip to s3://" + bucket + "/" + key); 392 | PutObjectResult s3result = aws.s3.putObject(bucket, key, zipFile); 393 | 394 | S3Location s3Location = new S3Location(); 395 | s3Location.setBucket(bucket); 396 | s3Location.setKey(key); 397 | s3Location.setBundleType(BundleType.Zip); 398 | s3Location.setETag(s3result.getETag()); 399 | 400 | RevisionLocation revisionLocation = new RevisionLocation(); 401 | revisionLocation.setRevisionType(RevisionLocationType.S3); 402 | revisionLocation.setS3Location(s3Location); 403 | 404 | return revisionLocation; 405 | } finally { 406 | final boolean deleted = zipFile.delete(); 407 | if (!deleted) { 408 | logger.println("Failed to clean up file " + zipFile.getPath()); 409 | } 410 | } 411 | } 412 | 413 | private RevisionLocation createFromGitHub() { 414 | GitHubLocation githubLocation = new GitHubLocation(); 415 | githubLocation.setRepository(this.githubRepository); 416 | githubLocation.setCommitId(this.githubCommitId); 417 | 418 | RevisionLocation revisionLocation = new RevisionLocation(); 419 | revisionLocation.setRevisionType(RevisionLocationType.GitHub); 420 | revisionLocation.setGitHubLocation(githubLocation); 421 | 422 | return revisionLocation; 423 | } 424 | 425 | private void registerRevision(AWSClients aws, RevisionLocation revisionLocation) { 426 | 427 | String applicationName = getApplicationNameFromEnv(); 428 | this.logger.println("Registering revision for application '" + applicationName + "'"); 429 | 430 | aws.codedeploy.registerApplicationRevision( 431 | new RegisterApplicationRevisionRequest() 432 | .withApplicationName(applicationName) 433 | .withRevision(revisionLocation) 434 | .withDescription("Application revision registered via Jenkins") 435 | ); 436 | } 437 | 438 | private String createDeployment(AWSClients aws, RevisionLocation revisionLocation) throws Exception { 439 | 440 | this.logger.println("Creating deployment with revision at " + revisionLocation); 441 | 442 | CreateDeploymentResult createDeploymentResult = aws.codedeploy.createDeployment( 443 | new CreateDeploymentRequest() 444 | .withDeploymentConfigName(getDeploymentConfigFromEnv()) 445 | .withDeploymentGroupName(getDeploymentGroupNameFromEnv()) 446 | .withApplicationName(getApplicationNameFromEnv()) 447 | .withRevision(revisionLocation) 448 | .withDescription("Deployment created by Jenkins") 449 | ); 450 | 451 | return createDeploymentResult.getDeploymentId(); 452 | } 453 | 454 | private boolean waitForDeployment(AWSClients aws, String deploymentId) throws InterruptedException { 455 | 456 | if (!this.waitForCompletion) { 457 | return true; 458 | } 459 | 460 | logger.println("Monitoring deployment with ID " + deploymentId + "..."); 461 | GetDeploymentRequest deployInfoRequest = new GetDeploymentRequest(); 462 | deployInfoRequest.setDeploymentId(deploymentId); 463 | 464 | DeploymentInfo deployStatus = aws.codedeploy.getDeployment(deployInfoRequest).getDeploymentInfo(); 465 | 466 | long startTimeMillis; 467 | if (deployStatus == null || deployStatus.getStartTime() == null) { 468 | startTimeMillis = new Date().getTime(); 469 | } else { 470 | startTimeMillis = deployStatus.getStartTime().getTime(); 471 | } 472 | 473 | boolean success = true; 474 | long pollingTimeoutMillis = this.pollingTimeoutSec * 1000L; 475 | long pollingFreqMillis = this.pollingFreqSec * 1000L; 476 | 477 | while (deployStatus == null || deployStatus.getCompleteTime() == null) { 478 | 479 | if (deployStatus == null) { 480 | logger.println("Deployment status: unknown."); 481 | } else { 482 | DeploymentOverview overview = deployStatus.getDeploymentOverview(); 483 | logger.println("Deployment status: " + deployStatus.getStatus() + "; instances: " + overview); 484 | } 485 | 486 | deployStatus = aws.codedeploy.getDeployment(deployInfoRequest).getDeploymentInfo(); 487 | Date now = new Date(); 488 | 489 | if (now.getTime() - startTimeMillis >= pollingTimeoutMillis) { 490 | this.logger.println("Exceeded maximum polling time of " + pollingTimeoutMillis + " milliseconds."); 491 | success = false; 492 | break; 493 | } 494 | 495 | Thread.sleep(pollingFreqMillis); 496 | } 497 | 498 | logger.println("Deployment status: " + deployStatus.getStatus() + "; instances: " + deployStatus.getDeploymentOverview()); 499 | 500 | if (!deployStatus.getStatus().equals(DeploymentStatus.Succeeded.toString())) { 501 | this.logger.println("Deployment did not succeed. Final status: " + deployStatus.getStatus()); 502 | success = false; 503 | } 504 | 505 | return success; 506 | } 507 | 508 | // Overridden for better type safety. 509 | // If your plugin doesn't really define any property on Descriptor, 510 | // you don't have to do this. 511 | @Override 512 | public DescriptorImpl getDescriptor() { 513 | 514 | return (DescriptorImpl) super.getDescriptor(); 515 | } 516 | 517 | public BuildStepMonitor getRequiredMonitorService() { 518 | return BuildStepMonitor.NONE; 519 | } 520 | 521 | /** 522 | * 523 | * Descriptor for {@link AWSCodeDeployPublisher}. Used as a singleton. 524 | * The class is marked as public so that it can be accessed from views. 525 | * 526 | * See src/main/resources/com/amazonaws/codedeploy/AWSCodeDeployPublisher/*.jelly 527 | * for the actual HTML fragment for the configuration screen. 528 | */ 529 | @Extension // This indicates to Jenkins that this is an implementation of an extension point. 530 | public static final class DescriptorImpl extends BuildStepDescriptor { 531 | 532 | private String externalId; 533 | private String awsAccessKey; 534 | private String awsSecretKey; 535 | private String proxyHost; 536 | private int proxyPort; 537 | 538 | /** 539 | * In order to load the persisted global configuration, you have to 540 | * call load() in the constructor. 541 | */ 542 | public DescriptorImpl() { 543 | load(); 544 | 545 | if (externalId == null) { 546 | setExternalId(UUID.randomUUID().toString()); 547 | } 548 | } 549 | 550 | public FormValidation doCheckName(@QueryParameter String value) 551 | throws IOException, ServletException { 552 | if (value.length() == 0) 553 | return FormValidation.error("Please add the appropriate values"); 554 | return FormValidation.ok(); 555 | } 556 | 557 | public boolean isApplicable(Class aClass) { 558 | // Indicates that this builder can be used with all kinds of project types 559 | return true; 560 | } 561 | 562 | /** 563 | * This human readable name is used in the configuration screen. 564 | */ 565 | public String getDisplayName() { 566 | return "Deploy an application to AWS CodeDeploy"; 567 | } 568 | 569 | @Override 570 | public boolean configure(StaplerRequest req, JSONObject formData) throws FormException { 571 | 572 | awsAccessKey = formData.getString("awsAccessKey"); 573 | awsSecretKey = formData.getString("awsSecretKey"); 574 | proxyHost = formData.getString("proxyHost"); 575 | proxyPort = Integer.parseInt(formData.getString("proxyPort")); 576 | 577 | req.bindJSON(this, formData); 578 | save(); 579 | return super.configure(req, formData); 580 | } 581 | 582 | public String getExternalId() { 583 | return externalId; 584 | } 585 | 586 | public void setExternalId(String externalId) { 587 | this.externalId = externalId; 588 | } 589 | 590 | public void setProxyHost(String proxyHost) { 591 | this.proxyHost = proxyHost; 592 | } 593 | 594 | public String getProxyHost() { 595 | return proxyHost; 596 | } 597 | 598 | public void setProxyPort(int proxyPort) { 599 | this.proxyPort = proxyPort; 600 | } 601 | 602 | public int getProxyPort() { 603 | return proxyPort; 604 | } 605 | 606 | public String getAccountId() { 607 | return AWSClients.getAccountId(getProxyHost(), getProxyPort()); 608 | } 609 | 610 | public FormValidation doTestConnection( 611 | @QueryParameter String s3bucket, 612 | @QueryParameter String applicationName, 613 | @QueryParameter String region, 614 | @QueryParameter String iamRoleArn, 615 | @QueryParameter String proxyHost, 616 | @QueryParameter int proxyPort) { 617 | 618 | System.out.println("Testing connection with parameters: " 619 | + s3bucket + "," 620 | + applicationName + "," 621 | + region + "," 622 | + iamRoleArn + "," 623 | + this.externalId + "," 624 | + proxyHost + "," 625 | + proxyPort 626 | ); 627 | 628 | try { 629 | AWSClients awsClients = AWSClients.fromIAMRole(region, iamRoleArn, this.externalId, proxyHost, proxyPort); 630 | awsClients.testConnection(s3bucket, applicationName); 631 | } catch (Exception e) { 632 | return FormValidation.error("Connection test failed with error: " + e.getMessage()); 633 | } 634 | 635 | return FormValidation.ok("Connection test passed."); 636 | } 637 | 638 | public ListBoxModel doFillRegionItems() { 639 | ListBoxModel items = new ListBoxModel(); 640 | for (Regions region : AVAILABLE_REGIONS) { 641 | items.add(region.toString(), region.getName()); 642 | } 643 | return items; 644 | } 645 | 646 | public String getAwsSecretKey() 647 | { 648 | return awsSecretKey; 649 | } 650 | 651 | public void setAwsSecretKey(String awsSecretKey) 652 | { 653 | this.awsSecretKey = awsSecretKey; 654 | } 655 | 656 | public String getAwsAccessKey() 657 | { 658 | return awsAccessKey; 659 | } 660 | 661 | public void setAwsAccessKey(String awsAccessKey) 662 | { 663 | this.awsAccessKey = awsAccessKey; 664 | } 665 | 666 | } 667 | 668 | public String getApplicationName() { 669 | return applicationName; 670 | } 671 | 672 | public String getDeploymentGroupName() { 673 | return deploymentGroupName; 674 | } 675 | 676 | public String getDeploymentConfig() { 677 | return deploymentConfig; 678 | } 679 | 680 | public String getS3bucket() { 681 | return s3bucket; 682 | } 683 | 684 | public String getS3prefix() { 685 | return s3prefix; 686 | } 687 | 688 | public Long getPollingTimeoutSec() { 689 | return pollingTimeoutSec; 690 | } 691 | 692 | public String getIamRoleArn() { 693 | return iamRoleArn; 694 | } 695 | 696 | public String getAwsAccessKey() { 697 | return awsAccessKey; 698 | } 699 | 700 | public String getAwsSecretKey() { 701 | return awsSecretKey; 702 | } 703 | 704 | public Long getPollingFreqSec() { 705 | return pollingFreqSec; 706 | } 707 | 708 | public String getExternalId() { 709 | return externalId; 710 | } 711 | 712 | public String getDeploymentMethod() { 713 | return deploymentMethod; 714 | } 715 | 716 | public String getVersionFileName() { 717 | return versionFileName; 718 | } 719 | 720 | public boolean getWaitForCompletion() { 721 | return waitForCompletion; 722 | } 723 | 724 | public boolean getDeploymentGroupAppspec() { 725 | return deploymentGroupAppspec; 726 | } 727 | 728 | public String getCredentials() { 729 | return credentials; 730 | } 731 | 732 | public String getIncludes() { 733 | return includes; 734 | } 735 | 736 | public String getExcludes() { 737 | return excludes; 738 | } 739 | 740 | public String getSubdirectory() { 741 | return subdirectory; 742 | } 743 | 744 | public String getRegion() { 745 | return region; 746 | } 747 | 748 | public String getProxyHost() { 749 | return proxyHost; 750 | } 751 | 752 | public int getProxyPort() { 753 | return proxyPort; 754 | } 755 | 756 | public String getApplicationNameFromEnv() { 757 | return Util.replaceMacro(this.applicationName, envVars); 758 | } 759 | 760 | public String getDeploymentGroupNameFromEnv() { 761 | return Util.replaceMacro(this.deploymentGroupName, envVars); 762 | } 763 | 764 | public String getDeploymentConfigFromEnv() { 765 | return Util.replaceMacro(this.deploymentConfig, envVars); 766 | } 767 | 768 | public String getS3BucketFromEnv() { 769 | return Util.replaceMacro(this.s3bucket, envVars); 770 | } 771 | 772 | public String getS3PrefixFromEnv() { 773 | return Util.replaceMacro(this.s3prefix, envVars); 774 | } 775 | 776 | public String getSubdirectoryFromEnv() { 777 | return Util.replaceMacro(this.subdirectory, envVars); 778 | } 779 | } 780 | -------------------------------------------------------------------------------- /src/main/resources/com/amazonaws/codedeploy/AWSCodeDeployPublisher/config.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | Register the new revision with the specified CodeDeploy application but do not deploy it. 53 | 54 | 55 | 56 | 57 | 58 | Register the new revision and deploy it to the specified CodeDeploy deployment group. 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | If these keys are left blank, the plugin will attempt to use credentials from the default provider chain. That 73 | is: Environment Variables, Java System properties, credentials profile file, and finally, EC2 Instance profile. 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | This plugin uses temporary access keys via the AWS Security Token Service. For instructions on setting this up, 89 | see the help text on the "IAM Role ARN" field above.

90 | AWS Account ID: ${descriptor.accountId}
91 | External ID: ${descriptor.externalId} 92 |
93 | 94 |
95 |
96 | -------------------------------------------------------------------------------- /src/main/resources/com/amazonaws/codedeploy/AWSCodeDeployPublisher/global.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | If keys are not specified, the plugin will use the default credentials providers and pull from one of: 6 | Environment variables, credentials profile, or IAM instance role. The latter is recommended if running on an EC2 7 | instance. 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/main/resources/com/amazonaws/codedeploy/AWSCodeDeployPublisher/help-applicationName.html: -------------------------------------------------------------------------------- 1 |
2 | The name of the AWS CodeDeploy application you wish to deploy to. This plugin assumes that you've 3 | already created the application and deployment group. If you haven't already, work through the How 5 | to create an Application with AWS CodeDeploy documentation. You can use environment variables 6 | in this field. 7 |
8 | -------------------------------------------------------------------------------- /src/main/resources/com/amazonaws/codedeploy/AWSCodeDeployPublisher/help-awsAccessKey.html: -------------------------------------------------------------------------------- 1 |
2 |

AWS Access and Secret keys to use for this deployment. At minimum the keys must be allowed to execute 3 | codedeploy:* and s3:Put*. It's a best practice to have these keys be from an IAM role 4 | with limited scope.

5 | 6 |

7 | If your Jenkins install is running on an EC2 instance with an associate IAM role, you can leave these fields 8 | blank. You will just need to ensure that the role has the correct policies. 9 |

10 |
11 | -------------------------------------------------------------------------------- /src/main/resources/com/amazonaws/codedeploy/AWSCodeDeployPublisher/help-awsSecretKey.html: -------------------------------------------------------------------------------- 1 |
2 |

AWS Access and Secret keys to use for this deployment. At minimum the keys must be allowed to execute 3 | codedeploy:* and s3:Put*. It's a best practice to have these keys be from an IAM role 4 | with limited scope.

5 | 6 |

7 | If your Jenkins install is running on an EC2 instance with an associate IAM role, you can leave these fields 8 | blank. You will just need to ensure that the role has the correct policies. 9 |

10 |
11 | -------------------------------------------------------------------------------- /src/main/resources/com/amazonaws/codedeploy/AWSCodeDeployPublisher/help-deploymentGroupAppspec.html: -------------------------------------------------------------------------------- 1 |
2 |

If checked, the build will use a dedicated appspec.yml file per deployment group.

3 |

The appspec file should be named "appspec.DEPLOYMENT_GROUP_NAME.yml" and must be present in the jenkins project workspace.



4 |

e.g.: appsec.staging.yml

5 |
6 | -------------------------------------------------------------------------------- /src/main/resources/com/amazonaws/codedeploy/AWSCodeDeployPublisher/help-deploymentGroupName.html: -------------------------------------------------------------------------------- 1 |
2 | The name of the AWS CodeDeploy deployment group attached to your application that you want to 3 | deploy to. This plugin assumes that you've already created the application and deployment group. 4 | If you haven't already, work through the How to create an Application with AWS CodeDeploy documentation. 5 |
6 | -------------------------------------------------------------------------------- /src/main/resources/com/amazonaws/codedeploy/AWSCodeDeployPublisher/help-excludes.html: -------------------------------------------------------------------------------- 1 |
2 | Includes and Excludes together define the file(s) that will be contained in the 3 | application revision that is uploaded to Amazon S3. 4 |
5 | -------------------------------------------------------------------------------- /src/main/resources/com/amazonaws/codedeploy/AWSCodeDeployPublisher/help-externalId.html: -------------------------------------------------------------------------------- 1 |
2 | The External ID you should use in the IAM cross-account access policy. 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/com/amazonaws/codedeploy/AWSCodeDeployPublisher/help-githubCommitId.html: -------------------------------------------------------------------------------- 1 |
2 | The GitHub commit id hash of the revision to be pushed to CodeDeploy. This is to be used in Pipeline mode. 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/com/amazonaws/codedeploy/AWSCodeDeployPublisher/help-githubRepository.html: -------------------------------------------------------------------------------- 1 |
2 | The GitHub repository used to pull the revision from. This is to be used in Pipeline mode. 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/com/amazonaws/codedeploy/AWSCodeDeployPublisher/help-iamRoleArn.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | In order to keep your application(s) more secure, this plugin only uses temporary credentials via STS, scoped to each application. To set this up: 4 |

5 | 6 |
    7 |
  1. Log into the AWS Management Console, and navigate to the Identity and Access Management console.
  2. 8 |
  3. Click on Roles, then click Create New Role.
  4. 9 |
  5. Give an appropriate name for this role (for example, "JenkinsCodeDeployProject").
  6. 10 |
  7. In the "Select Role Type" screen, click "Role for Cross-Account Access" then select Allows IAM users from a 3rd party AWS account to access this account.
  8. 11 |
  9. The account and external IDs for this Jenkins project are listed below
  10. 12 |
  11. In the policy screen, select Custom Policy and copy-paste the following policy: 13 |
      14 |
    • {"Version": "2012-10-17", "Statement": [{"Effect": "Allow", "Action": ["codedeploy:*", "s3:*"], "Resource": "*"}]}
    • 15 |
    16 |
  12. 17 |
  13. Click Create Role, then copy-paste the Role ARN into the below field.
  14. 18 |
  15. Click the Test Connection button to ensure that the permissions are set up properly.
  16. 19 |
20 |
21 | -------------------------------------------------------------------------------- /src/main/resources/com/amazonaws/codedeploy/AWSCodeDeployPublisher/help-includes.html: -------------------------------------------------------------------------------- 1 |
2 | Includes and Excludes together define the file(s) that will be contained in the 3 | application revision that is uploaded to Amazon S3. 4 |
5 | -------------------------------------------------------------------------------- /src/main/resources/com/amazonaws/codedeploy/AWSCodeDeployPublisher/help-pollingTimeout.html: -------------------------------------------------------------------------------- 1 |
2 | The maximum amount of time (in milliseconds) to wait for a deployment to finish. 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/com/amazonaws/codedeploy/AWSCodeDeployPublisher/help-proxyHost.html: -------------------------------------------------------------------------------- 1 |
2 |

Proxy host DNS name

3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/com/amazonaws/codedeploy/AWSCodeDeployPublisher/help-proxyPort.html: -------------------------------------------------------------------------------- 1 |
2 |

Proxy host port

3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/com/amazonaws/codedeploy/AWSCodeDeployPublisher/help-s3bucket.html: -------------------------------------------------------------------------------- 1 |
2 | The bucket in S3 where new AWS CodeDeploy revisions will be uploaded. 3 |
4 | -------------------------------------------------------------------------------- /src/main/resources/com/amazonaws/codedeploy/AWSCodeDeployPublisher/help-s3prefix.html: -------------------------------------------------------------------------------- 1 |
2 | The prefix in the S3 Bucket to prepend to the AWS CodeDeploy revision. Default is the root of the bucket. You can 3 | use environment variables in this field. 4 |
5 | -------------------------------------------------------------------------------- /src/main/resources/com/amazonaws/codedeploy/AWSCodeDeployPublisher/help-subdirectory.html: -------------------------------------------------------------------------------- 1 |
2 | A subdirectory inside the workspace to be packed instead of the whole workspace. Remember that the 3 | appspec.yml must be placed at the top of the .zip archive. The excludes and includes will 4 | be applied based on this path. 5 |
6 | -------------------------------------------------------------------------------- /src/main/resources/com/amazonaws/codedeploy/AWSCodeDeployPublisher/help-waitForCompletion.html: -------------------------------------------------------------------------------- 1 |
2 |

If checked, this build will wait for the AWS CodeDeploy deployment to finish (with either success or failure). Polling 3 | Timeout, below, sets the maximum amount of time to wait.

If unchecked, the deployment will be handed off 4 | to AWS CodeDeploy and the build will move on to the next step.

5 | 6 |

The build will be marked a failure if either the timeout is reached or the deployment fails. The 7 | build log will indicate which.

8 |
9 | -------------------------------------------------------------------------------- /src/main/resources/index.jelly: -------------------------------------------------------------------------------- 1 | 2 |
3 | This plugin provides a "post-build" step for AWS CodeDeploy. 4 |
5 | -------------------------------------------------------------------------------- /src/test/java/com/amazonaws/codedeploy/CodeDeployTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"). 5 | * You may not use this file except in compliance with the License. 6 | * A copy of the License is located at 7 | * 8 | * http://aws.amazon.com/apache2.0 9 | * 10 | * or in the "license" file accompanying this file. This file is distributed 11 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 | * express or implied. See the License for the specific language governing 13 | * permissions and limitations under the License. 14 | */ 15 | package com.amazonaws.codedeploy; 16 | 17 | import com.gargoylesoftware.htmlunit.WebAssert; 18 | import com.gargoylesoftware.htmlunit.html.HtmlPage; 19 | import org.jvnet.hudson.test.HudsonTestCase; 20 | 21 | public class CodeDeployTest extends HudsonTestCase { 22 | 23 | public void testConfig() throws Exception { 24 | HtmlPage page = new WebClient().goTo("configure"); 25 | WebAssert.assertTextPresent(page, "AWS CodeDeploy"); 26 | } 27 | } 28 | --------------------------------------------------------------------------------