├── .github └── workflows │ ├── gradle.yml │ └── release.yml ├── .gitignore ├── Readme.md ├── Release-Notes.md ├── build.gradle ├── gradle.properties ├── gradle ├── libs.versions.toml ├── publishing.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── renovate.json └── src ├── main ├── java │ └── com │ │ └── dtolabs │ │ └── rundeck │ │ └── plugin │ │ └── resources │ │ └── ec2 │ │ ├── EC2ResourceModelSource.java │ │ ├── EC2ResourceModelSourceFactory.java │ │ ├── EC2Supplier.java │ │ ├── EC2SupplierImpl.java │ │ ├── Ec2Instance.java │ │ └── InstanceToNodeMapper.java └── resources │ └── defaultMapping.properties └── test └── groovy └── com └── dtolabs └── rundeck └── plugin └── resources └── ec2 ├── EC2ResourceModelSourceSpec.groovy └── InstanceToNodeMapperSpec.groovy /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Java CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v4 12 | with: 13 | fetch-depth: 0 14 | - name: Get Fetch Tags 15 | run: git -c protocol.version=2 fetch --tags --progress --no-recurse-submodules origin 16 | if: "!contains(github.ref, 'refs/tags')" 17 | - name: Set up JDK 11 18 | uses: actions/setup-java@v4 19 | with: 20 | java-version: '11' 21 | distribution: 'zulu' 22 | - name: Grant execute permission for gradlew 23 | run: chmod +x gradlew 24 | - name: Build with Gradle 25 | run: ./gradlew build 26 | - name: Get Release Version 27 | id: get_version 28 | run: VERSION=$(./gradlew currentVersion -q -Prelease.quiet) && echo ::set-output name=VERSION::$VERSION 29 | - name: Upload sshj-plugin jar 30 | uses: actions/upload-artifact@v4 31 | with: 32 | # Artifact name 33 | name: Grails-Plugin-${{ steps.get_version.outputs.VERSION }} 34 | # Directory containing files to upload 35 | path: build/libs/rundeck-ec2-nodes-plugin-${{ steps.get_version.outputs.VERSION }}.jar 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | # Sequence of patterns matched against refs/tags 4 | tags: 5 | - 'v[0-9]+.[0-9]+.[0-9]+' # Push events to matching v*, i.e. v1.0, v20.15.10 6 | 7 | name: Upload Release Asset 8 | 9 | jobs: 10 | build: 11 | name: Upload Release Asset 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout code 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | - name: Set up JDK 11 19 | uses: actions/setup-java@v4 20 | with: 21 | java-version: '11' 22 | distribution: 'zulu' 23 | - name: Build with Gradle 24 | run: ./gradlew build 25 | - name: Get Release Version 26 | id: get_version 27 | run: VERSION=$(./gradlew currentVersion -q -Prelease.quiet) && echo "VERSION=$VERSION" >> $GITHUB_OUTPUT 28 | - name: Create Release 29 | id: create_release 30 | run: | 31 | gh release create \ 32 | --generate-notes \ 33 | --title 'Release ${{ steps.get_version.outputs.VERSION }}' \ 34 | ${{ github.ref_name }} \ 35 | build/libs/rundeck-ec2-nodes-plugin-${{ steps.get_version.outputs.VERSION }}.jar 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | - name: Publish to Maven Central 39 | run: ./gradlew -PsigningKey=${SIGNING_KEY_B64} -PsigningPassword=${SIGNING_PASSWORD} -PsonatypeUsername=${SONATYPE_USERNAME} -PsonatypePassword=${SONATYPE_PASSWORD} publishToSonatype closeAndReleaseSonatypeStagingRepository 40 | env: 41 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 42 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 43 | SIGNING_KEY_B64: ${{ secrets.SIGNING_KEY_B64 }} 44 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE/Build files 2 | .gradle/ 3 | *.ipr 4 | *.iml 5 | *.iws 6 | .idea/ 7 | build/ 8 | 9 | # System files 10 | **/.DS_Store 11 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | Rundeck EC2 Nodes Plugin 2 | ======================== 3 | 4 | 5 | ![Build Status](https://github.com/rundeck-plugins/rundeck-ec2-nodes-plugin/actions/workflows/gradle.yml/badge.svg) 6 | 7 | This is a Resource Model Source plugin for [Rundeck][] that provides 8 | Amazon EC2 Instances as nodes for the Rundeck server. 9 | 10 | [Rundeck]: http://rundeck.org 11 | 12 | 13 | Installation 14 | ------------ 15 | 16 | Download from the [releases page](https://github.com/rundeck-plugins/rundeck-ec2-nodes-plugin/releases). 17 | 18 | Put the `rundeck-ec2-nodes-plugin-1.5.x.jar` into your `$RDECK_BASE/libext` dir. 19 | 20 | Release Notes 21 | ------------- 22 | 23 | See changes under Github [Releases](https://github.com/rundeck-plugins/rundeck-ec2-nodes-plugin/releases) 24 | 25 | ([Older Release Notes](https://github.com/rundeck-plugins/rundeck-ec2-nodes-plugin/blob/main/Release-Notes.md)) 26 | 27 | Usage 28 | ----- 29 | 30 | See: [Rundeck Docs > User Guide > Node Sources Overview](https://docs.rundeck.com/docs/manual/projects/resource-model-sources/) 31 | 32 | The provider name is: `aws-ec2` 33 | 34 | Here are the configuration properties: 35 | 36 | * `accessKey`: API AccessKey value (if not using IAM profile) 37 | * `secretKey`: API SecretKey value (if not using IAM profile) 38 | * `endpoint` - The URL of the AWS **endpoint** to use, or blank for the default endpoint. Can be a comma-separated list of endpoints (e.g. `https://ec2.us-west-1.amazonaws.com, https://ec2.us-east-1.amazonaws.com`) to integrate with multiple regions. See [Amazon EC2 Regions and Endpoints](http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region) for list of endpoints. 39 | * `synchronousLoad`: Do not use internal async loading behavior. (boolean, default: true) 40 | * `refreshInterval`: Unless using Synchronous Loading, time in seconds used as minimum interval between calls to the AWS API. (default 30) 41 | * `filter` A set of ";" separated query filters ("$Name=$Value") for the AWS EC2 API, see below. 42 | * `runningOnly`: if "true", automatically filter the * instances by "instance-state-name=running" 43 | * `useDefaultMapping`: if "true", base all mapping definitions off the default mapping provided. 44 | * `mappingParams`: A set of ";" separated mapping entries 45 | * `mappingFile`: Path to a java properties-formatted mapping definition file. 46 | * `pageResults`: Max elements per page for AWS API calls - REQUIRED 47 | 48 | If you leave `accessKey` and `secretKey` blank, the EC2 IAM profile will be used. 49 | 50 | Note: Rundeck 2.6.3+ uses an asynchronous nodes cache by 51 | default. You should enable `synchronousLoad` if you are using the 52 | rundeck nodes cache, or set the `refreshInterval` to 0. 53 | 54 | ## Filter definition 55 | 56 | The syntax for defining filters uses `$Name=$Value1,$Value2[;$Name=$value[;...]]` 57 | for any of the allowed filter names (see [DescribeInstances][1] for the available filter Names). 58 | 59 | *Note*: you do not need to specify `Filter.1.Name=$Name`, etc. as described in the EC2 API documentation, 60 | this will handled for you. Simply list the Name = Value pairs, separated by `;`. 61 | 62 | You can specify multiple values in the filter by separating the values with `,`. 63 | 64 | [1]: http://docs.amazonwebservices.com/AWSEC2/latest/APIReference/ApiReference-query-DescribeInstances.html 65 | 66 | Example: to filter based on a Tag named "MyTag" with a value of "Some Tag Value": 67 | 68 | tag:MyTag=Some Tag Value 69 | 70 | Example: to filter *any* instance with a Tag named `MyTag`: 71 | 72 | tag-key=MyTag 73 | 74 | Example combining matching a tag value and the instance type: 75 | 76 | tag:MyTag=Some Tag Value;instance-type=m1.small 77 | 78 | Example including two instance types, the results will have one or the other instance-type: 79 | 80 | instance-type=m1.small,m1.large 81 | 82 | Mapping Definition 83 | ---------- 84 | 85 | Rundeck Node attributes are configured by mapping EC2 Instance properties via a 86 | mapping configuration. 87 | 88 | The mapping declares the node attributes that will be set, and what their values 89 | will be set to using a "selector" on properties of the EC2 Instance object. 90 | 91 | Here is the default mapping: 92 | 93 | description.default=EC2 node instance 94 | editUrl.default=https://console.aws.amazon.com/ec2/home#s=Instances&selectInstance=${node.instanceId} 95 | hostname.selector=publicDnsName,privateIpAddress 96 | sshport.default=22 97 | sshport.selector=tags/ssh_config_Port 98 | instanceId.selector=instanceId 99 | nodename.selector=tags/Name,instanceId 100 | osArch.selector=architecture 101 | osFamily.default=unix 102 | osFamily.selector=platform 103 | osName.default=Linux 104 | osName.selector=platform 105 | privateDnsName.selector=privateDnsName 106 | privateIpAddress.selector=privateIpAddress 107 | state.selector=state.name 108 | tag.pending.selector=state.name=pending 109 | tag.running.selector=state.name=running 110 | tag.shutting-down.selector=state.name=shutting-down 111 | tag.stopped.selector=state.name=stopped 112 | tag.stopping.selector=state.name=stopping 113 | tag.terminated.selector=state.name=terminated 114 | tags.default=ec2 115 | tags.selector=tags/Rundeck-Tags 116 | username.default=ec2-user 117 | username.selector=tags/Rundeck-User 118 | 119 | Configuring the Mapping 120 | ----------------------- 121 | 122 | You can configure your source to start with the above default mapping with the 123 | `useDefaultMapping` property. 124 | 125 | You can then selectively change it either by setting the `mappingParams` or 126 | pointing to a new properties file with `mappingFile`. 127 | 128 | For example, you can put this in the `mappingParams` field in the GUI to change 129 | the default tags for your nodes, remove the "stopping" tag selector, and add a 130 | new "ami_id" selector: 131 | 132 | tags.default=mytag, mytag2;tag.stopping.selector=;ami_id.selector=imageId 133 | 134 | Mapping format 135 | --------------- 136 | 137 | The mapping consists of defining either a selector or a default for 138 | the desired Node fields. The "nodename" field is required, and will 139 | automatically be set to the instance ID if no other value is defined. 140 | 141 | For purposes of the mapping definition, a `field selector` is either: 142 | 143 | * An EC2 fieldname, or dot-separated field names 144 | * "tags/" followed by a Tag name, e.g. "tags/My Tag" 145 | * "tags/*" for use by the `attributes.selector` mapping 146 | 147 | Selectors use the Apache [BeanUtils](http://commons.apache.org/beanutils/) to extract a property value from the AWS API 148 | [Instance class](http://docs.amazonwebservices.com/AWSJavaSDK/latest/javadoc/com/amazonaws/services/ec2/model/Instance.html). 149 | This means you can use dot-separated fieldnames to traverse the object graph. 150 | E.g. "state.name" to specify the "name" field of the State property of the Instance. 151 | 152 | format: 153 | 154 | # define a selector for "property": 155 | .selector= 156 | # define a default value for "property": 157 | .default= 158 | # Special attributes selector to map all Tags to attributes 159 | attributes.selector=tags/* 160 | # The value for the tags selector will be treated as a comma-separated list of strings 161 | tags.selector= 162 | # the default tags list 163 | tags.default=a,b,c 164 | # Define a single tag which will be set if and only if the selector result is not empty 165 | tag..selector= 166 | # Define a single tag which will be set if the selector result equals the 167 | tag..selector== 168 | 169 | Note, a ".selector" value can have multiple selectors defined, separated by commas, 170 | and they will be evaluated in order with the first value available being used. E.g. "nodename.selector=tags/Name,instanceId", which will look for a tag named "Name", otherwise use the instanceId. 171 | 172 | You can also use the `=` feature to set a tag only if the field selector has a certain value. 173 | 174 | ### Tags selector 175 | 176 | When defining field selector for the `tags` node property, the string value selected (if any) will 177 | be treated as a comma-separated list of strings to use as node tags. You could, for example, set a custom EC2 Tag on 178 | an instance to contain this list of tags, in this example from the simplemapping.properties file: 179 | 180 | tags.selector=tags/Rundeck-Tags 181 | 182 | So creating the "Rundeck-Tags" Tag on the EC2 Instance with a value of "alpha, beta" will result in the node having 183 | those two node tags. 184 | 185 | The tags.selector also supports a "merge" ability, so you can merge multiple Instance Tags into the Rundeck tags by separating multiple selectors with a "|" character: 186 | 187 | tags.selector=tags/Environment|tags/Role 188 | 189 | ### Appending values 190 | 191 | A field selector can conjoin multiple values using `+`, and can append literal text like the `_` character for example. 192 | 193 | # conjoin two fields with no separation between the values 194 | # this will result in "field1field2" 195 | .selector=+ 196 | 197 | # conjoin multiple fields with a literal string delimiter 198 | # this will result in "field1-*-field2" 199 | .selector=+"-*-"+ 200 | 201 | Use a quoted value to insert a delimiter, with either single or double quotes. 202 | 203 | Here is an example to use the "Name" instance tag, and InstanceId, to generate 204 | a unique node name for rundeck: 205 | 206 | nodename.selector=tags/Name+'-'+instanceId 207 | 208 | 209 | Mapping EC2 Instances to Rundeck Nodes 210 | ================= 211 | 212 | Rundeck node definitions specify mainly the pertinent data for connecting to and organizing the Nodes. EC2 Instances have metadata that can be mapped onto the fields used for Rundeck Nodes. 213 | 214 | Rundeck nodes have the following metadata fields: 215 | 216 | * `nodename` - unique identifier 217 | * `hostname` - IP address/hostname to connect to the node 218 | * `sshport` - The ssh port, if resolved to another port than 22 hostname will be set to ``:`` 219 | * `username` - SSH username to connect to the node 220 | * `description` - textual description 221 | * `osName` - OS name 222 | * `osFamily` - OS family: unix, windows, cygwin. 223 | * `osArch` - OS architecture 224 | * `osVersion` - OS version 225 | * `tags` - set of labels for organization 226 | * `editUrl` - URL to edit the definition of this node object 227 | * `remoteUrl` - URL to edit the definition of this node object using Rundeck-specific integration 228 | 229 | In addition, Nodes can have arbitrary attribute values. 230 | 231 | EC2 Instance Field Selectors 232 | ----------------- 233 | 234 | EC2 Instances have a set of metadata that can be mapped to any of the Rundeck node fields, or to Settings or tags for the node. 235 | 236 | EC2 fields: 237 | 238 | * amiLaunchIndex 239 | * architecture 240 | * clientToken 241 | * imageId 242 | * imageName 243 | * instanceId 244 | * instanceLifecycle 245 | * instanceType 246 | * kernelId 247 | * keyName 248 | * launchTime 249 | * license 250 | * platform 251 | * privateDnsName 252 | * privateIpAddress 253 | * publicDnsName 254 | * publicIpAddress 255 | * ramdiskId 256 | * rootDeviceName 257 | * rootDeviceType 258 | * spotInstanceRequestId 259 | * state 260 | * stateReason 261 | * stateTransitionReason 262 | * subnetId 263 | * virtualizationType 264 | * vpcId 265 | * `tags/*` 266 | 267 | EC2 Instances can also have "Tags" which are key/value pairs attached to the Instance. A common Tag is "Name" which could be a unique identifier for the Instance, making it a useful mapping to the Node's name field. Note that EC2 Tags differ from Rundeck Node tags: Rundeck tags are simple string labels and are not key/value pairs. 268 | 269 | Authenticating to EC2 Nodes with Rundeck 270 | ----------- 271 | 272 | Once you get your EC2 Instances listed in Rundeck as Nodes, you may be wondering "Now how do I use this?" 273 | 274 | Rundeck uses SSH by default with private key authentication, so in order to connect to your EC2 instances out 275 | of the box you will need to configure Rundeck to use the right private SSH key to connect to your nodes, 276 | which can be done in either of a few ways: 277 | 278 | 1. Copy your private key to the default location used by Rundeck which is `~/.ssh/id_rsa` 279 | 2. Copy your private key elsewhere, and override it on a project level. Change project.properties and set the `project.ssh-keypath` to point to the file. 280 | 3. Copy your private key elsewhere, and set the location as an attribute on your nodes (shown below) 281 | 282 | To set the ssh keypath attribute on the EC2 Nodes produced by the plugin, you can modify your mapping configuration. 283 | 284 | E.g. in the "Mapping Params" field, set: 285 | 286 | `Mapping Params: ssh-keypath.default=/path/to/key` 287 | 288 | This will set the "ssh-keypath" attribute on your EC2 Nodes, allowing correct private key ssh authentication. 289 | 290 | The default mapping also configures a default `username` attribute to be `ec2-user`, but if you want to change the default set: 291 | 292 | `Mapping Params: ssh-keypath.default=/path/to/key;username.default=my-username` 293 | 294 | 295 | # How to 296 | 297 | ## Build 298 | 299 | Build the project with Gradle 300 | 301 | ./gradlew build 302 | 303 | ## Test 304 | 305 | Test the project with Gradle 306 | 307 | ./gradlew check 308 | 309 | ## Release 310 | 311 | Release the project. 312 | 313 | ./gradlew release 314 | 315 | ## Version 316 | 317 | Get current version from axion-release plugin 318 | 319 | ./gradlew currentVersion 320 | 321 | ## increment minor version 322 | 323 | Bump minor version 324 | 325 | ./gradlew markNextVersion -Prelease.incrementer=incrementMinor 326 | 327 | ## increment major version 328 | 329 | Bump major version 330 | 331 | ./gradlew markNextVersion -Prelease.incrementer=incrementMajor 332 | -------------------------------------------------------------------------------- /Release-Notes.md: -------------------------------------------------------------------------------- 1 | Release Notes 2 | ========= 3 | 4 | 1.5.6 5 | ----- 6 | 7 | Date: 2018-08-15 8 | 9 | Changes: 10 | 11 | * Support multiple values in aws filter: [#80](https://github.com/rundeck-plugins/rundeck-ec2-nodes-plugin/pull/80) 12 | * use "FilterName=Value1,Value2;FilterName2=Value1,Value2" etc 13 | * Fixed [#56](https://github.com/rundeck-plugins/rundeck-ec2-nodes-plugin/issues/56) missing clientConfiguration from STS api call 14 | 15 | 1.5.5 16 | ----- 17 | 18 | Date: 2017-06-19 19 | 20 | Changegs: 21 | 22 | * Fix [#67](https://github.com/rundeck-plugins/rundeck-ec2-nodes-plugin/issues/67) Update aws-sdk to 1.11.148 23 | 24 | 1.5.4 25 | ----- 26 | 27 | Date: 2017-02-23 28 | 29 | Changes: 30 | 31 | * Fix [#61](https://github.com/rundeck-plugins/rundeck-ec2-nodes-plugin/issues/61) add synchronousLoad to disable asynchronous behavior 32 | 33 | 1.5.3 34 | ----- 35 | 36 | Date: 2016-12-16 37 | 38 | Changes: 39 | 40 | * fix [#44](https://github.com/rundeck-plugins/rundeck-ec2-nodes-plugin/issues/44) default hostname selector should fallback to privateIpAddress 41 | * fix [#10](https://github.com/rundeck-plugins/rundeck-ec2-nodes-plugin/issues/10) add conjoined selector values using `+` and quoted literals 42 | 43 | 1.5.2 44 | --- 45 | 46 | Date: 2016-03-04 47 | 48 | Changes: 49 | 50 | * fix metadata for plugin file version [#38](https://github.com/rundeck-plugins/rundeck-ec2-nodes-plugin/issues/38) 51 | * add support for assumeRole for IAM profile [#40](https://github.com/rundeck-plugins/rundeck-ec2-nodes-plugin/pull/40) 52 | 53 | 54 | 1.5.1 55 | --- 56 | 57 | Date: 10/15/2015 58 | 59 | Changes: 60 | 61 | * add support for setting ssh port [#33](https://github.com/rundeck-plugins/rundeck-ec2-nodes-plugin/pull/33) 62 | 63 | 1.5 64 | --- 65 | 66 | Date: 12/10/2014 67 | 68 | Changes: 69 | 70 | * Fix issue of adding http proxy support [Issue #14](https://github.com/rundeck-plugins/rundeck-ec2-nodes-plugin/issues/14) 71 | * Make AWS access key and secret optional, and use IAM profile instead [#20](https://github.com/rundeck-plugins/rundeck-ec2-nodes-plugin/issues/20) 72 | * Edit link for nodes updated for new AWS console URL scheme 73 | * Use Password field input for AWS SecretKey and Proxy Password 74 | * Fix use of "|" in `tags.selector` mapping [#18](https://github.com/rundeck-plugins/rundeck-ec2-nodes-plugin/issues/18) 75 | * Fix: Errant whitespace in project.properties can cause AWS auth to fail [#13](https://github.com/rundeck-plugins/rundeck-ec2-nodes-plugin/issues/13) 76 | 77 | 1.4 78 | --- 79 | 80 | Date: 1/24/2014 81 | 82 | Changes: 83 | 84 | * Fix log4j problem with Rundeck 1.6 85 | * works with Rundeck 1.6/2.x 86 | 87 | 1.3 88 | --- 89 | 90 | Date: 2/22/2013 91 | 92 | Changes: 93 | 94 | * Update to work with Rundeck 1.5 95 | * Fix incorrect description of Mapping Params config property 96 | 97 | 1.2 98 | --- 99 | 100 | Date: 11/16/2011 101 | 102 | Changes: 103 | 104 | * Added "merge" feature to `tags.selector` mapping 105 | 106 | 1.1 107 | --- 108 | 109 | Date: 10/5/2011 110 | 111 | Changes: 112 | 113 | * Fix issue executing on nodes [Issue #1](https://github.com/gschueler/rundeck-ec2-nodes-plugin/issues/1) 114 | 115 | 1.0 116 | --- 117 | 118 | Initial release 119 | 120 | Date: 9/9/2011 121 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | mavenCentral() 4 | } 5 | } 6 | 7 | plugins { 8 | id 'java' 9 | id 'groovy' 10 | id 'idea' 11 | alias(libs.plugins.axionRelease) 12 | alias(libs.plugins.nexusPublish) 13 | } 14 | 15 | group = 'org.rundeck.plugins' 16 | ext.publishName = "EC2 Nodes Plugin ${project.version}" 17 | ext.publishDescription = project.description ?: 'Produces Rundeck Nodes from AWS EC2' 18 | ext.githubSlug = 'rundeck-plugins/rundeck-ec2-nodes-plugin' 19 | ext.developers = [ 20 | [id: 'gschueler', name: 'Greg Schueler', email: 'greg@rundeck.com'] 21 | ] 22 | 23 | java { 24 | sourceCompatibility = JavaVersion.VERSION_11 25 | withJavadocJar() 26 | withSourcesJar() 27 | } 28 | allprojects { 29 | gradle.projectsEvaluated { 30 | tasks.withType(JavaCompile) { 31 | options.compilerArgs.add("-Xlint:deprecation") 32 | } 33 | } 34 | } 35 | defaultTasks 'clean', 'build' 36 | ext.rundeckPluginVersion = '1.1' 37 | scmVersion { 38 | ignoreUncommittedChanges = false 39 | tag { 40 | prefix = 'v' 41 | versionSeparator = '' 42 | } 43 | } 44 | project.version = scmVersion.version 45 | 46 | configurations{ 47 | //declare custom pluginLibs configuration to include only libs for this plugin 48 | pluginLibs { 49 | } 50 | 51 | //declare compile to extend from pluginLibs so it inherits the dependencies 52 | implementation { 53 | extendsFrom pluginLibs 54 | } 55 | } 56 | repositories { 57 | mavenCentral() 58 | mavenLocal() 59 | } 60 | dependencies { 61 | implementation libs.slf4jApi 62 | implementation(libs.rundeckCore) { 63 | exclude group: "com.google.guava" 64 | } 65 | implementation libs.bundles.awsSdk 66 | implementation libs.jacksonDatabind 67 | implementation libs.commonsBeanutils 68 | 69 | pluginLibs(libs.awsSdkEc2) { 70 | exclude group: "org.apache.httpcomponents", module: "httpclient" 71 | exclude group: "com.fasterxml.jackson.core" 72 | exclude group: "com.fasterxml.jackson.dataformat" 73 | } 74 | pluginLibs(libs.awsSdkSts) { 75 | exclude group: "org.apache.httpcomponents", module: "httpclient" 76 | exclude group: "com.fasterxml.jackson.core" 77 | exclude group: "com.fasterxml.jackson.dataformat" 78 | } 79 | 80 | testImplementation libs.bundles.testLibs 81 | } 82 | 83 | // task to copy plugin libs to output/lib dir 84 | task copyToLib(type: Copy) { 85 | into "$buildDir/output/lib" 86 | from configurations.pluginLibs 87 | } 88 | 89 | 90 | jar { 91 | //include contents of output dir 92 | from "$buildDir/output" 93 | manifest { 94 | attributes 'Rundeck-Plugin-Name': 'EC2 Nodes Plugin' 95 | attributes 'Rundeck-Plugin-Description': 'Resource Model Source plugin that provides Amazon EC2 Instances as nodes.' 96 | attributes 'Rundeck-Plugin-Rundeck-Compatibility-Version': '2.2.2+' 97 | attributes 'Rundeck-Plugin-Tags': 'java,aws,ec2,resource model' 98 | attributes 'Rundeck-Plugin-License': 'Apache 2.0' 99 | attributes 'Rundeck-Plugin-Author': 'Rundeck, Inc.' 100 | attributes 'Rundeck-Plugin-Source-Link': 'https://github.com/rundeck-plugins/rundeck-ec2-nodes-plugin' 101 | attributes 'Rundeck-Plugin-Version': rundeckPluginVersion, 'Rundeck-Plugin-Archive': 'true', 'Rundeck-Plugin-Libs-Load-First':'false' 102 | //create space-separated list of pluginLibs 103 | def libList = configurations.pluginLibs.collect{'lib/'+it.name}.join(' ') 104 | attributes 'Rundeck-Plugin-Classnames': 'com.dtolabs.rundeck.plugin.resources.ec2.EC2ResourceModelSourceFactory', 'Rundeck-Plugin-Libs': "${libList}" 105 | attributes 'Rundeck-Plugin-File-Version': version 106 | } 107 | } 108 | 109 | test { 110 | useJUnitPlatform() 111 | } 112 | //set jar task to depend on copyToLib 113 | jar.dependsOn(copyToLib) 114 | 115 | nexusPublishing { 116 | packageGroup = 'org.rundeck.plugins' 117 | repositories { 118 | sonatype() 119 | } 120 | } 121 | 122 | apply from: "${rootDir}/gradle/publishing.gradle" -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rundeck-plugins/rundeck-ec2-nodes-plugin/b1dfa939fcf05259e04c96e8abe62e4efe771e9f/gradle.properties -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | axionRelease = "1.18.17" 3 | awsSdk = "1.12.780" 4 | cglib = "3.3.0" 5 | commonsBeanutils = "1.10.1" 6 | groovy = "3.0.23" 7 | jacksonDatabind = "2.18.2" 8 | nexusPublish = "2.0.0" 9 | objenesis = "3.4" 10 | rundeckCore = "5.8.0-20241205" 11 | slf4j = "1.7.36" 12 | spock = "2.3-groovy-3.0" 13 | 14 | [libraries] 15 | slf4jApi = { group = "org.slf4j", name = "slf4j-api", version.ref = "slf4j" } 16 | rundeckCore = { group = "org.rundeck", name = "rundeck-core", version.ref = "rundeckCore" } 17 | awsSdkCore = { group = "com.amazonaws", name = "aws-java-sdk-core", version.ref = "awsSdk" } 18 | awsSdkSts = { group = "com.amazonaws", name = "aws-java-sdk-sts", version.ref = "awsSdk" } 19 | jacksonDatabind = { group = "com.fasterxml.jackson.core", name = "jackson-databind", version.ref = "jacksonDatabind" } 20 | commonsBeanutils = { group = "commons-beanutils", name = "commons-beanutils", version.ref = "commonsBeanutils" } 21 | awsSdkEc2 = { group = "com.amazonaws", name = "aws-java-sdk-ec2", version.ref = "awsSdk" } 22 | groovyAll = { group = "org.codehaus.groovy", name = "groovy-all", version.ref = "groovy" } 23 | spockCore = { group = "org.spockframework", name = "spock-core", version.ref = "spock" } 24 | cglibNodep = { group = "cglib", name = "cglib-nodep", version.ref = "cglib" } 25 | objenesis = { group = "org.objenesis", name = "objenesis", version.ref = "objenesis" } 26 | 27 | [bundles] 28 | awsSdk = ["awsSdkCore", "awsSdkSts"] 29 | testLibs = ["groovyAll", "spockCore", "cglibNodep", "objenesis"] 30 | 31 | [plugins] 32 | axionRelease = { id = "pl.allegro.tech.build.axion-release", version.ref = "axionRelease" } 33 | nexusPublish = { id = "io.github.gradle-nexus.publish-plugin", version.ref = "nexusPublish" } 34 | -------------------------------------------------------------------------------- /gradle/publishing.gradle: -------------------------------------------------------------------------------- 1 | /** 2 | * Define project extension values in the project gradle file before including this file: 3 | * 4 | * publishName = 'Name of Package' 5 | * publishDescription = 'description' (optional) 6 | * githubSlug = Github slug e.g. 'rundeck/rundeck-cli' 7 | * developers = [ [id:'id', name:'name', email: 'email' ] ] list of developers 8 | * 9 | * Define project properties to sign and publish when invoking publish task: 10 | * 11 | * ./gradlew \ 12 | * -PsigningKey="base64 encoded gpg key" \ 13 | * -PsigningPassword="password for key" \ 14 | * -PsonatypeUsername="sonatype token user" \ 15 | * -PsonatypePassword="sonatype token password" \ 16 | * publishToSonatype closeAndReleaseSonatypeStagingRepository 17 | */ 18 | apply plugin: 'maven-publish' 19 | apply plugin: 'signing' 20 | 21 | publishing { 22 | publications { 23 | "${project.name}"(MavenPublication) { publication -> 24 | from components.java 25 | 26 | pom { 27 | name = publishName 28 | description = project.ext.hasProperty('publishDescription') ? project.ext.publishDescription : 29 | project.description ?: publishName 30 | url = "https://github.com/${githubSlug}" 31 | licenses { 32 | license { 33 | name = 'The Apache Software License, Version 2.0' 34 | url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 35 | distribution = 'repo' 36 | } 37 | } 38 | scm { 39 | url = "https://github.com/${githubSlug}" 40 | connection = "scm:git:git@github.com/${githubSlug}.git" 41 | developerConnection = "scm:git:git@github.com:${githubSlug}.git" 42 | } 43 | if (project.ext.developers) { 44 | developers { 45 | project.ext.developers.each { dev -> 46 | developer { 47 | id = dev.id 48 | name = dev.name 49 | email = dev.email 50 | } 51 | } 52 | } 53 | } 54 | } 55 | 56 | } 57 | } 58 | } 59 | def base64Decode = { String prop -> 60 | project.findProperty(prop) ? 61 | new String(Base64.getDecoder().decode(project.findProperty(prop).toString())).trim() : 62 | null 63 | } 64 | 65 | if (project.hasProperty('signingKey') && project.hasProperty('signingPassword')) { 66 | signing { 67 | useInMemoryPgpKeys(base64Decode("signingKey"), project.signingPassword) 68 | sign(publishing.publications) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rundeck-plugins/rundeck-ec2-nodes-plugin/b1dfa939fcf05259e04c96e8abe62e4efe771e9f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | org.gradle.wrapper.GradleWrapperMain \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 DTO Solutions, Inc. (http://dtosolutions.com) 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 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * EC2ResourceModelSource.java 19 | * 20 | * User: Greg Schueler greg@dtosolutions.com 21 | * Created: 9/1/11 4:34 PM 22 | * 23 | */ 24 | package com.dtolabs.rundeck.plugin.resources.ec2; 25 | 26 | import com.amazonaws.ClientConfiguration; 27 | import com.amazonaws.auth.*; 28 | import com.amazonaws.regions.RegionUtils; 29 | import com.amazonaws.regions.Regions; 30 | import com.amazonaws.services.securitytoken.AWSSecurityTokenService; 31 | import com.amazonaws.services.securitytoken.AWSSecurityTokenServiceClientBuilder; 32 | import com.amazonaws.services.securitytoken.model.AssumeRoleRequest; 33 | import com.amazonaws.services.securitytoken.model.AssumeRoleResult; 34 | import com.amazonaws.services.securitytoken.model.Credentials; 35 | import com.dtolabs.rundeck.core.common.INodeSet; 36 | import com.dtolabs.rundeck.core.plugins.configuration.ConfigurationException; 37 | import com.dtolabs.rundeck.core.resources.ResourceModelSource; 38 | import com.dtolabs.rundeck.core.resources.ResourceModelSourceException; 39 | import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree; 40 | import org.rundeck.app.spi.Services; 41 | import org.rundeck.storage.api.PathUtil; 42 | import org.rundeck.storage.api.StorageException; 43 | import org.slf4j.Logger; 44 | import org.slf4j.LoggerFactory; 45 | 46 | import java.io.*; 47 | import java.util.ArrayList; 48 | import java.util.Collections; 49 | import java.util.Properties; 50 | import java.util.concurrent.ExecutionException; 51 | import java.util.concurrent.ExecutorService; 52 | import java.util.concurrent.Executors; 53 | import java.util.concurrent.Future; 54 | 55 | import static com.dtolabs.rundeck.plugin.resources.ec2.EC2ResourceModelSourceFactory.SYNCHRONOUS_LOAD; 56 | 57 | /** 58 | *

59 | * EC2ResourceModelSource produces nodes by querying the AWS EC2 API to list instances. 60 | *

61 | *

62 | * The RunDeck node definitions are created from the instances on a mapping system to convert properties of the amazon 63 | * instances to attributes defined on the nodes. 64 | *

65 | *

66 | * The EC2 requests are performed asynchronously, so the first request to {@link #getNodes()} will return null, and 67 | * subsequent requests may return the data when it's available. 68 | *

69 | * @author Greg Schueler greg@rundeck.com 70 | */ 71 | public class EC2ResourceModelSource implements ResourceModelSource { 72 | static Logger logger = LoggerFactory.getLogger(EC2ResourceModelSource.class); 73 | private String accessKey; 74 | private String secretKey; 75 | private String secretKeyStoragePath; 76 | long refreshInterval = 30000; 77 | long lastRefresh = 0; 78 | String filterParams; 79 | String endpoint; 80 | String httpProxyHost; 81 | int httpProxyPort = 80; 82 | String httpProxyUser; 83 | String httpProxyPass; 84 | String region; 85 | String mappingParams; 86 | File mappingFile; 87 | Services services; 88 | boolean useDefaultMapping = true; 89 | boolean runningOnly = false; 90 | boolean queryAsync = true; 91 | boolean queryNodeInstancesInParallel = false; 92 | Future futureResult = null; 93 | final Properties mapping = new Properties(); 94 | final String assumeRoleArn; 95 | final String assumeRoleArnCombinedWithExtId; 96 | AWSCredentialsProvider awsCredentialsProvider; 97 | 98 | final String externalId; 99 | int pageResults; 100 | 101 | ClientConfiguration clientConfiguration = new ClientConfiguration(); 102 | 103 | INodeSet iNodeSet; 104 | static final Properties defaultMapping = new Properties(); 105 | InstanceToNodeMapper mapper; 106 | 107 | ExecutorService executor = Executors.newFixedThreadPool(1); 108 | 109 | static { 110 | final String mapping = "nodename.selector=tags/Name,instanceId\n" 111 | + "hostname.selector=publicDnsName,privateIpAddress\n" 112 | + "sshport.default=22\n" 113 | + "sshport.selector=tags/ssh_config_Port\n" 114 | + "description.default=EC2 node instance\n" 115 | + "osArch.selector=architecture\n" 116 | + "osFamily.selector=platform\n" 117 | + "osFamily.default=unix\n" 118 | + "osName.selector=platform\n" 119 | + "osName.default=Linux\n" 120 | + "username.selector=tags/Rundeck-User\n" 121 | + "username.default=ec2-user\n" 122 | + "editUrl.default=https://console.aws.amazon.com/ec2/home#Instances:search=${node.instanceId}\n" 123 | + "privateIpAddress.selector=privateIpAddress\n" 124 | + "privateDnsName.selector=privateDnsName\n" 125 | + "tags.selector=tags/Rundeck-Tags\n" 126 | + "instanceId.selector=instanceId\n" 127 | + "tag.running.selector=state.name=running\n" 128 | + "tag.stopped.selector=state.name=stopped\n" 129 | + "tag.stopping.selector=state.name=stopping\n" 130 | + "tag.shutting-down.selector=state.name=shutting-down\n" 131 | + "tag.terminated.selector=state.name=terminated\n" 132 | + "tag.pending.selector=state.name=pending\n" 133 | + "state.selector=state.name\n" 134 | + "region.selector=region\n" 135 | + "tags.default=ec2\n"; 136 | try { 137 | 138 | final InputStream resourceAsStream = EC2ResourceModelSource.class.getClassLoader().getResourceAsStream( 139 | "defaultMapping.properties"); 140 | if (null != resourceAsStream) { 141 | try { 142 | defaultMapping.load(resourceAsStream); 143 | } finally { 144 | resourceAsStream.close(); 145 | } 146 | }else{ 147 | //fallback in case class loader is misbehaving 148 | final StringReader stringReader = new StringReader(mapping); 149 | try { 150 | defaultMapping.load(stringReader); 151 | } finally { 152 | stringReader.close(); 153 | } 154 | } 155 | 156 | } catch (IOException e) { 157 | e.printStackTrace(System.err); 158 | } 159 | } 160 | 161 | public EC2ResourceModelSource(final Properties configuration, final Services services) { 162 | this.services = services; 163 | this.accessKey = configuration.getProperty(EC2ResourceModelSourceFactory.ACCESS_KEY); 164 | this.secretKey = configuration.getProperty(EC2ResourceModelSourceFactory.SECRET_KEY); 165 | this.region = configuration.getProperty(EC2ResourceModelSourceFactory.REGION); 166 | this.secretKeyStoragePath = configuration.getProperty(EC2ResourceModelSourceFactory.SECRET_KEY_STORAGE_PATH); 167 | this.endpoint = configuration.getProperty(EC2ResourceModelSourceFactory.ENDPOINT); 168 | this.pageResults = Integer.parseInt(configuration.getProperty(EC2ResourceModelSourceFactory.MAX_RESULTS)); 169 | this.httpProxyHost = configuration.getProperty(EC2ResourceModelSourceFactory.HTTP_PROXY_HOST); 170 | this.assumeRoleArn = configuration.getProperty(EC2ResourceModelSourceFactory.ROLE_ARN); 171 | this.assumeRoleArnCombinedWithExtId = configuration.getProperty(EC2ResourceModelSourceFactory.ROLE_ARN_COMBINED_WITH_EXT_ID); 172 | this.externalId = configuration.getProperty(EC2ResourceModelSourceFactory.EXTERNAL_ID); 173 | int proxyPort = 80; 174 | 175 | final String proxyPortStr = configuration.getProperty(EC2ResourceModelSourceFactory.HTTP_PROXY_PORT); 176 | if (null != proxyPortStr && !"".equals(proxyPortStr)) { 177 | try { 178 | proxyPort = Integer.parseInt(proxyPortStr); 179 | } catch (NumberFormatException e) { 180 | logger.warn(EC2ResourceModelSourceFactory.HTTP_PROXY_PORT + " value is not valid: " + proxyPortStr); 181 | } 182 | } 183 | this.httpProxyPort = proxyPort; 184 | this.httpProxyUser = configuration.getProperty(EC2ResourceModelSourceFactory.HTTP_PROXY_USER); 185 | this.httpProxyPass = configuration.getProperty(EC2ResourceModelSourceFactory.HTTP_PROXY_PASS); 186 | 187 | this.filterParams = configuration.getProperty(EC2ResourceModelSourceFactory.FILTER_PARAMS); 188 | this.mappingParams = configuration.getProperty(EC2ResourceModelSourceFactory.MAPPING_PARAMS); 189 | final String mappingFilePath = configuration.getProperty(EC2ResourceModelSourceFactory.MAPPING_FILE); 190 | if (null != mappingFilePath) { 191 | mappingFile = new File(mappingFilePath); 192 | } 193 | int refreshSecs = 30; 194 | final String refreshStr = configuration.getProperty(EC2ResourceModelSourceFactory.REFRESH_INTERVAL); 195 | if (null != refreshStr && !"".equals(refreshStr)) { 196 | try { 197 | refreshSecs = Integer.parseInt(refreshStr); 198 | } catch (NumberFormatException e) { 199 | logger.warn(EC2ResourceModelSourceFactory.REFRESH_INTERVAL + " value is not valid: " + refreshStr); 200 | } 201 | } 202 | refreshInterval = refreshSecs * 1000; 203 | if (configuration.containsKey(EC2ResourceModelSourceFactory.USE_DEFAULT_MAPPING)) { 204 | useDefaultMapping = Boolean.parseBoolean(configuration.getProperty( 205 | EC2ResourceModelSourceFactory.USE_DEFAULT_MAPPING)); 206 | } 207 | if (configuration.containsKey(EC2ResourceModelSourceFactory.RUNNING_ONLY)) { 208 | runningOnly = Boolean.parseBoolean(configuration.getProperty( 209 | EC2ResourceModelSourceFactory.RUNNING_ONLY)); 210 | logger.info("[debug] runningOnly:" + runningOnly); 211 | } 212 | 213 | 214 | if (null != httpProxyHost && !"".equals(httpProxyHost)) { 215 | this.clientConfiguration.setProxyHost(httpProxyHost); 216 | this.clientConfiguration.setProxyPort(httpProxyPort); 217 | this.clientConfiguration.setProxyUsername(httpProxyUser); 218 | this.clientConfiguration.setProxyPassword(httpProxyPass); 219 | } 220 | 221 | queryAsync = !("true".equals(configuration.getProperty(SYNCHRONOUS_LOAD)) || refreshInterval <= 0); 222 | 223 | this.queryNodeInstancesInParallel = Boolean.parseBoolean(configuration.getProperty(EC2ResourceModelSourceFactory.QUERY_NODE_INSTANCES_IN_PARALLEL, "false")); 224 | 225 | final ArrayList params = new ArrayList(); 226 | if (null != filterParams) { 227 | Collections.addAll(params, filterParams.split(";")); 228 | } 229 | loadMapping(); 230 | 231 | mapper = new InstanceToNodeMapper(createEc2Supplier(), mapping, pageResults); 232 | mapper.setFilterParams(params); 233 | mapper.setEndpoint(endpoint); 234 | mapper.setRegion(region); 235 | mapper.setRunningStateOnly(runningOnly); 236 | } 237 | 238 | 239 | protected AWSCredentials createCredentials() { 240 | if (null != accessKey && null != secretKeyStoragePath) { 241 | KeyStorageTree keyStorage = services.getService(KeyStorageTree.class); 242 | String secretKey = getPasswordFromKeyStorage(secretKeyStoragePath, keyStorage); 243 | return new BasicAWSCredentials(accessKey.trim(), secretKey.trim()); 244 | } else if (null != accessKey && null != secretKey) { 245 | return new BasicAWSCredentials(accessKey.trim(), secretKey.trim()); 246 | } 247 | 248 | AWSCredentials credentials = null; 249 | if (this.externalId != null && this.assumeRoleArnCombinedWithExtId != null) { 250 | credentials = createAwsCredentials(null, this.assumeRoleArnCombinedWithExtId, this.externalId); 251 | } 252 | 253 | if (assumeRoleArn != null) { 254 | AWSCredentialsProvider provider = null; 255 | if (credentials != null) { 256 | provider = new AWSStaticCredentialsProvider(credentials); 257 | } 258 | 259 | return createAwsCredentials(provider, assumeRoleArn, null); 260 | } 261 | return credentials; 262 | } 263 | 264 | 265 | private EC2SupplierImpl createEc2Supplier() { 266 | return new EC2SupplierImpl( 267 | createCredentials(), 268 | clientConfiguration, 269 | // Use old default us-east-1 for AWS EC2, to maintain default behavior for existing configurations 270 | RegionUtils.getRegion(Regions.US_EAST_1.getName()) 271 | ); 272 | } 273 | 274 | private AWSCredentials createAwsCredentials(AWSCredentialsProvider provider, String assumeRoleArn, String externalId) { 275 | AWSSecurityTokenService sts_client; 276 | 277 | if (provider != null) { 278 | sts_client = AWSSecurityTokenServiceClientBuilder.standard() 279 | .withCredentials(provider) 280 | .withClientConfiguration(clientConfiguration) 281 | .build(); 282 | } else { 283 | sts_client = AWSSecurityTokenServiceClientBuilder.standard() 284 | .withClientConfiguration(clientConfiguration) 285 | .build(); 286 | } 287 | AssumeRoleRequest assumeRoleRequest = new AssumeRoleRequest(); 288 | assumeRoleRequest.setRoleArn(assumeRoleArn); 289 | if(externalId!=null){ 290 | assumeRoleRequest.setExternalId(externalId); 291 | } 292 | assumeRoleRequest.setRoleSessionName("RundeckEC2ResourceModelSourceSession"); 293 | AssumeRoleResult assumeRoleResult = sts_client.assumeRole(assumeRoleRequest); 294 | Credentials assumeCredentials = assumeRoleResult.getCredentials(); 295 | return new BasicSessionCredentials( 296 | assumeCredentials.getAccessKeyId(), 297 | assumeCredentials.getSecretAccessKey(), 298 | assumeCredentials.getSessionToken() 299 | ); 300 | } 301 | 302 | public synchronized INodeSet getNodes() throws ResourceModelSourceException { 303 | checkFuture(); 304 | 305 | // Return cached results if not time to refresh 306 | if (!needsRefresh()) { 307 | if (null != iNodeSet) { 308 | logger.info("Returning " + iNodeSet.getNodeNames().size() + " cached nodes from EC2"); 309 | } 310 | return iNodeSet; 311 | } 312 | 313 | /** 314 | * Rundeck now executes getNodes() in a thread pool by default. 315 | * If queryAync is false(default now) or this is the first fetch we just block here. 316 | */ 317 | if (lastRefresh > 0 && queryAsync && null == futureResult) { 318 | futureResult = executor.submit(() -> { 319 | return mapper.performQuery(queryNodeInstancesInParallel); 320 | }); 321 | lastRefresh = System.currentTimeMillis(); 322 | } else if (!queryAsync || lastRefresh < 1) { 323 | //always perform synchronous query the first time 324 | iNodeSet = mapper.performQuery(queryNodeInstancesInParallel); 325 | lastRefresh = System.currentTimeMillis(); 326 | } 327 | 328 | if (null != iNodeSet) { 329 | logger.info("Read " + iNodeSet.getNodeNames().size() + " nodes from EC2"); 330 | } 331 | 332 | return iNodeSet; 333 | } 334 | 335 | /** 336 | * if any future results are pending, check if they are done and retrieve the results 337 | */ 338 | private void checkFuture() { 339 | if (null != futureResult && futureResult.isDone()) { 340 | try { 341 | iNodeSet = futureResult.get(); 342 | } catch (InterruptedException e) { 343 | logger.debug("Interrupted",e); 344 | } catch (ExecutionException e) { 345 | logger.warn("Error performing query: " + e.getMessage(), e); 346 | } 347 | futureResult = null; 348 | } 349 | } 350 | 351 | /** 352 | * Returns true if the last refresh time was longer ago than the refresh interval 353 | */ 354 | private boolean needsRefresh() { 355 | return refreshInterval < 0 || (System.currentTimeMillis() - lastRefresh > refreshInterval); 356 | } 357 | 358 | private void loadMapping() { 359 | if (useDefaultMapping) { 360 | mapping.putAll(defaultMapping); 361 | } 362 | if (null != mappingFile) { 363 | try { 364 | final FileInputStream fileInputStream = new FileInputStream(mappingFile); 365 | try { 366 | mapping.load(fileInputStream); 367 | } finally { 368 | fileInputStream.close(); 369 | } 370 | } catch (IOException e) { 371 | logger.warn("Error loading mapping file",e); 372 | } 373 | } 374 | if (null != mappingParams) { 375 | for (final String s : mappingParams.split(";")) { 376 | if (s.contains("=")) { 377 | final String[] split = s.split("=", 2); 378 | if (2 == split.length) { 379 | mapping.put(split[0], split[1]); 380 | } 381 | } 382 | } 383 | } 384 | if (mapping.size() < 1) { 385 | mapping.putAll(defaultMapping); 386 | } 387 | } 388 | 389 | public void validate() throws ConfigurationException { 390 | if (null != accessKey && null == secretKey && null == secretKeyStoragePath) { 391 | throw new ConfigurationException("secretKey is required for use with accessKey"); 392 | } 393 | } 394 | 395 | static String getPasswordFromKeyStorage(String path, KeyStorageTree storage) { 396 | try{ 397 | String key = new String(storage.readPassword(path)); 398 | return key; 399 | }catch (Exception e){ 400 | throw StorageException.readException( 401 | PathUtil.asPath(path), 402 | "error accessing key storage at " + path + ": " + e.getMessage() 403 | ); 404 | } 405 | 406 | } 407 | } 408 | -------------------------------------------------------------------------------- /src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSourceFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 DTO Solutions, Inc. (http://dtosolutions.com) 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 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * EC2ResourceModelSource.java 19 | * 20 | * User: Greg Schueler greg@dtosolutions.com 21 | * Created: 9/1/11 4:27 PM 22 | * 23 | */ 24 | package com.dtolabs.rundeck.plugin.resources.ec2; 25 | 26 | import com.dtolabs.rundeck.core.common.Framework; 27 | import com.dtolabs.rundeck.core.plugins.Plugin; 28 | import com.dtolabs.rundeck.core.plugins.configuration.*; 29 | import com.dtolabs.rundeck.core.resources.ResourceModelSource; 30 | import com.dtolabs.rundeck.core.resources.ResourceModelSourceFactory; 31 | import com.dtolabs.rundeck.plugins.util.DescriptionBuilder; 32 | import org.rundeck.app.spi.Services; 33 | 34 | import java.io.File; 35 | import java.util.*; 36 | 37 | /** 38 | *

EC2ResourceModelSourceFactory is the factory that can create a {@link ResourceModelSource} based on a configuration.

39 | *

The configuration properties are:

40 | *
    41 | *
  • endpoint: the AWS endpoint to use, or blank for the default (us-east-1)
  • 42 | *
  • filter: A set of ";" separated query filters ("filter=value") for the AWS EC2 API, see 43 | * DescribeInstances
  • 44 | *
  • mappingParams: A set of ";" separated mapping entries
  • 45 | *
  • runningOnly: if "true", automatically filter the instances by "instance-state-name=running"
  • 46 | *
  • accessKey: API AccessKey value
  • 47 | *
  • secretKey: API SecretKey value
  • 48 | *
  • mappingFile: Path to a java properties-formatted mapping definition file.
  • 49 | *
  • refreshInterval: Time in seconds used as minimum interval between calls to the AWS API.
  • 50 | *
  • useDefaultMapping: if "true", base all mapping definitions off the default mapping provided.
  • 51 | *
52 | * @author Greg Schueler greg@rundeck.com 53 | */ 54 | @Plugin(name = "aws-ec2", service = "ResourceModelSource") 55 | public class EC2ResourceModelSourceFactory implements ResourceModelSourceFactory, Describable { 56 | public static final String PROVIDER_NAME = "aws-ec2"; 57 | private Framework framework; 58 | 59 | public static final String ENDPOINT = "endpoint"; 60 | public static final String FILTER_PARAMS = "filter"; 61 | public static final String MAPPING_PARAMS = "mappingParams"; 62 | public static final String RUNNING_ONLY = "runningOnly"; 63 | public static final String ACCESS_KEY = "accessKey"; 64 | public static final String SECRET_KEY = "secretKey"; 65 | public static final String SECRET_KEY_STORAGE_PATH = "secretKeyStoragePath"; 66 | public static final String ROLE_ARN = "assumeRoleArn"; 67 | public static final String ROLE_ARN_COMBINED_WITH_EXT_ID = "assumeRoleArnCombinedWithExternalId"; 68 | public static final String EXTERNAL_ID = "externalId"; 69 | public static final String REGION = "region"; 70 | public static final String MAPPING_FILE = "mappingFile"; 71 | public static final String REFRESH_INTERVAL = "refreshInterval"; 72 | public static final String SYNCHRONOUS_LOAD = "synchronousLoad"; 73 | public static final String USE_DEFAULT_MAPPING = "useDefaultMapping"; 74 | public static final String QUERY_NODE_INSTANCES_IN_PARALLEL = "queryNodeInstancesInParallel"; 75 | public static final String HTTP_PROXY_HOST = "httpProxyHost"; 76 | public static final String HTTP_PROXY_PORT = "httpProxyPort"; 77 | public static final String HTTP_PROXY_USER = "httpProxyUser"; 78 | public static final String HTTP_PROXY_PASS = "httpProxyPass"; 79 | public static final String MAX_RESULTS = "pageResults"; 80 | 81 | public EC2ResourceModelSourceFactory() { 82 | 83 | } 84 | public EC2ResourceModelSourceFactory(final Framework framework) { 85 | } 86 | 87 | public ResourceModelSource createResourceModelSource(Services services, final Properties configuration) throws ConfigurationException { 88 | final EC2ResourceModelSource ec2ResourceModelSource = new EC2ResourceModelSource(configuration, services); 89 | ec2ResourceModelSource.validate(); 90 | return ec2ResourceModelSource; 91 | } 92 | 93 | public ResourceModelSource createResourceModelSource(Properties configuration) throws ConfigurationException { 94 | return null; 95 | } 96 | 97 | public static final Map PROXY_OPTIONS = Map.of( 98 | StringRenderingConstants.GROUP_NAME, "Proxy", 99 | StringRenderingConstants.GROUPING, "secondary" 100 | ); 101 | 102 | public static final Map PASSWORD_OPTIONS = Collections.singletonMap(StringRenderingConstants.DISPLAY_TYPE_KEY, StringRenderingConstants.DisplayType.PASSWORD); 103 | 104 | public static final Description DESC = DescriptionBuilder.builder() 105 | .name(PROVIDER_NAME) 106 | .title("AWS EC2 Resources") 107 | .description("Produces nodes from AWS EC2") 108 | 109 | .property(PropertyUtil.string(ACCESS_KEY, "Access Key", "AWS Access Key", false, null)) 110 | .property( 111 | PropertyUtil.string( 112 | SECRET_KEY, 113 | "Secret Key", 114 | "AWS Secret Key. Required if Access Key is used and Secret Key Storage Path is blank.\nIf `Access Key` is not used, then the IAM profile will be used.", 115 | false, 116 | null, 117 | null, 118 | null, 119 | PASSWORD_OPTIONS 120 | ) 121 | ) 122 | .property( 123 | PropertyUtil.string( 124 | SECRET_KEY_STORAGE_PATH, 125 | "Secret Key Storage Path", 126 | "Key Storage Path for AWS Secret Key. Required if Access Key is used and Secret Key is blank.\nIf `Access Key` is not used, then the IAM profile will be used.", 127 | false, 128 | null, 129 | null, 130 | null, 131 | Map.of( 132 | StringRenderingConstants.SELECTION_ACCESSOR_KEY, StringRenderingConstants.SelectionAccessor.STORAGE_PATH, 133 | StringRenderingConstants.STORAGE_PATH_ROOT_KEY, "keys", 134 | StringRenderingConstants.STORAGE_FILE_META_FILTER_KEY, "Rundeck-data-type=password" 135 | ) 136 | ) 137 | ) 138 | .property( 139 | PropertyUtil.string( 140 | ROLE_ARN, 141 | "Assume Role ARN", 142 | "IAM Role ARN to assume, if using IAM Profile only.\n\nSee [IAM Roles](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html).", 143 | false, 144 | null, 145 | null, 146 | null 147 | ) 148 | ) 149 | .property(PropertyUtil.bool( 150 | SYNCHRONOUS_LOAD, 151 | "Synchronous Loading", 152 | "Do not use internal async loading behavior.\n\n" + 153 | "Note: Rundeck 2.6.3+ uses an asynchronous nodes cache by " + 154 | "default. You should enable this if you are using the " + 155 | "rundeck nodes cache.", 156 | false, 157 | "true" 158 | )) 159 | .property(PropertyUtil.integer(REFRESH_INTERVAL, "Async Refresh Interval", 160 | "Unless using Synchronous Loading, minimum time in seconds between API requests to AWS (default is 30)", false, "30")) 161 | .property(PropertyUtil.string( 162 | FILTER_PARAMS, 163 | "Filter Params", 164 | "AWS EC2 filters, in the form `Filter=Value`.\n\nYou can " 165 | + "specify multiple filters by separating them with `;`, and " 166 | + "you can specify multiple values by separating them with `," 167 | + "`. See [AWS DescribeInstances](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeInstances.html) for more information about filters.\n\n" 168 | + "Example: `tag:MyTag=Some Tag Value;instance-type=m1.small,m1.large`", 169 | false, 170 | null 171 | )) 172 | .property(PropertyUtil.string(ENDPOINT, "Endpoint", "AWS EC2 Endpoint to specify region, or blank for default. Include comma-separated list of endpoints to integrate with multiple regions.\n\n" + 173 | "Example: `https://ec2.us-west-1.amazonaws.com, https://ec2.us-east-1.amazonaws.com` This would retrieve instances from the `US-WEST-1` and `US-EAST-1` regions.\n" + 174 | "Optionally use `ALL_REGIONS` to automatically pull in instances from all regions that the AWS credentials (or IAM Role) have access to.", 175 | false, 176 | null)) 177 | .property(PropertyUtil.string(REGION, "Region", "AWS EC2 region.", 178 | false, 179 | null)) 180 | .property(PropertyUtil.string(HTTP_PROXY_HOST, "HTTP Proxy Host", "HTTP Proxy Host Name, or blank for default", false, null, null,null, 181 | PROXY_OPTIONS 182 | )) 183 | .property(PropertyUtil.integer(HTTP_PROXY_PORT, "HTTP Proxy Port", "HTTP Proxy Port, or blank for 80", false, "80",null,null,PROXY_OPTIONS)) 184 | .property(PropertyUtil.string(HTTP_PROXY_USER, "HTTP Proxy User", "HTTP Proxy User Name, or blank for default", false, null,null,null,PROXY_OPTIONS)) 185 | .property( 186 | PropertyUtil.string( 187 | HTTP_PROXY_PASS, 188 | "HTTP Proxy Password", 189 | "HTTP Proxy Password, or blank for default", 190 | false, 191 | null, 192 | null, 193 | null, 194 | Map.of( 195 | StringRenderingConstants.GROUP_NAME, "Proxy", 196 | StringRenderingConstants.GROUPING, "secondary", 197 | StringRenderingConstants.DISPLAY_TYPE_KEY, StringRenderingConstants.DisplayType.PASSWORD 198 | ) 199 | ) 200 | ) 201 | .property(PropertyUtil.string(MAPPING_PARAMS, "Mapping Params", 202 | "Property mapping definitions. Specify multiple mappings in the form " + 203 | "\"attributeName.selector=selector\" or \"attributeName.default=value\", " + 204 | "separated by \";\"", 205 | false, null)) 206 | .property(PropertyUtil.string(MAPPING_FILE, "Mapping File", "Property mapping File", false, null, 207 | s -> { 208 | if (!new File(s).isFile()) { 209 | throw new ValidationException("File does not exist: " + s); 210 | } 211 | return true; 212 | })) 213 | .property(PropertyUtil.bool(USE_DEFAULT_MAPPING, "Use Default Mapping", 214 | "Start with default mapping definition. (Defaults will automatically be used if no others are " + 215 | "defined.)", 216 | false, "true")) 217 | .property(PropertyUtil.bool(RUNNING_ONLY, "Only Running Instances", 218 | "Include Running state instances only. If false, all instances will be returned that match your " + 219 | "filters.", 220 | false, "true")) 221 | .property(PropertyUtil.integer(MAX_RESULTS, "Max API Results", 222 | "Max number of reservations returned per AWS API call.", 223 | false, "100")) 224 | .property(PropertyUtil.bool(QUERY_NODE_INSTANCES_IN_PARALLEL, "Query Node Instances in Parallel", 225 | "Query node instances in parallel. If false, instances will be queried one at a time.", 226 | false, "false")) 227 | 228 | .build(); 229 | 230 | public Description getDescription() { 231 | return DESC; 232 | } 233 | } 234 | -------------------------------------------------------------------------------- /src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2Supplier.java: -------------------------------------------------------------------------------- 1 | package com.dtolabs.rundeck.plugin.resources.ec2; 2 | 3 | import com.amazonaws.services.ec2.AmazonEC2; 4 | 5 | /** 6 | * Interface for supplying AmazonEC2 clients 7 | */ 8 | public interface EC2Supplier { 9 | /** 10 | * Return an AmazonEC2 client for the default region 11 | * 12 | * @return AmazonEC2 client 13 | */ 14 | AmazonEC2 getEC2ForDefaultRegion(); 15 | 16 | /** 17 | * Return an AmazonEC2 client for the specified region 18 | * 19 | * @param region region name 20 | * @return AmazonEC2 client 21 | */ 22 | AmazonEC2 getEC2ForRegion(String region); 23 | 24 | /** 25 | * Return an AmazonEC2 client for the specified endpoint 26 | * 27 | * @param endpoint endpoint URL 28 | * @return AmazonEC2 client 29 | */ 30 | AmazonEC2 getEC2ForEndpoint(String endpoint); 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/EC2SupplierImpl.java: -------------------------------------------------------------------------------- 1 | package com.dtolabs.rundeck.plugin.resources.ec2; 2 | 3 | import com.amazonaws.ClientConfiguration; 4 | import com.amazonaws.auth.AWSCredentials; 5 | import com.amazonaws.auth.AWSStaticCredentialsProvider; 6 | import com.amazonaws.client.builder.AwsClientBuilder; 7 | import com.amazonaws.regions.Region; 8 | import com.amazonaws.regions.Regions; 9 | import com.amazonaws.services.ec2.AmazonEC2; 10 | import com.amazonaws.services.ec2.AmazonEC2ClientBuilder; 11 | 12 | 13 | /** 14 | * Implementation of EC2Supplier, uses the AWS SDK to create AmazonEC2 clients via the AmazonEC2ClientBuilder 15 | */ 16 | public class EC2SupplierImpl implements EC2Supplier { 17 | final private AWSCredentials credentials; 18 | final private ClientConfiguration clientConfiguration; 19 | final private Region defaultRegion; 20 | 21 | /** 22 | * Create an instance with the specified credentials and client configuration 23 | * 24 | * @param credentials AWS credentials 25 | * @param clientConfiguration client configuration 26 | * @param region default region 27 | */ 28 | public EC2SupplierImpl(AWSCredentials credentials, ClientConfiguration clientConfiguration, Region region) { 29 | this.credentials = credentials; 30 | this.clientConfiguration = clientConfiguration; 31 | this.defaultRegion = region; 32 | } 33 | 34 | @Override 35 | public AmazonEC2 getEC2ForDefaultRegion() { 36 | return getEC2ForRegion(null); 37 | } 38 | 39 | /** 40 | * Return an AmazonEC2 client for the specified region, if the region is null, the default region is used 41 | * 42 | * @param region region name 43 | * @return AmazonEC2 client 44 | */ 45 | @Override 46 | public AmazonEC2 getEC2ForRegion(String region) { 47 | if (null == region) { 48 | region = defaultRegion.getName(); 49 | } 50 | AmazonEC2ClientBuilder builder = AmazonEC2ClientBuilder.standard().withRegion(region).withClientConfiguration(clientConfiguration); 51 | if (null != credentials) { 52 | builder.withCredentials(new AWSStaticCredentialsProvider(credentials)); 53 | } 54 | return builder.build(); 55 | } 56 | 57 | @Override 58 | public AmazonEC2 getEC2ForEndpoint(String endpoint) { 59 | if (null == endpoint) { 60 | return getEC2ForDefaultRegion(); 61 | } 62 | AwsClientBuilder.EndpointConfiguration endpointConfiguration = new AwsClientBuilder.EndpointConfiguration(endpoint, null); 63 | AmazonEC2ClientBuilder amazonEC2ClientBuilder = AmazonEC2ClientBuilder.standard().withEndpointConfiguration(endpointConfiguration); 64 | if (null != credentials) { 65 | amazonEC2ClientBuilder.withCredentials(new AWSStaticCredentialsProvider(credentials)); 66 | } 67 | if (null != clientConfiguration) { 68 | amazonEC2ClientBuilder.withClientConfiguration(clientConfiguration); 69 | } 70 | return amazonEC2ClientBuilder.build(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/Ec2Instance.java: -------------------------------------------------------------------------------- 1 | package com.dtolabs.rundeck.plugin.resources.ec2; 2 | 3 | import com.amazonaws.services.ec2.model.Instance; 4 | import com.amazonaws.services.ec2.model.Region; 5 | 6 | import java.lang.reflect.Field; 7 | import java.util.ArrayList; 8 | import java.util.Collections; 9 | import java.util.List; 10 | 11 | public class Ec2Instance extends Instance { 12 | 13 | String imageName; 14 | String region; 15 | 16 | public String getImageName() { 17 | return imageName; 18 | } 19 | 20 | public String getRegion() { 21 | return region; 22 | } 23 | 24 | public void setRegion(String region) { 25 | this.region = region; 26 | } 27 | 28 | public void setImageName(String imageName) { 29 | this.imageName = imageName; 30 | } 31 | 32 | public static Ec2Instance builder(Instance instance) { 33 | Ec2Instance ec2Custom = new Ec2Instance(); 34 | try { 35 | copy(instance,ec2Custom); 36 | } catch (Exception e) { 37 | return null; 38 | } 39 | return ec2Custom; 40 | } 41 | 42 | private static void copy(X src,Y dest) throws Exception 43 | { 44 | List aFields = getAllFields(src.getClass()); 45 | List bFields = getAllFields(dest.getClass()); 46 | 47 | for (Field aField : aFields) { 48 | aField.setAccessible(true); 49 | for (Field bField : bFields) { 50 | bField.setAccessible(true); 51 | if (aField.getName().equals(bField.getName())) 52 | { 53 | bField.set(dest, aField.get(src)); 54 | } 55 | } 56 | } 57 | } 58 | 59 | private static List getAllFields(Class type) 60 | { 61 | ArrayList allFields = new ArrayList(); 62 | while (type != Object.class) 63 | { 64 | Collections.addAll(allFields, type.getDeclaredFields()); 65 | type = type.getSuperclass(); 66 | } 67 | return allFields; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/dtolabs/rundeck/plugin/resources/ec2/InstanceToNodeMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 DTO Solutions, Inc. (http://dtosolutions.com) 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 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * NodeGenerator.java 19 | * 20 | * User: Greg Schueler greg@dtosolutions.com 21 | * Created: Oct 18, 2010 7:03:37 PM 22 | * 23 | */ 24 | package com.dtolabs.rundeck.plugin.resources.ec2; 25 | 26 | import com.amazonaws.services.ec2.AmazonEC2; 27 | import com.amazonaws.services.ec2.AmazonEC2Client; 28 | import com.amazonaws.services.ec2.model.*; 29 | import com.dtolabs.rundeck.core.common.INodeEntry; 30 | import com.dtolabs.rundeck.core.common.NodeEntryImpl; 31 | import com.dtolabs.rundeck.core.common.NodeSetImpl; 32 | import org.apache.commons.beanutils.BeanUtils; 33 | import org.slf4j.Logger; 34 | import org.slf4j.LoggerFactory; 35 | 36 | import java.util.*; 37 | import java.util.concurrent.*; 38 | import java.util.regex.Matcher; 39 | import java.util.regex.Pattern; 40 | import java.util.stream.Collectors; 41 | 42 | /** 43 | * InstanceToNodeMapper produces Rundeck node definitions from EC2 Instances 44 | * 45 | * @author Greg Schueler greg@dtosolutions.com 46 | */ 47 | class InstanceToNodeMapper { 48 | static final Logger logger = LoggerFactory.getLogger(InstanceToNodeMapper.class); 49 | private ArrayList filterParams; 50 | private String endpoint; 51 | private String region; 52 | private boolean runningStateOnly = true; 53 | private Properties mapping; 54 | private final int maxResults; 55 | private final EC2Supplier ec2Supplier; 56 | private DescribeAvailabilityZonesResult zones; 57 | 58 | private static final String[] extraInstanceMappingAttributes= {"imageName","region"}; 59 | 60 | /** 61 | * Create with the credentials and mapping definition 62 | */ 63 | InstanceToNodeMapper(final EC2Supplier ec2Supplier, final Properties mapping, final int maxResults) { 64 | this.ec2Supplier = ec2Supplier; 65 | this.mapping = mapping; 66 | this.maxResults = maxResults; 67 | } 68 | 69 | 70 | /** 71 | * Perform the query and return the set of instances 72 | * 73 | */ 74 | public NodeSetImpl performQuery(boolean queryNodeInstancesInParallel) { 75 | final NodeSetImpl nodeSet = new NodeSetImpl(); 76 | 77 | Set instances = new HashSet<>(); 78 | 79 | DescribeInstancesRequest request = new DescribeInstancesRequest().withFilters(buildFilters()).withMaxResults(maxResults); 80 | 81 | if(getEndpoint() != null) { 82 | ExecutorService executor = null; 83 | Collection>> futures = new LinkedList>>(); 84 | Set>> tasks = new HashSet<>(); 85 | List endpoints = determineEndpoints(); 86 | for (String endpoint : endpoints) { 87 | if(queryNodeInstancesInParallel) { 88 | if(executor == null){ 89 | logger.info("Creating thread pool for {} regions", endpoints.size() ); 90 | executor = Executors.newFixedThreadPool(endpoints.size()); 91 | } 92 | tasks.add(new Callable>() { 93 | @Override 94 | public Set call() throws Exception { 95 | return getInstancesByRegion(endpoint); 96 | }; 97 | }); 98 | }else{ 99 | instances.addAll(getInstancesByRegion(endpoint)); 100 | } 101 | } 102 | if(queryNodeInstancesInParallel) { 103 | try { 104 | logger.info("Querying {} regions in parallel", endpoints.size() ); 105 | futures = executor.invokeAll(tasks); 106 | } catch (InterruptedException e) { 107 | throw new RuntimeException(e); 108 | } finally { 109 | try { 110 | for (Future> future : futures) { 111 | if (future != null) { 112 | instances.addAll(future.get()); 113 | } 114 | } 115 | logger.info("Finished querying {} regions in parallel", endpoints.size() ); 116 | executor.shutdown(); 117 | } catch (InterruptedException e) { 118 | throw new RuntimeException(e); 119 | } catch (ExecutionException e) { 120 | throw new RuntimeException(e); 121 | } 122 | } 123 | try { 124 | // Wait for 90 seconds for all tasks to finish 125 | logger.info("Waiting for {} seconds for all tasks to finish", 90); 126 | executor.awaitTermination(90, TimeUnit.SECONDS); 127 | } catch (InterruptedException ignored) { 128 | // Restore interrupted status 129 | logger.warn("Thread interrupted while waiting for tasks to finish", ignored); 130 | Thread.currentThread().interrupt(); 131 | } finally { 132 | // Force shutdown if not already done 133 | logger.warn("Forcing shutdown of thread pool"); 134 | executor.shutdownNow(); 135 | } 136 | } 137 | } 138 | else if(region != null){ 139 | AmazonEC2 ec2ForRegion = ec2Supplier.getEC2ForRegion(region); 140 | 141 | 142 | zones = ec2ForRegion.describeAvailabilityZones(); 143 | 144 | final Set newInstances = addExtraMappingAttribute(ec2ForRegion, query(ec2ForRegion, request)); 145 | 146 | if (newInstances != null && !newInstances.isEmpty()) { 147 | instances.addAll(newInstances); 148 | } 149 | } 150 | else{ 151 | AmazonEC2 ec2 = ec2Supplier.getEC2ForDefaultRegion(); 152 | zones = ec2.describeAvailabilityZones(); 153 | 154 | instances = addExtraMappingAttribute(ec2,query(ec2, request)); 155 | } 156 | mapInstances(nodeSet, instances); 157 | return nodeSet; 158 | } 159 | 160 | private List determineEndpoints() { 161 | ArrayList endpoints = new ArrayList<>(); 162 | if (getEndpoint().equals("ALL_REGIONS")) { 163 | 164 | //Retrieve dynamic list of EC2 regions from AWS 165 | DescribeRegionsResult regionsResult = ec2Supplier.getEC2ForDefaultRegion().describeRegions(); 166 | for (Region region : regionsResult.getRegions()) { 167 | endpoints.add(region.getEndpoint()); 168 | } 169 | 170 | } else { 171 | try { 172 | //Use comma-separated list of region supplied by user 173 | endpoints.addAll(Arrays.asList(getEndpoint().replaceAll("\\s+", "").split(","))); 174 | } catch (NullPointerException e) { 175 | throw new IllegalArgumentException("Failed to parse endpoint: Region cannot be empty"); 176 | } 177 | } 178 | return endpoints; 179 | } 180 | 181 | private Set getInstancesByRegion(String endpoint) { 182 | Set allInstances = new HashSet<>(); 183 | AmazonEC2 ec2 = ec2Supplier.getEC2ForEndpoint(endpoint); 184 | zones = ec2.describeAvailabilityZones(); 185 | final ArrayList filters = buildFilters(); 186 | 187 | final Set newInstances = addExtraMappingAttribute(ec2, query(ec2, new DescribeInstancesRequest().withFilters(filters).withMaxResults(maxResults))); 188 | 189 | if (!newInstances.isEmpty() && newInstances != null) { 190 | allInstances.addAll(newInstances); 191 | } 192 | 193 | return allInstances; 194 | } 195 | 196 | private Set query(final AmazonEC2 ec2, final DescribeInstancesRequest request) { 197 | //create "running" filter 198 | final Set instances = new HashSet<>(); 199 | 200 | String token = null; 201 | do { 202 | final DescribeInstancesRequest pagedRequest = request.clone(); 203 | pagedRequest.setNextToken(token); 204 | 205 | final DescribeInstancesResult describeInstancesRequest = ec2.describeInstances(pagedRequest); 206 | 207 | token = describeInstancesRequest.getNextToken(); 208 | 209 | instances.addAll(examineResult(describeInstancesRequest)); 210 | } while(token != null); 211 | 212 | return instances; 213 | } 214 | 215 | private Set examineResult(DescribeInstancesResult describeInstancesRequest) { 216 | final List reservations = describeInstancesRequest.getReservations(); 217 | final Set instances = new HashSet<>(); 218 | 219 | for (final Reservation reservation : reservations) { 220 | instances.addAll(reservation.getInstances()); 221 | } 222 | return instances; 223 | } 224 | 225 | private ArrayList buildFilters() { 226 | final ArrayList filters = new ArrayList<>(); 227 | if (isRunningStateOnly()) { 228 | final Filter filter = new Filter("instance-state-name").withValues(InstanceStateName.Running.toString()); 229 | filters.add(filter); 230 | } 231 | 232 | if (null != getFilterParams()) { 233 | for (final String filterParam : getFilterParams()) { 234 | final String[] x = filterParam.split("=", 2); 235 | if (!"".equals(x[0]) && !"".equals(x[1])) { 236 | filters.add(new Filter(x[0]).withValues(x[1].split(","))); 237 | } 238 | } 239 | } 240 | return filters; 241 | } 242 | 243 | private void mapInstances(final NodeSetImpl nodeSet, final Set instances) { 244 | for (final Instance inst : instances) { 245 | final INodeEntry iNodeEntry; 246 | try { 247 | iNodeEntry = InstanceToNodeMapper.instanceToNode(inst, mapping); 248 | if (null != iNodeEntry) { 249 | nodeSet.putNode(iNodeEntry); 250 | } 251 | } catch (GeneratorException e) { 252 | logger.error("Generator error",e); 253 | } 254 | } 255 | } 256 | 257 | /** 258 | * Convert an AWS EC2 Instance to a RunDeck INodeEntry based on the mapping input 259 | */ 260 | @SuppressWarnings("unchecked") 261 | static INodeEntry instanceToNode(final Instance inst, final Properties mapping) throws GeneratorException { 262 | final NodeEntryImpl node = new NodeEntryImpl(); 263 | 264 | //evaluate single settings.selector=tags/* mapping 265 | if ("tags/*".equals(mapping.getProperty("attributes.selector"))) { 266 | //iterate through instance tags and generate settings 267 | for (final Tag tag : inst.getTags()) { 268 | if (null == node.getAttributes()) { 269 | node.setAttributes(new HashMap<>()); 270 | } 271 | node.getAttributes().put(tag.getKey(), tag.getValue()); 272 | } 273 | } 274 | if (null != mapping.getProperty("tags.selector")) { 275 | final String selector = mapping.getProperty("tags.selector"); 276 | final String value = applySelector(inst, selector, mapping.getProperty("tags.default"), true); 277 | if (null != value) { 278 | final String[] values = value.split(","); 279 | final HashSet tagset = new HashSet<>(); 280 | for (final String s : values) { 281 | tagset.add(s.trim()); 282 | } 283 | if (null == node.getTags()) { 284 | node.setTags(tagset); 285 | } else { 286 | final Set orig = new HashSet(node.getTags()); 287 | orig.addAll(tagset); 288 | node.setTags(orig); 289 | } 290 | } 291 | } 292 | if (null == node.getTags()) { 293 | node.setTags(new HashSet()); 294 | } 295 | final Set orig = new HashSet(node.getTags()); 296 | //apply specific tag selectors 297 | final Pattern tagPat = Pattern.compile("^tag\\.(.+?)\\.selector$"); 298 | //evaluate tag selectors 299 | for (final Object o : mapping.keySet()) { 300 | final String key = (String) o; 301 | final String selector = mapping.getProperty(key); 302 | //split selector by = if present 303 | final String[] selparts = selector.split("="); 304 | final Matcher m = tagPat.matcher(key); 305 | if (m.matches()) { 306 | final String tagName = m.group(1); 307 | if (null == node.getAttributes()) { 308 | node.setAttributes(new HashMap<>()); 309 | } 310 | final String value = applySelector(inst, selparts[0], null); 311 | if (null != value) { 312 | if (selparts.length > 1 && !value.equals(selparts[1])) { 313 | continue; 314 | } 315 | //use add the tag if the value is not null 316 | orig.add(tagName); 317 | } 318 | } 319 | } 320 | node.setTags(orig); 321 | 322 | //apply default values which do not have corresponding selector 323 | final Pattern attribDefPat = Pattern.compile("^([^.]+?)\\.default$"); 324 | //evaluate selectors 325 | for (final Object o : mapping.keySet()) { 326 | final String key = (String) o; 327 | final String value = mapping.getProperty(key); 328 | final Matcher m = attribDefPat.matcher(key); 329 | if (m.matches() && (!mapping.containsKey(key + ".selector") || "".equals(mapping.getProperty( 330 | key + ".selector")))) { 331 | final String attrName = m.group(1); 332 | if (null == node.getAttributes()) { 333 | node.setAttributes(new HashMap<>()); 334 | } 335 | if (null != value) { 336 | node.getAttributes().put(attrName, value); 337 | } 338 | } 339 | } 340 | 341 | final Pattern attribPat = Pattern.compile("^([^.]+?)\\.selector$"); 342 | //evaluate selectors 343 | for (final Object o : mapping.keySet()) { 344 | final String key = (String) o; 345 | final String selector = mapping.getProperty(key); 346 | final Matcher m = attribPat.matcher(key); 347 | if (m.matches()) { 348 | final String attrName = m.group(1); 349 | if(attrName.equals("tags")){ 350 | //already handled 351 | continue; 352 | } 353 | if (null == node.getAttributes()) { 354 | node.setAttributes(new HashMap<>()); 355 | } 356 | final String value = applySelector(inst, selector, mapping.getProperty(attrName + ".default")); 357 | if (null != value) { 358 | //use nodename-settingname to make the setting unique to the node 359 | node.getAttributes().put(attrName, value); 360 | } 361 | } 362 | } 363 | // String hostSel = mapping.getProperty("hostname.selector"); 364 | // String host = applySelector(inst, hostSel, mapping.getProperty("hostname.default")); 365 | // if (null == node.getHostname()) { 366 | // System.err.println("Unable to determine hostname for instance: " + inst.getInstanceId()); 367 | // return null; 368 | // } 369 | String name = node.getNodename(); 370 | if (null == name || name.isEmpty()) { 371 | name = node.getHostname(); 372 | } 373 | if (null == name || name.isEmpty()) { 374 | name = inst.getInstanceId(); 375 | } 376 | node.setNodename(name); 377 | 378 | // Set ssh port on hostname if not 22 379 | String sshport = node.getAttributes().get("sshport"); 380 | if (sshport != null && !sshport.isEmpty() && !sshport.equals("22")) { 381 | node.setHostname(node.getHostname() + ":" + sshport); 382 | } 383 | 384 | 385 | 386 | return node; 387 | } 388 | 389 | /** 390 | * Return the result of the selector applied to the instance, otherwise return the defaultValue. The selector can be 391 | * a comma-separated list of selectors 392 | */ 393 | public static String applySelector(final Instance inst, final String selector, final String defaultValue) throws 394 | GeneratorException { 395 | return applySelector(inst, selector, defaultValue, false); 396 | } 397 | 398 | /** 399 | * Return the result of the selector applied to the instance, otherwise return the defaultValue. The selector can be 400 | * a comma-separated list of selectors. 401 | * @param inst the instance 402 | * @param selector the selector string 403 | * @param defaultValue a default value to return if there is no result from the selector 404 | * @param tagMerge if true, allow | separator to merge multiple values 405 | */ 406 | public static String applySelector(final Instance inst, final String selector, final String defaultValue, 407 | final boolean tagMerge) throws 408 | GeneratorException { 409 | 410 | if (null != selector) { 411 | for (final String selPart : selector.split(",")) { 412 | if (tagMerge) { 413 | final StringBuilder sb = new StringBuilder(); 414 | for (final String subPart : selPart.split(Pattern.quote("|"))) { 415 | final String val = applyMultiSelector(inst, subPart.split(Pattern.quote("+"))); 416 | if (null != val) { 417 | if (sb.length() > 0) { 418 | sb.append(","); 419 | } 420 | sb.append(val); 421 | } 422 | } 423 | if (sb.length() > 0) { 424 | return sb.toString(); 425 | } 426 | } else { 427 | final String val = applyMultiSelector(inst, selPart.split(Pattern.quote("+"))); 428 | if (null != val) { 429 | return val; 430 | } 431 | } 432 | } 433 | } 434 | return defaultValue; 435 | } 436 | 437 | private static final Pattern quoted = Pattern.compile("^(['\"])(.+)\\1$"); 438 | 439 | /** 440 | * Return conjoined multiple selector and literal values only if some selector value matches, otherwise null. 441 | * Apply multiple selectors and separators to determine the value, the selector values are conjoined 442 | * in order if they resolve to a non-blank value. If a selector is a quoted string, the contents are 443 | * conjoined literally 444 | * 445 | * @param inst 446 | * @param selectors 447 | * 448 | * @return conjoined selector values with literal separators, if some selector was resolved, otherwise null 449 | * 450 | * @throws GeneratorException 451 | */ 452 | static String applyMultiSelector(final Instance inst, final String... selectors) throws 453 | GeneratorException 454 | { 455 | StringBuilder sb = new StringBuilder(); 456 | boolean hasVal = false; 457 | for (String selector : selectors) { 458 | Matcher matcher = quoted.matcher(selector); 459 | if (matcher.matches()) { 460 | sb.append(matcher.group(2)); 461 | } else { 462 | String val = applySingleSelector(inst, selector); 463 | if (null != val && !val.isEmpty()) { 464 | hasVal = true; 465 | sb.append(val); 466 | } 467 | } 468 | } 469 | 470 | return hasVal ? sb.toString() : null; 471 | } 472 | static String applySingleSelector(final Instance inst, final String selector) throws 473 | GeneratorException { 474 | if (null != selector && selector.startsWith("tags/")) { 475 | final String tag = selector.substring("tags/".length()); 476 | final List tags = inst.getTags(); 477 | for (final Tag tag1 : tags) { 478 | if (tag.equals(tag1.getKey())) { 479 | return tag1.getValue(); 480 | } 481 | } 482 | } else if (null != selector && !selector.isEmpty()) { 483 | try { 484 | final String value = BeanUtils.getProperty(inst, selector); 485 | if (null != value) { 486 | return value; 487 | } 488 | } catch (Exception e) { 489 | throw new GeneratorException(e); 490 | } 491 | } 492 | 493 | return null; 494 | } 495 | 496 | /** 497 | * Return the list of "filter=value" filters 498 | */ 499 | public ArrayList getFilterParams() { 500 | return filterParams; 501 | } 502 | 503 | /** 504 | * Return the endpoint 505 | */ 506 | public String getEndpoint() { 507 | return endpoint; 508 | } 509 | 510 | /** 511 | * Return true if runningStateOnly 512 | */ 513 | public boolean isRunningStateOnly() { 514 | return runningStateOnly; 515 | } 516 | 517 | /** 518 | * If true, the an automatic "running" state filter will be applied 519 | */ 520 | public void setRunningStateOnly(final boolean runningStateOnly) { 521 | this.runningStateOnly = runningStateOnly; 522 | } 523 | 524 | /** 525 | * Set the list of "filter=value" filters 526 | */ 527 | public void setFilterParams(final ArrayList filterParams) { 528 | this.filterParams = filterParams; 529 | } 530 | 531 | /** 532 | * Set the region endpoint to use. 533 | */ 534 | public void setEndpoint(final String endpoint) { 535 | this.endpoint = endpoint; 536 | } 537 | 538 | public void setRegion(final String region) { 539 | this.region = region; 540 | } 541 | 542 | public Properties getMapping() { 543 | return mapping; 544 | } 545 | 546 | public void setMapping(Properties mapping) { 547 | this.mapping = mapping; 548 | } 549 | 550 | public static class GeneratorException extends Exception { 551 | public GeneratorException() { 552 | } 553 | 554 | public GeneratorException(final String message) { 555 | super(message); 556 | } 557 | 558 | public GeneratorException(final String message, final Throwable cause) { 559 | super(message, cause); 560 | } 561 | 562 | public GeneratorException(final Throwable cause) { 563 | super(cause); 564 | } 565 | } 566 | 567 | public Set addExtraMappingAttribute(AmazonEC2 ec2, Set instances) { 568 | for(String extraAttribute: extraInstanceMappingAttributes){ 569 | if(mappingHasExtraAttribute(extraAttribute)){ 570 | if(extraAttribute.equals("imageName")){ 571 | instances = addingImageName(ec2, instances); 572 | } 573 | if(extraAttribute.equals("region")){ 574 | instances = addingRegion(instances); 575 | } 576 | } 577 | } 578 | return instances; 579 | } 580 | 581 | public boolean mappingHasExtraAttribute(String extraAttribute){ 582 | if(mapping.containsValue(extraAttribute)){ 583 | return true; 584 | }else{ 585 | for (String key : mapping.stringPropertyNames()) { 586 | if(mapping.getProperty(key).contains(extraAttribute)){ 587 | return true; 588 | } 589 | } 590 | } 591 | return false; 592 | } 593 | 594 | public Set addingImageName(AmazonEC2 ec2, Set originalInstances) { 595 | Set instances = new HashSet<>(); 596 | Map ec2Images = new HashMap<>(); 597 | Set imagesList = originalInstances.stream().map(Instance::getImageId).collect(Collectors.toSet()); 598 | logger.debug("Image list: {}", imagesList); 599 | try{ 600 | DescribeImagesRequest describeImagesRequest = new DescribeImagesRequest(); 601 | describeImagesRequest.setImageIds(imagesList); 602 | 603 | DescribeImagesResult result = ec2.describeImages(describeImagesRequest); 604 | 605 | for(Image image :result.getImages()){ 606 | ec2Images.put(image.getImageId(),image); 607 | } 608 | }catch(Exception e){ 609 | logger.error("error getting image info{}", e.getMessage()); 610 | } 611 | 612 | for (final Instance inst : originalInstances) { 613 | Ec2Instance customInstance = Ec2Instance.builder(inst); 614 | 615 | if(ec2Images.containsKey(inst.getImageId())){ 616 | Image image = ec2Images.get(inst.getImageId()); 617 | customInstance.setImageName(image.getName()); 618 | }else{ 619 | customInstance.setImageName("Not found"); 620 | logger.debug("Image not found" + inst.getImageId()); 621 | } 622 | instances.add(customInstance); 623 | 624 | } 625 | 626 | return instances; 627 | } 628 | 629 | public Set addingRegion(Set originalInstances){ 630 | Set instances = new HashSet<>(); 631 | 632 | for (final Instance inst : originalInstances) { 633 | String region = getRegionAvailableZone(inst.getPlacement().getAvailabilityZone()); 634 | Ec2Instance customInstance = Ec2Instance.builder(inst); 635 | 636 | if(region!=null){ 637 | customInstance.setRegion(region); 638 | } 639 | instances.add(customInstance); 640 | } 641 | 642 | return instances; 643 | } 644 | 645 | private String getRegionAvailableZone(String availableZone){ 646 | 647 | String region = null; 648 | 649 | for(AvailabilityZone zone : zones.getAvailabilityZones()) { 650 | if (zone.getZoneName().equals(availableZone)){ 651 | region = zone.getRegionName(); 652 | } 653 | } 654 | 655 | 656 | return region; 657 | } 658 | 659 | } 660 | -------------------------------------------------------------------------------- /src/main/resources/defaultMapping.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2011 DTO Solutions, Inc. (http://dtosolutions.com) 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 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | description.default=EC2 node instance 18 | editUrl.default=https://console.aws.amazon.com/ec2/home#Instances:search=${node.instanceId} 19 | hostname.selector=publicDnsName,privateIpAddress 20 | sshport.default=22 21 | sshport.selector=tags/ssh_config_Port 22 | instanceId.selector=instanceId 23 | nodename.selector=tags/Name,instanceId 24 | osArch.selector=architecture 25 | osFamily.default=unix 26 | osFamily.selector=platform 27 | osName.default=Linux 28 | osName.selector=platform 29 | privateDnsName.selector=privateDnsName 30 | privateIpAddress.selector=privateIpAddress 31 | state.selector=state.name 32 | tag.pending.selector=state.name=pending 33 | tag.running.selector=state.name=running 34 | tag.shutting-down.selector=state.name=shutting-down 35 | tag.stopped.selector=state.name=stopped 36 | tag.stopping.selector=state.name=stopping 37 | tag.terminated.selector=state.name=terminated 38 | tags.default=ec2 39 | tags.selector=tags/Rundeck-Tags 40 | username.default=ec2-user 41 | username.selector=tags/Rundeck-User 42 | region.selector=region -------------------------------------------------------------------------------- /src/test/groovy/com/dtolabs/rundeck/plugin/resources/ec2/EC2ResourceModelSourceSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.dtolabs.rundeck.plugin.resources.ec2 2 | 3 | import com.dtolabs.rundeck.core.common.Framework 4 | import com.dtolabs.rundeck.core.common.IRundeckProject 5 | import com.dtolabs.rundeck.core.common.ProjectManager 6 | import com.dtolabs.rundeck.core.storage.keys.KeyStorageTree 7 | import org.rundeck.app.spi.Services 8 | import org.rundeck.storage.api.StorageException 9 | import spock.lang.Specification 10 | 11 | class EC2ResourceModelSourceSpec extends Specification { 12 | def "user configured access credentials prefer key storage"() { 13 | given: "a user's plugin config" 14 | //Define good and bad keys and paths 15 | def validAccessKey = "validAccessKey" 16 | def validSecretKey = "validSecretKey" 17 | def validKeyPath = "keys/validKeyPath" 18 | def badPath = "keys/badPath" 19 | def badPass = "myNetflixPassword" 20 | 21 | // Mock services and Key Storage return of passwords 22 | def serviceWithGoodPass = mockServicesWithPassword(validKeyPath, validSecretKey) 23 | def serviceWithBadPass = mockServicesWithPassword(badPath, badPass) 24 | 25 | // Create a default config object (these are the settings the user would setup via the Plugin UI) 26 | def defaultConfig = createDefaultConfig() 27 | defaultConfig.setProperty(EC2ResourceModelSourceFactory.ACCESS_KEY, validAccessKey) 28 | 29 | // Create a working config from the defaults 30 | def workingConfig = new Properties() 31 | workingConfig.putAll(defaultConfig) 32 | // Send a bad key to ensure key path takes precedence and succeeds 33 | workingConfig.setProperty(EC2ResourceModelSourceFactory.SECRET_KEY, badPass) 34 | workingConfig.setProperty(EC2ResourceModelSourceFactory.SECRET_KEY_STORAGE_PATH, validKeyPath) 35 | 36 | // Create a failing config from the defaults 37 | def failingConfig = new Properties() 38 | failingConfig.putAll(defaultConfig) 39 | // Send a valid key to ensure storage path takes precedence and fails 40 | failingConfig.setProperty(EC2ResourceModelSourceFactory.SECRET_KEY, validSecretKey) 41 | failingConfig.setProperty(EC2ResourceModelSourceFactory.SECRET_KEY_STORAGE_PATH, badPath) 42 | 43 | // Create objects using actual ResourceModelSource and Factory 44 | EC2ResourceModelSource workingRms = ec2ResourceModelSource(serviceWithGoodPass, workingConfig) 45 | EC2ResourceModelSource failingRms = ec2ResourceModelSource(serviceWithBadPass, failingConfig) 46 | 47 | when: "we check the access keys of the resource model source objects" 48 | // Instead of using getNodes, which would all be highly mocked, just check that we got as far as setting 49 | // proper credentials right before the point we would call to AWS 50 | def workingRmsPass = workingRms.createCredentials().getAWSSecretKey() 51 | def failingRmsPass = failingRms.createCredentials().getAWSSecretKey() 52 | 53 | then: "we see that the proper keys from the key storage or the inline key have been derived" 54 | workingRmsPass == validSecretKey 55 | failingRmsPass == badPass 56 | } 57 | def "fail properly when invalid key path is provided"() { 58 | given: "User plugin config that uses an invalid key path" 59 | //Define good and bad keys and paths 60 | def validAccessKey = "validAccessKey" 61 | def goodKeyPath = "keys/validKeyPath" 62 | def badPath = "keys/badPath" 63 | def badPass = "myNetflixPassword" 64 | 65 | // Mock services and Key Storage return of passwords 66 | def serviceWithBadPass = mockServicesWithPassword(badPath, null) 67 | 68 | // Create a default config object (these are the settings the user would setup via the Plugin UI) 69 | def config = createDefaultConfig() 70 | config.setProperty(EC2ResourceModelSourceFactory.ACCESS_KEY, validAccessKey) 71 | config.setProperty(EC2ResourceModelSourceFactory.SECRET_KEY_STORAGE_PATH, badPath) 72 | 73 | when: "user attempts to create EC2ResourceModelSource instance using invalid path" 74 | def failingRms = ec2ResourceModelSource(serviceWithBadPass, config) 75 | 76 | then: "expect a StorageException#readException to be returned" 77 | StorageException ex = thrown() 78 | ex.message.contains("error accessing key storage at ${badPath}") 79 | } 80 | // 81 | // Private Methods 82 | // 83 | private def createDefaultConfig() { 84 | def configuration = new Properties() 85 | def assumeRoleArn = "arn:aws:iam::123456789012:role/fake-test-arn" 86 | def endpoint = "ALL_REGIONS" 87 | def pageResults = "100" 88 | def proxyPortStr = "80" 89 | def refreshStr = "30" 90 | def useDefaultMapping = "true" 91 | def runningOnly = "true" 92 | 93 | configuration.setProperty(EC2ResourceModelSourceFactory.ROLE_ARN, assumeRoleArn) 94 | configuration.setProperty(EC2ResourceModelSourceFactory.ENDPOINT, endpoint); 95 | configuration.setProperty(EC2ResourceModelSourceFactory.MAX_RESULTS, pageResults); 96 | configuration.setProperty(EC2ResourceModelSourceFactory.HTTP_PROXY_PORT, proxyPortStr); 97 | configuration.setProperty(EC2ResourceModelSourceFactory.REFRESH_INTERVAL, refreshStr); 98 | configuration.setProperty(EC2ResourceModelSourceFactory.USE_DEFAULT_MAPPING, useDefaultMapping) 99 | configuration.setProperty(EC2ResourceModelSourceFactory.RUNNING_ONLY, runningOnly) 100 | 101 | return configuration 102 | } 103 | 104 | private def ec2ResourceModelSource(Services services, Properties configuration) { 105 | def framework = Mock(Framework) 106 | def factory = new EC2ResourceModelSourceFactory(framework) 107 | 108 | return factory.createResourceModelSource(services, configuration) 109 | } 110 | 111 | private def mockServicesWithPassword(String path, String password) { 112 | def storageTree = Mock(KeyStorageTree) { 113 | readPassword(path) >> { 114 | return password.bytes 115 | } 116 | } 117 | 118 | def services = Mock(Services) { 119 | getService(KeyStorageTree.class) >> storageTree 120 | } 121 | 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/test/groovy/com/dtolabs/rundeck/plugin/resources/ec2/InstanceToNodeMapperSpec.groovy: -------------------------------------------------------------------------------- 1 | package com.dtolabs.rundeck.plugin.resources.ec2 2 | 3 | 4 | import com.amazonaws.services.ec2.AmazonEC2Client 5 | import com.amazonaws.services.ec2.model.AvailabilityZone 6 | import com.amazonaws.services.ec2.model.DescribeAvailabilityZonesResult 7 | import com.amazonaws.services.ec2.model.DescribeImagesResult 8 | import com.amazonaws.services.ec2.model.DescribeInstancesResult 9 | import com.amazonaws.services.ec2.model.DescribeRegionsResult 10 | import com.amazonaws.services.ec2.model.Image 11 | import com.amazonaws.services.ec2.model.Instance 12 | import com.amazonaws.services.ec2.model.InstanceState 13 | import com.amazonaws.services.ec2.model.InstanceStateName 14 | import com.amazonaws.services.ec2.model.Placement 15 | import com.amazonaws.services.ec2.model.Region 16 | import com.amazonaws.services.ec2.model.Reservation 17 | import com.amazonaws.services.ec2.model.Tag 18 | import spock.lang.Specification 19 | import spock.lang.Unroll 20 | 21 | /** 22 | * @author greg 23 | * @since 12/16/16 24 | */ 25 | class InstanceToNodeMapperSpec extends Specification { 26 | def "single selector valid properties"() { 27 | given: 28 | def i = mkInstance() 29 | 30 | when: 31 | def result = InstanceToNodeMapper.applySingleSelector(i, selector) 32 | 33 | then: 34 | result == expect 35 | 36 | where: 37 | selector | expect 38 | 'instanceId' | 'aninstanceId' 39 | 'architecture' | 'anarch' 40 | 'state.name' | 'running' 41 | 'privateIpAddress' | '127.0.9.9' 42 | 'publicDnsName' | null 43 | } 44 | 45 | @Unroll 46 | def "single selector invalid properties #selector"() { 47 | given: 48 | def i = mkInstance() 49 | 50 | when: 51 | def result = InstanceToNodeMapper.applySingleSelector(i, selector) 52 | 53 | then: 54 | result == null 55 | InstanceToNodeMapper.GeneratorException e = thrown() 56 | e != null 57 | 58 | where: 59 | selector | _ 60 | 'fromtom' | _ 61 | 'bigglesworth' | _ 62 | 'badapple' | _ 63 | } 64 | 65 | def "single selector tags"() { 66 | given: 67 | def i = mkInstance() 68 | 69 | when: 70 | def result = InstanceToNodeMapper.applySingleSelector(i, selector) 71 | 72 | then: 73 | result == expect 74 | 75 | where: 76 | selector | expect 77 | 'tags/Name' | 'bob' 78 | 'tags/env' | 'PROD' 79 | 'tags/missing' | null 80 | } 81 | 82 | @Unroll 83 | def "apply selector #selector"() { 84 | given: 85 | def i = mkInstance() 86 | when: 87 | def result = InstanceToNodeMapper.applySelector(i, selector, defVal) 88 | then: 89 | result == expect 90 | 91 | where: 92 | selector | defVal | expect 93 | 'instanceId,publicDnsName' | null | 'aninstanceId' 94 | 'publicDnsName,instanceId' | null | 'aninstanceId' 95 | 'privateIpAddress,instanceId' | null | '127.0.9.9' 96 | 'instanceId,privateIpAddress' | null | 'aninstanceId' 97 | 'publicDnsName,privateDnsName' | null | null 98 | 'publicDnsName,privateDnsName' | 'a default' | 'a default' 99 | } 100 | 101 | def "apply selector multipart"() { 102 | given: 103 | def i = mkInstance() 104 | when: 105 | def result = InstanceToNodeMapper.applySelector(i, selector, defVal) 106 | then: 107 | result == expect 108 | 109 | where: 110 | selector | defVal | expect 111 | 'instanceId+publicDnsName' | null | 'aninstanceId' 112 | 'instanceId+\'_\'+publicDnsName' | null | 'aninstanceId_' 113 | 'instanceId+"_"+publicDnsName' | null | 'aninstanceId_' 114 | 'publicDnsName+instanceId' | null | 'aninstanceId' 115 | 'publicDnsName+\'_\'+instanceId' | null | '_aninstanceId' 116 | 'publicDnsName+"/"+instanceId' | null | '/aninstanceId' 117 | 'privateIpAddress+instanceId' | null | '127.0.9.9aninstanceId' 118 | 'privateIpAddress+\'_\'+instanceId' | null | '127.0.9.9_aninstanceId' 119 | 'privateIpAddress+"::"+instanceId' | null | '127.0.9.9::aninstanceId' 120 | 'instanceId+privateIpAddress' | null | 'aninstanceId127.0.9.9' 121 | 'instanceId+"-"+privateIpAddress' | null | 'aninstanceId-127.0.9.9' 122 | 'publicDnsName+instanceId' | null | 'aninstanceId' 123 | 'publicDnsName+"-"+instanceId' | null | '-aninstanceId' 124 | 'publicDnsName+privateDnsName' | null | null 125 | 'publicDnsName+"_"+privateDnsName' | null | null 126 | 'publicDnsName+privateDnsName' | 'a default' | 'a default' 127 | 'publicDnsName+"_"+privateDnsName' | 'a default' | 'a default' 128 | 'tags/Name+"-"+instanceId' | null | 'bob-aninstanceId' 129 | } 130 | 131 | def "apply selector merged tags"() { 132 | given: 133 | def i = mkInstance() 134 | when: 135 | def result = InstanceToNodeMapper.applySelector(i, selector, defVal, true) 136 | then: 137 | result == expect 138 | 139 | where: 140 | selector | defVal | expect 141 | 'tags/Name|tags/env' | null | 'bob,PROD' 142 | 'publicDnsName|instanceId' | null | 'aninstanceId' 143 | 'privateIpAddress|instanceId' | null | '127.0.9.9,aninstanceId' 144 | 'instanceId|privateIpAddress' | null | 'aninstanceId,127.0.9.9' 145 | 'publicDnsName|instanceId' | null | 'aninstanceId' 146 | 'publicDnsName|privateDnsName' | null | null 147 | 'publicDnsName|privateDnsName' | 'a default' | 'a default' 148 | } 149 | 150 | def "apply selector merged tags multi"() { 151 | given: 152 | def i = mkInstance() 153 | when: 154 | def result = InstanceToNodeMapper.applySelector(i, selector, defVal, true) 155 | then: 156 | result == expect 157 | 158 | where: 159 | selector | defVal | expect 160 | 'instanceId|tags/Name+"_"+tags/env' | null | 'aninstanceId,bob_PROD' 161 | } 162 | 163 | def "extra mapping image"() { 164 | given: 165 | 166 | Instance instance = mkInstance() 167 | Image image = mkImage() 168 | Reservation reservertion = Mock(Reservation){ 169 | getInstances()>>[instance] 170 | } 171 | AmazonEC2Client ec2 = Mock(AmazonEC2Client){ 172 | describeInstances(_) >> Mock(DescribeInstancesResult){ 173 | getReservations() >> [reservertion] 174 | } 175 | describeImages(_) >> Mock(DescribeImagesResult){ 176 | getImages()>>[image] 177 | } 178 | describeAvailabilityZones()>>Mock(DescribeAvailabilityZonesResult){ 179 | getAvailabilityZones()>>[] 180 | } 181 | } 182 | EC2Supplier supplier = Mock(EC2Supplier) { 183 | 0 * getEC2ForDefaultRegion() 184 | 1 * getEC2ForRegion(_) >> ec2 185 | 0 * getEC2ForEndpoint(_) 186 | } 187 | 188 | 189 | int pageResults = 100 190 | Properties mapping = new Properties() 191 | mapping.put("ami_image.selector",mapperValue) 192 | def mapper = new InstanceToNodeMapper(supplier, mapping, pageResults); 193 | mapper.setRegion("us-west-1") 194 | 195 | when: 196 | def instances = mapper.performQuery(false) 197 | then: 198 | instances!=null 199 | instances.getNode("aninstanceId").getAttributes().containsKey(expected) 200 | 201 | where: 202 | mapperValue | expected 203 | 'imageName' | "ami_image" 204 | 'imageId+"-"+imageName' | "ami_image" 205 | } 206 | 207 | def "extra mapping not calling image list"() { 208 | given: 209 | 210 | Instance instance = mkInstance() 211 | Image image = mkImage() 212 | Reservation reservertion = Mock(Reservation){ 213 | getInstances()>>[instance] 214 | } 215 | AmazonEC2Client ec2 = Mock(AmazonEC2Client){ 216 | describeInstances(_) >> Mock(DescribeInstancesResult){ 217 | getReservations() >> [reservertion] 218 | } 219 | describeImages(_) >> Mock(DescribeImagesResult){ 220 | getImages()>>[image] 221 | } 222 | describeAvailabilityZones()>>Mock(DescribeAvailabilityZonesResult){ 223 | getAvailabilityZones()>>[] 224 | } 225 | } 226 | EC2Supplier supplier = Mock(EC2Supplier) { 227 | 0 * getEC2ForDefaultRegion() 228 | 1 * getEC2ForRegion(_) >> ec2 229 | 0 * getEC2ForEndpoint(_) 230 | } 231 | int pageResults = 100 232 | Properties mapping = new Properties() 233 | mapping.put("nodename.selector","instanceId") 234 | def mapper = new InstanceToNodeMapper(supplier, mapping, pageResults); 235 | mapper.setRegion("us-west-1") 236 | when: 237 | def instances = mapper.performQuery(false) 238 | then: 239 | instances!=null 240 | 0*ec2.describeImages(_) 241 | } 242 | 243 | def "region added to the node attributes with region specified"() { 244 | given: 245 | 246 | Instance instance = mkInstance() 247 | Image image = mkImage() 248 | List zones = new ArrayList<>() 249 | AvailabilityZone zone1 = new AvailabilityZone() 250 | zone1.setRegionName(region) 251 | zone1.setZoneName("us-east-1a") 252 | 253 | Reservation reservertion = Mock(Reservation){ 254 | getInstances()>>[instance] 255 | } 256 | AmazonEC2Client ec2 = Mock(AmazonEC2Client){ 257 | describeInstances(_) >> Mock(DescribeInstancesResult){ 258 | getReservations() >> [reservertion] 259 | } 260 | describeImages(_) >> Mock(DescribeImagesResult){ 261 | getImages()>>[image] 262 | } 263 | describeAvailabilityZones()>>Mock(DescribeAvailabilityZonesResult){ 264 | getAvailabilityZones()>>[zone1] 265 | } 266 | } 267 | 268 | EC2Supplier supplier = Mock(EC2Supplier) { 269 | 0 * getEC2ForDefaultRegion() 270 | 1 * getEC2ForRegion(region) >> ec2 271 | 0 * getEC2ForEndpoint(_) 272 | } 273 | 274 | int pageResults = 100 275 | Properties mapping = new Properties() 276 | mapping.put("region.selector","region") 277 | def mapper = new InstanceToNodeMapper(supplier, mapping, pageResults); 278 | mapper.setRegion(region) 279 | when: 280 | def instances = mapper.performQuery(false) 281 | then: 282 | instances!=null 283 | instances.getNode("aninstanceId").getAttributes().containsKey("region") 284 | instances.getNode("aninstanceId").getAttributes().get("region") == region 285 | 286 | where: 287 | region << ['us-east-1','us-west-2'] 288 | } 289 | 290 | def "region added to the node attributes with endpoint(s) specified"() { 291 | given: 292 | 293 | Image image = mkImage() 294 | 295 | EC2Supplier supplier = Mock(EC2Supplier) { 296 | 0 * getEC2ForDefaultRegion() 297 | 0 * getEC2ForRegion() 298 | 299 | _ * getEC2ForEndpoint({ it in endpoints }) >> { args -> 300 | AvailabilityZone zone1 = new AvailabilityZone() 301 | def region=regions[endpoints.indexOf(args[0])] 302 | zone1.setRegionName(region) 303 | zone1.setZoneName("${region}a") 304 | Mock(AmazonEC2Client) { 305 | describeInstances(_) >> Mock(DescribeInstancesResult) { 306 | getReservations() >> [Mock(Reservation){ 307 | getInstances()>>[mkInstance(region)] 308 | }] 309 | } 310 | describeImages(_) >> Mock(DescribeImagesResult) { 311 | getImages() >> [image] 312 | } 313 | describeAvailabilityZones() >> Mock(DescribeAvailabilityZonesResult) { 314 | getAvailabilityZones() >> [zone1] 315 | } 316 | } 317 | } 318 | 319 | 0 * _(*_) 320 | } 321 | 322 | int pageResults = 100 323 | Properties mapping = new Properties() 324 | mapping.put("region.selector","region") 325 | def mapper = new InstanceToNodeMapper(supplier, mapping, pageResults); 326 | mapper.setEndpoint(endpoints.join(', ')) 327 | when: 328 | def instances = mapper.performQuery(false) 329 | then: 330 | instances!=null 331 | instances.getNode("aninstanceId").getAttributes().containsKey("region") 332 | instances.getNode("aninstanceId").getAttributes().get("region") in regions 333 | 334 | where: 335 | endpoints | regions 336 | ['https://ec2.us-east-1.amazonaws.com'] | ['us-east-1'] 337 | ['https://ec2.us-west-2.amazonaws.com'] | ['us-west-2'] 338 | ['https://ec2.us-west-1.amazonaws.com', 'https://ec2.us-east-1.amazonaws.com'] | ['us-west-1','us-east-1'] 339 | } 340 | def "region added to the node attributes with ALL_REGIONS specified"() { 341 | given: 342 | 343 | Image image = mkImage() 344 | 345 | EC2Supplier supplier = Mock(EC2Supplier) { 346 | 1 * getEC2ForDefaultRegion() >> Mock(AmazonEC2Client) { 347 | 1 * describeRegions() >> Mock(DescribeRegionsResult) { 348 | 1 * getRegions() >> regions.collect({ region -> 349 | Mock(Region) { 350 | _ * getRegionName() >> region 351 | _ * getEndpoint() >> "https://ec2.${region}.amazonaws.com" 352 | } 353 | }) 354 | } 355 | } 356 | 0 * getEC2ForRegion() 357 | 358 | _ * getEC2ForEndpoint({ it in endpointsFound }) >> { args -> 359 | AvailabilityZone zone1 = new AvailabilityZone() 360 | def region=regions[endpointsFound.indexOf(args[0])] 361 | zone1.setRegionName(region) 362 | zone1.setZoneName("${region}a") 363 | Mock(AmazonEC2Client) { 364 | describeInstances(_) >> Mock(DescribeInstancesResult) { 365 | getReservations() >> [Mock(Reservation){ 366 | getInstances()>>[mkInstance(region)] 367 | }] 368 | } 369 | describeImages(_) >> Mock(DescribeImagesResult) { 370 | getImages() >> [image] 371 | } 372 | describeAvailabilityZones() >> Mock(DescribeAvailabilityZonesResult) { 373 | getAvailabilityZones() >> [zone1] 374 | } 375 | } 376 | } 377 | 378 | 0 * _(*_) 379 | } 380 | 381 | int pageResults = 100 382 | Properties mapping = new Properties() 383 | mapping.put("region.selector","region") 384 | def mapper = new InstanceToNodeMapper(supplier, mapping, pageResults); 385 | mapper.setEndpoint(endpoint) 386 | when: 387 | def instances = mapper.performQuery(false) 388 | then: 389 | instances!=null 390 | instances.getNode("aninstanceId").getAttributes().containsKey("region") 391 | instances.getNode("aninstanceId").getAttributes().get("region") in regions 392 | 393 | where: 394 | endpoint | endpointsFound|regions 395 | 'ALL_REGIONS' | ['https://ec2.us-west-1.amazonaws.com', 'https://ec2.us-east-1.amazonaws.com']|['us-west-1', 'us-east-1'] 396 | 397 | } 398 | 399 | // 400 | // Private Methods 401 | // 402 | private static Instance mkInstance(String region='us-east-1') { 403 | Instance i = new Instance() 404 | i.withTags(new Tag('Name', 'bob'), new Tag('env', 'PROD')) 405 | i.setInstanceId("aninstanceId") 406 | i.setArchitecture("anarch") 407 | i.setImageId("ami-something") 408 | i.setPlacement(new Placement("${region}a")) 409 | 410 | def state = new InstanceState() 411 | state.setName(InstanceStateName.Running) 412 | i.setState(state) 413 | i.setPrivateIpAddress('127.0.9.9') 414 | return i 415 | } 416 | 417 | private static Image mkImage(){ 418 | Image image = new Image() 419 | image.setImageId("ami-something") 420 | image.setName("AMISomething") 421 | return image 422 | } 423 | } 424 | --------------------------------------------------------------------------------