├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── IMDG-snapshot.yml │ ├── pr-builder.yml │ └── publish-snapshot.yml ├── .gitignore ├── LICENSE ├── README.md ├── checkstyle ├── ClassHeader.txt ├── checkstyle.xml └── suppressions.xml ├── findbugs └── findbugs-exclude.xml ├── pom.xml ├── rbac.yaml └── src ├── main ├── java │ └── com │ │ └── hazelcast │ │ └── kubernetes │ │ ├── DnsEndpointResolver.java │ │ ├── HazelcastKubernetesDiscoveryStrategy.java │ │ ├── HazelcastKubernetesDiscoveryStrategyFactory.java │ │ ├── KubernetesApiEndpointResolver.java │ │ ├── KubernetesClient.java │ │ ├── KubernetesClientException.java │ │ ├── KubernetesConfig.java │ │ ├── KubernetesProperties.java │ │ ├── RestClient.java │ │ ├── RestClientException.java │ │ ├── RetryUtils.java │ │ └── package-info.java └── resources │ └── META-INF │ └── services │ └── com.hazelcast.spi.discovery.DiscoveryStrategyFactory └── test ├── java └── com │ └── hazelcast │ └── kubernetes │ ├── DnsEndpointResolverTest.java │ ├── HazelcastKubernetesDiscoveryStrategyFactoryTest.java │ ├── KubernetesApiEndpointResolverTest.java │ ├── KubernetesClientTest.java │ ├── KubernetesConfigTest.java │ ├── KubernetesPropertiesTest.java │ ├── RestClientTest.java │ └── RetryUtilsTest.java └── resources ├── ca.crt └── keystore.jks /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @cloud-native 2 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "maven" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | time: "02:00" 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | time: "02:00" -------------------------------------------------------------------------------- /.github/workflows/IMDG-snapshot.yml: -------------------------------------------------------------------------------- 1 | name: IMDG-Snapshot 2 | on: 3 | schedule: 4 | - cron: '0 3 * * *' 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | java: [ '8' ] 12 | architecture: [ 'x64' ] 13 | hazelcast: [ '5.0-SNAPSHOT' ] 14 | name: Build against IMDG ${{ matrix.hazelcast }} with JDK ${{ matrix.java }} on ${{ matrix.architecture }} 15 | steps: 16 | - uses: actions/checkout@v2.3.4 17 | - name: Setup JDK 18 | uses: actions/setup-java@v1 19 | with: 20 | java-version: ${{ matrix.java }} 21 | architecture: ${{ matrix.architecture }} 22 | 23 | - uses: actions/cache@v2.1.6 24 | with: 25 | path: ~/.m2/repository 26 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 27 | restore-keys: ${{ runner.os }}-maven- 28 | 29 | - name: Build with Maven 30 | run: mvn -Dhazelcast.version=${{ matrix.hazelcast }} -U verify 31 | -------------------------------------------------------------------------------- /.github/workflows/pr-builder.yml: -------------------------------------------------------------------------------- 1 | name: Pull-Request 2 | on: 3 | pull_request: 4 | branches: 5 | - master 6 | - 1.5.x 7 | paths-ignore: 8 | - '**.md' 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | java: [ '8' ] 16 | architecture: [ 'x64' ] 17 | name: Build with JDK ${{ matrix.java }} on ${{ matrix.architecture }} 18 | steps: 19 | - uses: actions/checkout@v2.3.4 20 | - name: Setup JDK 21 | uses: actions/setup-java@v1 22 | with: 23 | java-version: ${{ matrix.java }} 24 | architecture: ${{ matrix.architecture }} 25 | 26 | - uses: actions/cache@v2.1.6 27 | with: 28 | path: ~/.m2/repository 29 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 30 | restore-keys: ${{ runner.os }}-maven- 31 | 32 | - name: Build with Maven 33 | run: mvn -B verify 34 | -------------------------------------------------------------------------------- /.github/workflows/publish-snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Publish-Snapshot 2 | on: 3 | push: 4 | branches: master 5 | paths-ignore: 6 | - '**.md' 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | java: [ '8' ] 14 | architecture: [ 'x64' ] 15 | name: Build with JDK ${{ matrix.java }} on ${{ matrix.architecture }} 16 | steps: 17 | - uses: actions/checkout@v2.3.4 18 | - name: Setup JDK 19 | uses: actions/setup-java@v1 20 | with: 21 | java-version: ${{ matrix.java }} 22 | architecture: ${{ matrix.architecture }} 23 | 24 | - uses: actions/cache@v2.1.6 25 | with: 26 | path: ~/.m2/repository 27 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 28 | restore-keys: ${{ runner.os }}-maven- 29 | 30 | - name: Build with Maven 31 | run: mvn -B verify 32 | publish: 33 | needs: build 34 | runs-on: ubuntu-latest 35 | steps: 36 | - uses: actions/checkout@v2 37 | - name: Set up Maven Central Repository 38 | uses: actions/setup-java@v1 39 | with: 40 | java-version: 1.8 41 | server-id: snapshot-repository 42 | server-username: MAVEN_CENTRAL_USERNAME 43 | server-password: MAVEN_CENTRAL_PASSWORD 44 | - name: Publish Snapshot JARs 45 | run: mvn -B deploy -Prelease-snapshot -DskipTests 46 | env: 47 | MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} 48 | MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | build/ 3 | .gradle/ 4 | .project 5 | .classpath 6 | .settings/ 7 | .idea/ 8 | .patch 9 | .diff 10 | *.iml 11 | *.ipr 12 | *.iws 13 | .DS_Store 14 | atlassian-ide-plugin.xml 15 | .checkstyle 16 | .fbExcludeFilterFile 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### **DEPRECATED:** `hazelcast-kubernetes` plugin has been merged with [`hazelcast`](https://github.com/hazelcast/hazelcast)! 2 | Since version `5.0` `hazelcast` includes `hazelcast-kubernetes` and does not require additional dependency. For details about running Hazelcast on Kubernetes consider the [documentation](https://docs.hazelcast.com/hazelcast/latest/kubernetes/deploying-in-kubernetes). 3 | 4 | -------------------------------------------------------------------------------- /checkstyle/ClassHeader.txt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * 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 | -------------------------------------------------------------------------------- /checkstyle/checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 21 | 22 | 46 | 47 | 48 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 320 | 321 | 322 | 323 | -------------------------------------------------------------------------------- /checkstyle/suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 18 | 19 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /findbugs/findbugs-exclude.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 16 | 4.0.0 17 | 18 | com.hazelcast 19 | hazelcast-kubernetes 20 | 2.2.4-SNAPSHOT 21 | Kubernetes Discovery Plugin for Hazelcast 22 | Kubernetes Service Discovery for Hazelcast Discovery SPI 23 | http://github.com/hazelcast/hazelcast-kubernetes 24 | 2015 25 | bundle 26 | 27 | 28 | 29 | ${project.basedir} 30 | UTF-8 31 | ${maven.build.timestamp} 32 | yyyy-MM-dd HH:mm 33 | 34 | 1.2.17 35 | 4.13.2 36 | 2.27.2 37 | 2.2 38 | 3.12.4 39 | 2.0.9 40 | 3.0.1u2 41 | 42 | 4.2.2 43 | 44 | 3.1.2 45 | 3.0.5 46 | 2.22.2 47 | 3.3.0.603 48 | 0.8.7 49 | 3.3.1 50 | 3.0.1 51 | 5.1.2 52 | 3.8.1 53 | 2.5.3 54 | 55 | 56 | 57 | 58 | release-repository 59 | https://oss.sonatype.org/service/local/staging/deploy/maven2 60 | 61 | 62 | snapshot-repository 63 | Maven2 Snapshot Repository 64 | https://oss.sonatype.org/content/repositories/snapshots 65 | false 66 | 67 | 68 | 69 | 70 | 71 | snapshot-repository 72 | Maven2 Snapshot Repository 73 | https://oss.sonatype.org/content/repositories/snapshots 74 | 75 | 76 | 77 | 78 | scm:git:git://github.com/hazelcast/hazelcast-kubernetes.git 79 | scm:git:git@github.com:hazelcast/hazelcast-kubernetes.git 80 | https://github.com/hazelcast/hazelcast-kubernetes/ 81 | HEAD 82 | 83 | 84 | 85 | 86 | cengelbert 87 | Christoph Engelbert (@noctarius2k) 88 | noctarius@apache.org 89 | +1 90 | 91 | 92 | 93 | 94 | 95 | APACHE LICENSE 2.0 96 | http://www.apache.org/licenses/LICENSE-2.0 97 | 98 | 99 | 100 | 101 | github 102 | https://github.com/hazelcast/hazelcast-kubernetes/issues 103 | 104 | 105 | 106 | Jenkins 107 | https://hazelcast-l337.ci.cloudbees.com/job/Kubernetes-pr-builder/ 108 | 109 | 110 | 111 | 112 | com.hazelcast 113 | hazelcast 114 | ${hazelcast.version} 115 | provided 116 | 117 | 118 | com.google.code.findbugs 119 | annotations 120 | ${findbugs.annotations.version} 121 | provided 122 | 123 | 124 | com.hazelcast 125 | hazelcast 126 | test 127 | ${hazelcast.version} 128 | tests 129 | 130 | 131 | junit 132 | junit 133 | ${junit.version} 134 | test 135 | true 136 | 137 | 138 | com.github.tomakehurst 139 | wiremock 140 | ${wiremock.version} 141 | test 142 | 143 | 144 | org.hamcrest 145 | hamcrest 146 | ${hamcrest.version} 147 | test 148 | 149 | 150 | log4j 151 | log4j 152 | ${log4j.version} 153 | test 154 | true 155 | 156 | 157 | org.mockito 158 | mockito-core 159 | ${mockito.version} 160 | test 161 | 162 | 163 | org.powermock 164 | powermock-api-mockito2 165 | ${powermock.version} 166 | test 167 | 168 | 169 | org.powermock 170 | powermock-module-junit4 171 | ${powermock.version} 172 | test 173 | 174 | 175 | 176 | 177 | 178 | 179 | org.apache.felix 180 | maven-bundle-plugin 181 | ${maven.bundle.plugin.version} 182 | true 183 | 184 | 185 | 186 | com.hazelcast.kubernetes.* 187 | 188 | 189 | com.hazelcast.config,com.hazelcast.config.properties, 190 | com.hazelcast.spi.discovery,com.hazelcast.core, 191 | com.hazelcast.logging,com.hazelcast.nio,com.hazelcast.internal.nio, com.hazelcast.internal.util, 192 | com.hazelcast.internal.json,javax.net.ssl,javax.security.auth.x500, 193 | javax.naming, javax.naming.directory, com.hazelcast.spi.partitiongroup 194 | 195 | 196 | 197 | 198 | 199 | 200 | org.apache.maven.plugins 201 | maven-compiler-plugin 202 | ${maven.compiler.plugin.version} 203 | 204 | 1.8 205 | 1.8 206 | 207 | 208 | 209 | org.apache.maven.plugins 210 | maven-source-plugin 211 | 212 | 213 | attach-sources 214 | 215 | jar 216 | 217 | 218 | 219 | 220 | 221 | org.apache.maven.plugins 222 | maven-javadoc-plugin 223 | ${maven.javadoc.plugin.version} 224 | 225 | 226 | attach-javadocs 227 | 228 | jar 229 | 230 | 231 | 8 232 | public 233 | 234 | com/hazelcast/kubernetes/HazelcastKubernetesDiscoveryStrategyFactory.java 235 | 236 | 237 | com/hazelcast/kubernetes/*.java 238 | 239 | true 240 | 241 | http://docs.hazelcast.org/docs/3.7/javadoc/ 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | org.apache.maven.plugins 251 | maven-checkstyle-plugin 252 | ${maven.checkstyle.plugin.version} 253 | 254 | 255 | validate 256 | 257 | checkstyle 258 | 259 | 260 | 261 | 262 | ${main.basedir}/checkstyle/checkstyle.xml 263 | ${main.basedir}/checkstyle/suppressions.xml 264 | ${main.basedir}/checkstyle/ClassHeader.txt 265 | false 266 | true 267 | true 268 | true 269 | true 270 | false 271 | true 272 | main.basedir=${main.basedir} 273 | 274 | 275 | 276 | 277 | org.codehaus.mojo 278 | findbugs-maven-plugin 279 | ${maven.findbugs.plugin.version} 280 | 281 | 282 | compile 283 | 284 | check 285 | 286 | 287 | 288 | 289 | true 290 | ${main.basedir}/findbugs/findbugs-exclude.xml 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | org.apache.maven.plugins 301 | maven-javadoc-plugin 302 | 303 | 8 304 | public 305 | 306 | com/hazelcast/kubernetes/HazelcastKubernetesDiscoveryStrategyFactory.java 307 | 308 | 309 | com/hazelcast/kubernetes/*.java 310 | 311 | true 312 | 313 | http://docs.hazelcast.org/docs/3.7/javadoc/ 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | test-coverage 323 | 324 | 325 | -Xms128m -Xmx1G -XX:MaxPermSize=128M 326 | -Dhazelcast.version.check.enabled=false 327 | -Dhazelcast.mancenter.enabled=false 328 | -Dhazelcast.logging.type=none 329 | -Dhazelcast.test.use.network=false 330 | 331 | 332 | 333 | 334 | 335 | org.jacoco 336 | jacoco-maven-plugin 337 | ${maven.jacoco.plugin.version} 338 | 339 | 340 | 341 | org.apache.maven.plugins 342 | maven-surefire-plugin 343 | ${maven.surefire.plugin.version} 344 | 345 | true 346 | true 347 | 348 | 349 | 350 | 351 | org.codehaus.mojo 352 | sonar-maven-plugin 353 | ${maven.sonar.plugin.version} 354 | 355 | 356 | 357 | 358 | 359 | release 360 | 361 | true 362 | 363 | 364 | 365 | 366 | org.apache.maven.plugins 367 | maven-gpg-plugin 368 | ${maven.gpg.plugin.version} 369 | 370 | 371 | sign-artifacts 372 | verify 373 | 374 | sign 375 | 376 | 377 | 378 | 379 | 380 | 381 | org.apache.maven.plugins 382 | maven-javadoc-plugin 383 | ${maven.javadoc.plugin.version} 384 | 385 | 8 386 | 387 | 388 | api_1.6 389 | http://download.oracle.com/javase/1.6.0/docs/api/ 390 | 391 | 392 | api_1.7 393 | http://download.oracle.com/javase/1.7.0/docs/api/ 394 | 395 | 396 | 1024 397 | 398 | 399 | 400 | attach-javadocs 401 | 402 | jar 403 | 404 | 405 | 406 | 407 | 408 | 409 | org.sonatype.plugins 410 | nexus-staging-maven-plugin 411 | 1.6.8 412 | true 413 | 414 | release-repository 415 | https://oss.sonatype.org/ 416 | true 417 | 418 | 419 | 420 | 421 | 422 | 423 | release-snapshot 424 | 425 | true 426 | 427 | 428 | 429 | 430 | org.apache.maven.plugins 431 | maven-javadoc-plugin 432 | ${maven.javadoc.plugin.version} 433 | 434 | 8 435 | 436 | 437 | api_1.6 438 | http://download.oracle.com/javase/1.6.0/docs/api/ 439 | 440 | 441 | api_1.7 442 | http://download.oracle.com/javase/1.7.0/docs/api/ 443 | 444 | 445 | 446 | *.impl:*.internal:*.operations:*.proxy:*.util:com.hazelcast.aws.security: 447 | *.handlermigration:*.client.connection.nio:*.client.console:*.buildutils: 448 | *.client.protocol.generator:*.cluster.client:*.concurrent:*.collection: 449 | *.nio.ascii:*.nio.ssl:*.nio.tcp:*.partition.client:*.transaction.client: 450 | *.core.server:com.hazelcast.instance:com.hazelcast.PlaceHolder 451 | 452 | 453 | 454 | 455 | attach-javadocs 456 | 457 | jar 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | release-sign-artifacts 467 | 468 | 469 | performRelease 470 | true 471 | 472 | 473 | 474 | 475 | 476 | org.apache.maven.plugins 477 | maven-release-plugin 478 | ${maven.release.plugin.version} 479 | 480 | false 481 | false 482 | 483 | 484 | 485 | org.apache.maven.plugins 486 | maven-gpg-plugin 487 | 3.0.1 488 | 489 | 490 | sign-artifacts 491 | verify 492 | 493 | sign 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | -------------------------------------------------------------------------------- /rbac.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: hazelcast-cluster-role 5 | rules: 6 | - apiGroups: 7 | - "" 8 | resources: 9 | - endpoints 10 | - pods 11 | - nodes 12 | - services 13 | verbs: 14 | - get 15 | - list 16 | 17 | --- 18 | 19 | apiVersion: rbac.authorization.k8s.io/v1 20 | kind: ClusterRoleBinding 21 | metadata: 22 | name: hazelcast-cluster-role-binding 23 | roleRef: 24 | apiGroup: rbac.authorization.k8s.io 25 | kind: ClusterRole 26 | name: hazelcast-cluster-role 27 | subjects: 28 | - kind: ServiceAccount 29 | name: default 30 | namespace: default 31 | -------------------------------------------------------------------------------- /src/main/java/com/hazelcast/kubernetes/DnsEndpointResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * 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 | package com.hazelcast.kubernetes; 18 | 19 | import com.hazelcast.config.NetworkConfig; 20 | import com.hazelcast.logging.ILogger; 21 | import com.hazelcast.cluster.Address; 22 | import com.hazelcast.spi.discovery.DiscoveryNode; 23 | import com.hazelcast.spi.discovery.SimpleDiscoveryNode; 24 | 25 | import java.net.InetAddress; 26 | import java.net.UnknownHostException; 27 | import java.util.ArrayList; 28 | import java.util.Collections; 29 | import java.util.HashSet; 30 | import java.util.List; 31 | import java.util.Set; 32 | import java.util.concurrent.Callable; 33 | import java.util.concurrent.ExecutionException; 34 | import java.util.concurrent.ExecutorService; 35 | import java.util.concurrent.Executors; 36 | import java.util.concurrent.Future; 37 | import java.util.concurrent.TimeUnit; 38 | import java.util.concurrent.TimeoutException; 39 | 40 | final class DnsEndpointResolver 41 | extends HazelcastKubernetesDiscoveryStrategy.EndpointResolver { 42 | // executor service for dns lookup calls 43 | private static final ExecutorService DNS_LOOKUP_SERVICE = Executors.newCachedThreadPool(); 44 | 45 | private final String serviceDns; 46 | private final int port; 47 | private final int serviceDnsTimeout; 48 | 49 | DnsEndpointResolver(ILogger logger, String serviceDns, int port, int serviceDnsTimeout) { 50 | super(logger); 51 | this.serviceDns = serviceDns; 52 | this.port = port; 53 | this.serviceDnsTimeout = serviceDnsTimeout; 54 | } 55 | 56 | List resolve() { 57 | try { 58 | return lookup(); 59 | } catch (TimeoutException e) { 60 | logger.warning(String.format("DNS lookup for serviceDns '%s' failed: DNS resolution timeout", serviceDns)); 61 | return Collections.emptyList(); 62 | } catch (UnknownHostException e) { 63 | logger.warning(String.format("DNS lookup for serviceDns '%s' failed: unknown host", serviceDns)); 64 | return Collections.emptyList(); 65 | } catch (Exception e) { 66 | logger.warning(String.format("DNS lookup for serviceDns '%s' failed", serviceDns), e); 67 | return Collections.emptyList(); 68 | } 69 | } 70 | 71 | private List lookup() 72 | throws UnknownHostException, InterruptedException, ExecutionException, TimeoutException { 73 | Set addresses = new HashSet(); 74 | 75 | Future future = DNS_LOOKUP_SERVICE.submit(new Callable() { 76 | @Override 77 | public InetAddress[] call() throws Exception { 78 | return getAllInetAddresses(); 79 | } 80 | }); 81 | 82 | try { 83 | for (InetAddress address : future.get(serviceDnsTimeout, TimeUnit.SECONDS)) { 84 | if (addresses.add(address.getHostAddress()) && logger.isFinestEnabled()) { 85 | logger.finest("Found node service with address: " + address); 86 | } 87 | } 88 | } catch (ExecutionException e) { 89 | if (e.getCause() instanceof UnknownHostException) { 90 | throw (UnknownHostException) e.getCause(); 91 | } else { 92 | throw e; 93 | } 94 | } catch (TimeoutException e) { 95 | // cancel DNS lookup 96 | future.cancel(true); 97 | throw e; 98 | } 99 | 100 | if (addresses.size() == 0) { 101 | logger.warning("Could not find any service for serviceDns '" + serviceDns + "'"); 102 | return Collections.emptyList(); 103 | } 104 | 105 | List result = new ArrayList(); 106 | for (String address : addresses) { 107 | result.add(new SimpleDiscoveryNode(new Address(address, getHazelcastPort(port)))); 108 | } 109 | return result; 110 | } 111 | 112 | /** 113 | * Do the actual lookup 114 | * @return array of resolved inet addresses 115 | * @throws UnknownHostException 116 | */ 117 | private InetAddress[] getAllInetAddresses() throws UnknownHostException { 118 | return InetAddress.getAllByName(serviceDns); 119 | } 120 | 121 | private static int getHazelcastPort(int port) { 122 | if (port > 0) { 123 | return port; 124 | } 125 | return NetworkConfig.DEFAULT_PORT; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/com/hazelcast/kubernetes/HazelcastKubernetesDiscoveryStrategy.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * 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 | package com.hazelcast.kubernetes; 18 | 19 | import com.hazelcast.kubernetes.KubernetesConfig.DiscoveryMode; 20 | import com.hazelcast.logging.ILogger; 21 | import com.hazelcast.spi.discovery.AbstractDiscoveryStrategy; 22 | import com.hazelcast.spi.discovery.DiscoveryNode; 23 | import com.hazelcast.spi.partitiongroup.PartitionGroupMetaData; 24 | 25 | import java.net.InetAddress; 26 | import java.net.UnknownHostException; 27 | import java.util.HashMap; 28 | import java.util.List; 29 | import java.util.Map; 30 | 31 | final class HazelcastKubernetesDiscoveryStrategy 32 | extends AbstractDiscoveryStrategy { 33 | private final KubernetesClient client; 34 | private final EndpointResolver endpointResolver; 35 | private KubernetesConfig config; 36 | 37 | private final Map memberMetadata = new HashMap(); 38 | 39 | HazelcastKubernetesDiscoveryStrategy(ILogger logger, Map properties) { 40 | super(logger, properties); 41 | 42 | config = new KubernetesConfig(properties); 43 | logger.info(config.toString()); 44 | 45 | client = buildKubernetesClient(config); 46 | 47 | if (DiscoveryMode.DNS_LOOKUP.equals(config.getMode())) { 48 | endpointResolver = new DnsEndpointResolver(logger, config.getServiceDns(), config.getServicePort(), 49 | config.getServiceDnsTimeout()); 50 | } else { 51 | endpointResolver = new KubernetesApiEndpointResolver(logger, config.getServiceName(), config.getServicePort(), 52 | config.getServiceLabelName(), config.getServiceLabelValue(), 53 | config.getPodLabelName(), config.getPodLabelValue(), 54 | config.isResolveNotReadyAddresses(), client); 55 | } 56 | 57 | logger.info("Kubernetes Discovery activated with mode: " + config.getMode().name()); 58 | } 59 | 60 | private static KubernetesClient buildKubernetesClient(KubernetesConfig config) { 61 | return new KubernetesClient(config.getNamespace(), config.getKubernetesMasterUrl(), config.getKubernetesApiToken(), 62 | config.getKubernetesCaCertificate(), config.getKubernetesApiRetries(), config.isUseNodeNameAsExternalAddress()); 63 | } 64 | 65 | public void start() { 66 | endpointResolver.start(); 67 | } 68 | 69 | @Override 70 | public Map discoverLocalMetadata() { 71 | if (memberMetadata.isEmpty()) { 72 | memberMetadata.put(PartitionGroupMetaData.PARTITION_GROUP_ZONE, discoverZone()); 73 | memberMetadata.put("hazelcast.partition.group.node", discoverNodeName()); 74 | } 75 | return memberMetadata; 76 | } 77 | 78 | /** 79 | * Discovers the availability zone in which the current Hazelcast member is running. 80 | *

81 | * Note: ZONE_AWARE is available only for the Kubernetes API Mode. 82 | */ 83 | private String discoverZone() { 84 | if (DiscoveryMode.KUBERNETES_API.equals(config.getMode())) { 85 | try { 86 | String zone = client.zone(podName()); 87 | if (zone != null) { 88 | getLogger().info(String.format("Kubernetes plugin discovered availability zone: %s", zone)); 89 | return zone; 90 | } 91 | } catch (Exception e) { 92 | // only log the exception and the message, Hazelcast should still start 93 | getLogger().finest(e); 94 | } 95 | getLogger().info("Cannot fetch the current zone, ZONE_AWARE feature is disabled"); 96 | } 97 | return "unknown"; 98 | } 99 | 100 | /** 101 | * Discovers the name of the node which the current Hazelcast member pod is running on. 102 | *

103 | * Note: NODE_AWARE is available only for the Kubernetes API Mode. 104 | */ 105 | private String discoverNodeName() { 106 | if (DiscoveryMode.KUBERNETES_API.equals(config.getMode())) { 107 | try { 108 | String nodeName = client.nodeName(podName()); 109 | if (nodeName != null) { 110 | getLogger().info(String.format("Kubernetes plugin discovered node name: %s", nodeName)); 111 | return nodeName; 112 | } 113 | } catch (Exception e) { 114 | // only log the exception and the message, Hazelcast should still start 115 | getLogger().finest(e); 116 | } 117 | getLogger().warning("Cannot fetch name of the node, NODE_AWARE feature is disabled"); 118 | } 119 | return "unknown"; 120 | } 121 | 122 | private String podName() throws UnknownHostException { 123 | String podName = System.getenv("POD_NAME"); 124 | if (podName == null) { 125 | podName = System.getenv("HOSTNAME"); 126 | } 127 | if (podName == null) { 128 | podName = InetAddress.getLocalHost().getHostName(); 129 | } 130 | return podName; 131 | } 132 | 133 | @Override 134 | public Iterable discoverNodes() { 135 | return endpointResolver.resolve(); 136 | } 137 | 138 | public void destroy() { 139 | endpointResolver.destroy(); 140 | } 141 | 142 | abstract static class EndpointResolver { 143 | protected final ILogger logger; 144 | 145 | EndpointResolver(ILogger logger) { 146 | this.logger = logger; 147 | } 148 | 149 | abstract List resolve(); 150 | 151 | void start() { 152 | } 153 | 154 | void destroy() { 155 | } 156 | 157 | protected InetAddress mapAddress(String address) { 158 | if (address == null) { 159 | return null; 160 | } 161 | try { 162 | return InetAddress.getByName(address); 163 | } catch (UnknownHostException e) { 164 | logger.warning("Address '" + address + "' could not be resolved"); 165 | } 166 | return null; 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/main/java/com/hazelcast/kubernetes/HazelcastKubernetesDiscoveryStrategyFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * 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 | package com.hazelcast.kubernetes; 18 | 19 | import com.hazelcast.config.properties.PropertyDefinition; 20 | import com.hazelcast.logging.ILogger; 21 | import com.hazelcast.logging.Logger; 22 | import com.hazelcast.spi.discovery.DiscoveryNode; 23 | import com.hazelcast.spi.discovery.DiscoveryStrategy; 24 | import com.hazelcast.spi.discovery.DiscoveryStrategyFactory; 25 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 26 | 27 | import java.io.File; 28 | import java.net.InetAddress; 29 | import java.net.UnknownHostException; 30 | import java.util.Arrays; 31 | import java.util.Collection; 32 | import java.util.Collections; 33 | import java.util.Map; 34 | 35 | /** 36 | * Just the factory to create the Kubernetes Discovery Strategy 37 | */ 38 | public class HazelcastKubernetesDiscoveryStrategyFactory 39 | implements DiscoveryStrategyFactory { 40 | private static final Collection PROPERTY_DEFINITIONS; 41 | 42 | static { 43 | PROPERTY_DEFINITIONS = Collections.unmodifiableCollection(Arrays.asList( 44 | KubernetesProperties.SERVICE_DNS, 45 | KubernetesProperties.SERVICE_DNS_TIMEOUT, 46 | KubernetesProperties.SERVICE_NAME, 47 | KubernetesProperties.SERVICE_LABEL_NAME, 48 | KubernetesProperties.SERVICE_LABEL_VALUE, 49 | KubernetesProperties.NAMESPACE, 50 | KubernetesProperties.POD_LABEL_NAME, 51 | KubernetesProperties.POD_LABEL_VALUE, 52 | KubernetesProperties.RESOLVE_NOT_READY_ADDRESSES, 53 | KubernetesProperties.USE_NODE_NAME_AS_EXTERNAL_ADDRESS, 54 | KubernetesProperties.KUBERNETES_API_RETIRES, 55 | KubernetesProperties.KUBERNETES_MASTER_URL, 56 | KubernetesProperties.KUBERNETES_API_TOKEN, 57 | KubernetesProperties.KUBERNETES_CA_CERTIFICATE, 58 | KubernetesProperties.SERVICE_PORT)); 59 | } 60 | 61 | public Class getDiscoveryStrategyType() { 62 | return HazelcastKubernetesDiscoveryStrategy.class; 63 | } 64 | 65 | public DiscoveryStrategy newDiscoveryStrategy(DiscoveryNode discoveryNode, ILogger logger, 66 | Map properties) { 67 | 68 | return new HazelcastKubernetesDiscoveryStrategy(logger, properties); 69 | } 70 | 71 | public Collection getConfigurationProperties() { 72 | return PROPERTY_DEFINITIONS; 73 | } 74 | 75 | /** 76 | * In all Kubernetes environments the file "/var/run/secrets/kubernetes.io/serviceaccount/token" is injected into the 77 | * container. That is why we can use it to verify if this code is run in the Kubernetes environment. 78 | *

79 | * Note that if the Kubernetes environment is not configured correctly, this file my not exist. However, in such case, 80 | * this plugin won't work anyway, so it makes perfect sense to return {@code false}. 81 | * 82 | * @return true if running in the Kubernetes environment 83 | */ 84 | @Override 85 | public boolean isAutoDetectionApplicable() { 86 | return tokenFileExists() && defaultKubernetesMasterReachable(); 87 | } 88 | 89 | @SuppressFBWarnings("DMI_HARDCODED_ABSOLUTE_FILENAME") 90 | boolean tokenFileExists() { 91 | return new File("/var/run/secrets/kubernetes.io/serviceaccount/token").exists(); 92 | } 93 | 94 | private boolean defaultKubernetesMasterReachable() { 95 | try { 96 | InetAddress.getByName("kubernetes.default.svc"); 97 | return true; 98 | } catch (UnknownHostException e) { 99 | ILogger logger = Logger.getLogger(HazelcastKubernetesDiscoveryStrategyFactory.class); 100 | logger.warning("Hazelcast running on Kubernetes, but \"kubernetes.default.svc\" is not reachable. " 101 | + "Check your Kubernetes DNS configuration."); 102 | logger.finest(e); 103 | return false; 104 | } 105 | } 106 | 107 | @Override 108 | public DiscoveryStrategyLevel discoveryStrategyLevel() { 109 | return DiscoveryStrategyLevel.PLATFORM; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/com/hazelcast/kubernetes/KubernetesApiEndpointResolver.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * 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 | package com.hazelcast.kubernetes; 18 | 19 | import com.hazelcast.config.NetworkConfig; 20 | import com.hazelcast.kubernetes.KubernetesClient.Endpoint; 21 | import com.hazelcast.logging.ILogger; 22 | import com.hazelcast.cluster.Address; 23 | import com.hazelcast.spi.discovery.DiscoveryNode; 24 | import com.hazelcast.spi.discovery.SimpleDiscoveryNode; 25 | 26 | import java.net.InetAddress; 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | 30 | class KubernetesApiEndpointResolver 31 | extends HazelcastKubernetesDiscoveryStrategy.EndpointResolver { 32 | 33 | private final String serviceName; 34 | private final String serviceLabel; 35 | private final String serviceLabelValue; 36 | private final String podLabel; 37 | private final String podLabelValue; 38 | private final Boolean resolveNotReadyAddresses; 39 | private final int port; 40 | private final KubernetesClient client; 41 | 42 | KubernetesApiEndpointResolver(ILogger logger, String serviceName, int port, 43 | String serviceLabel, String serviceLabelValue, String podLabel, String podLabelValue, 44 | Boolean resolveNotReadyAddresses, KubernetesClient client) { 45 | 46 | super(logger); 47 | 48 | this.serviceName = serviceName; 49 | this.port = port; 50 | this.serviceLabel = serviceLabel; 51 | this.serviceLabelValue = serviceLabelValue; 52 | this.podLabel = podLabel; 53 | this.podLabelValue = podLabelValue; 54 | this.resolveNotReadyAddresses = resolveNotReadyAddresses; 55 | this.client = client; 56 | } 57 | 58 | @Override 59 | List resolve() { 60 | if (serviceName != null && !serviceName.isEmpty()) { 61 | logger.fine("Using service name to discover nodes."); 62 | return getSimpleDiscoveryNodes(client.endpointsByName(serviceName)); 63 | } else if (serviceLabel != null && !serviceLabel.isEmpty()) { 64 | logger.fine("Using service label to discover nodes."); 65 | return getSimpleDiscoveryNodes(client.endpointsByServiceLabel(serviceLabel, serviceLabelValue)); 66 | } else if (podLabel != null && !podLabel.isEmpty()) { 67 | logger.fine("Using pod label to discover nodes."); 68 | return getSimpleDiscoveryNodes(client.endpointsByPodLabel(podLabel, podLabelValue)); 69 | } 70 | return getSimpleDiscoveryNodes(client.endpoints()); 71 | } 72 | 73 | private List getSimpleDiscoveryNodes(List endpoints) { 74 | List discoveredNodes = new ArrayList(); 75 | for (Endpoint address : endpoints) { 76 | addAddress(discoveredNodes, address); 77 | } 78 | return discoveredNodes; 79 | } 80 | 81 | private void addAddress(List discoveredNodes, Endpoint endpoint) { 82 | if (Boolean.TRUE.equals(resolveNotReadyAddresses) || endpoint.isReady()) { 83 | Address privateAddress = createAddress(endpoint.getPrivateAddress()); 84 | Address publicAddress = createAddress(endpoint.getPublicAddress()); 85 | discoveredNodes 86 | .add(new SimpleDiscoveryNode(privateAddress, publicAddress, endpoint.getAdditionalProperties())); 87 | if (logger.isFinestEnabled()) { 88 | logger.finest(String.format("Found node service with addresses (private, public): %s, %s ", privateAddress, 89 | publicAddress)); 90 | } 91 | } 92 | } 93 | 94 | private Address createAddress(KubernetesClient.EndpointAddress address) { 95 | if (address == null) { 96 | return null; 97 | } 98 | String ip = address.getIp(); 99 | InetAddress inetAddress = mapAddress(ip); 100 | int port = port(address); 101 | return new Address(inetAddress, port); 102 | } 103 | 104 | private int port(KubernetesClient.EndpointAddress address) { 105 | if (this.port > 0) { 106 | return this.port; 107 | } 108 | if (address.getPort() != null) { 109 | return address.getPort(); 110 | } 111 | return NetworkConfig.DEFAULT_PORT; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/main/java/com/hazelcast/kubernetes/KubernetesClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * 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 | package com.hazelcast.kubernetes; 18 | 19 | import com.hazelcast.internal.json.Json; 20 | import com.hazelcast.internal.json.JsonArray; 21 | import com.hazelcast.internal.json.JsonObject; 22 | import com.hazelcast.internal.json.JsonValue; 23 | import com.hazelcast.logging.ILogger; 24 | import com.hazelcast.logging.Logger; 25 | 26 | import java.util.ArrayList; 27 | import java.util.Collections; 28 | import java.util.HashMap; 29 | import java.util.HashSet; 30 | import java.util.List; 31 | import java.util.Map; 32 | import java.util.Set; 33 | import java.util.concurrent.Callable; 34 | 35 | import static java.util.Arrays.asList; 36 | import static java.util.Collections.emptyList; 37 | 38 | /** 39 | * Responsible for connecting to the Kubernetes API. 40 | * 41 | * @see Kubernetes API 42 | */ 43 | @SuppressWarnings("checkstyle:methodcount") 44 | class KubernetesClient { 45 | private static final ILogger LOGGER = Logger.getLogger(KubernetesClient.class); 46 | 47 | private static final List NON_RETRYABLE_KEYWORDS = asList( 48 | "\"reason\":\"Forbidden\"", 49 | "\"reason\":\"Unauthorized\"", 50 | "Failure in generating SSLSocketFactory"); 51 | 52 | private final String namespace; 53 | private final String kubernetesMaster; 54 | private final String apiToken; 55 | private final String caCertificate; 56 | private final int retries; 57 | private boolean useNodeNameAsExternalAddress; 58 | 59 | private boolean isNoPublicIpAlreadyLogged; 60 | private boolean isKnownExceptionAlreadyLogged; 61 | 62 | KubernetesClient(String namespace, String kubernetesMaster, String apiToken, String caCertificate, int retries, 63 | boolean useNodeNameAsExternalAddress) { 64 | this.namespace = namespace; 65 | this.kubernetesMaster = kubernetesMaster; 66 | this.apiToken = apiToken; 67 | this.caCertificate = caCertificate; 68 | this.retries = retries; 69 | this.useNodeNameAsExternalAddress = useNodeNameAsExternalAddress; 70 | } 71 | 72 | /** 73 | * Retrieves POD addresses in the specified {@code namespace}. 74 | * 75 | * @return all POD addresses 76 | * @see Kubernetes Endpoint API 77 | */ 78 | List endpoints() { 79 | try { 80 | String urlString = String.format("%s/api/v1/namespaces/%s/pods", kubernetesMaster, namespace); 81 | return enrichWithPublicAddresses(parsePodsList(callGet(urlString))); 82 | } catch (RestClientException e) { 83 | return handleKnownException(e); 84 | } 85 | } 86 | 87 | /** 88 | * Retrieves POD addresses for all services in the specified {@code namespace} filtered by {@code serviceLabel} 89 | * and {@code serviceLabelValue}. 90 | * 91 | * @param serviceLabel label used to filter responses 92 | * @param serviceLabelValue label value used to filter responses 93 | * @return all POD addresses from the specified {@code namespace} filtered by the label 94 | * @see Kubernetes Endpoint API 95 | */ 96 | List endpointsByServiceLabel(String serviceLabel, String serviceLabelValue) { 97 | try { 98 | String param = String.format("labelSelector=%s=%s", serviceLabel, serviceLabelValue); 99 | String urlString = String.format("%s/api/v1/namespaces/%s/endpoints?%s", kubernetesMaster, namespace, param); 100 | return enrichWithPublicAddresses(parseEndpointsList(callGet(urlString))); 101 | } catch (RestClientException e) { 102 | return handleKnownException(e); 103 | } 104 | } 105 | 106 | /** 107 | * Retrieves POD addresses from the specified {@code namespace} and the given {@code endpointName}. 108 | * 109 | * @param endpointName endpoint name 110 | * @return all POD addresses from the specified {@code namespace} and the given {@code endpointName} 111 | * @see Kubernetes Endpoint API 112 | */ 113 | List endpointsByName(String endpointName) { 114 | try { 115 | String urlString = String.format("%s/api/v1/namespaces/%s/endpoints/%s", kubernetesMaster, namespace, endpointName); 116 | return enrichWithPublicAddresses(parseEndpoints(callGet(urlString))); 117 | } catch (RestClientException e) { 118 | return handleKnownException(e); 119 | } 120 | } 121 | 122 | /** 123 | * Retrieves POD addresses for all services in the specified {@code namespace} filtered by {@code podLabel} 124 | * and {@code podLabelValue}. 125 | * 126 | * @param podLabel label used to filter responses 127 | * @param podLabelValue label value used to filter responses 128 | * @return all POD addresses from the specified {@code namespace} filtered by the label 129 | * @see Kubernetes Endpoint API 130 | */ 131 | List endpointsByPodLabel(String podLabel, String podLabelValue) { 132 | try { 133 | String param = String.format("labelSelector=%s=%s", podLabel, podLabelValue); 134 | String urlString = String.format("%s/api/v1/namespaces/%s/pods?%s", kubernetesMaster, namespace, param); 135 | return enrichWithPublicAddresses(parsePodsList(callGet(urlString))); 136 | } catch (RestClientException e) { 137 | return handleKnownException(e); 138 | } 139 | } 140 | 141 | /** 142 | * Retrieves zone name for the specified {@code namespace} and the given {@code podName}. 143 | *

144 | * Note that the Kubernetes environment provides such information as defined 145 | * here. 146 | * 147 | * @param podName POD name 148 | * @return zone name 149 | * @see Kubernetes Endpoint API 150 | */ 151 | String zone(String podName) { 152 | String nodeUrlString = String.format("%s/api/v1/nodes/%s", kubernetesMaster, nodeName(podName)); 153 | return extractZone(callGet(nodeUrlString)); 154 | } 155 | 156 | /** 157 | * Retrieves node name for the specified {@code namespace} and the given {@code podName}. 158 | * 159 | * @param podName POD name 160 | * @return Node name 161 | * @see Kubernetes Endpoint API 162 | */ 163 | String nodeName(String podName) { 164 | String podUrlString = String.format("%s/api/v1/namespaces/%s/pods/%s", kubernetesMaster, namespace, podName); 165 | return extractNodeName(callGet(podUrlString)); 166 | } 167 | 168 | private static List parsePodsList(JsonObject podsListJson) { 169 | List addresses = new ArrayList(); 170 | 171 | for (JsonValue item : toJsonArray(podsListJson.get("items"))) { 172 | JsonObject status = item.asObject().get("status").asObject(); 173 | String ip = toString(status.get("podIP")); 174 | if (ip != null) { 175 | Integer port = extractContainerPort(item); 176 | addresses.add(new Endpoint(new EndpointAddress(ip, port), isReady(status))); 177 | } 178 | } 179 | return addresses; 180 | } 181 | 182 | private static Integer extractContainerPort(JsonValue podItemJson) { 183 | JsonArray containers = toJsonArray(podItemJson.asObject().get("spec").asObject().get("containers")); 184 | // If multiple containers are in one POD, then use the default Hazelcast port from the configuration. 185 | if (containers.size() == 1) { 186 | JsonValue container = containers.get(0); 187 | JsonArray ports = toJsonArray(container.asObject().get("ports")); 188 | // If multiple ports are exposed by a container, then use the default Hazelcast port from the configuration. 189 | if (ports.size() == 1) { 190 | JsonValue port = ports.get(0); 191 | JsonValue containerPort = port.asObject().get("containerPort"); 192 | if (containerPort != null && containerPort.isNumber()) { 193 | return containerPort.asInt(); 194 | } 195 | } 196 | } 197 | return null; 198 | } 199 | 200 | private static boolean isReady(JsonObject podItemStatusJson) { 201 | for (JsonValue containerStatus : toJsonArray(podItemStatusJson.get("containerStatuses"))) { 202 | // If multiple containers are in one POD, then each needs to be ready. 203 | if (!containerStatus.asObject().get("ready").asBoolean()) { 204 | return false; 205 | } 206 | } 207 | return true; 208 | } 209 | 210 | private static List parseEndpointsList(JsonObject endpointsListJson) { 211 | List endpoints = new ArrayList(); 212 | for (JsonValue item : toJsonArray(endpointsListJson.get("items"))) { 213 | endpoints.addAll(parseEndpoints(item)); 214 | } 215 | return endpoints; 216 | } 217 | 218 | private static List parseEndpoints(JsonValue endpointItemJson) { 219 | List addresses = new ArrayList(); 220 | 221 | for (JsonValue subset : toJsonArray(endpointItemJson.asObject().get("subsets"))) { 222 | Integer endpointPort = extractPort(subset); 223 | for (JsonValue address : toJsonArray(subset.asObject().get("addresses"))) { 224 | addresses.add(extractEntrypointAddress(address, endpointPort, true)); 225 | } 226 | for (JsonValue address : toJsonArray(subset.asObject().get("notReadyAddresses"))) { 227 | addresses.add(extractEntrypointAddress(address, endpointPort, false)); 228 | } 229 | } 230 | return addresses; 231 | } 232 | 233 | private static Integer extractPort(JsonValue subsetJson) { 234 | JsonArray ports = toJsonArray(subsetJson.asObject().get("ports")); 235 | if (ports.size() == 1) { 236 | JsonValue port = ports.get(0); 237 | return port.asObject().get("port").asInt(); 238 | } 239 | return null; 240 | } 241 | 242 | private static Endpoint extractEntrypointAddress(JsonValue endpointAddressJson, Integer endpointPort, boolean isReady) { 243 | String ip = endpointAddressJson.asObject().get("ip").asString(); 244 | Integer port = extractHazelcastServicePortFrom(endpointAddressJson, endpointPort); 245 | Map additionalProperties = extractAdditionalPropertiesFrom(endpointAddressJson); 246 | return new Endpoint(new EndpointAddress(ip, port), isReady, additionalProperties); 247 | } 248 | 249 | private static Integer extractHazelcastServicePortFrom(JsonValue endpointAddressJson, Integer endpointPort) { 250 | JsonValue servicePort = endpointAddressJson.asObject().get("hazelcast-service-port"); 251 | if (servicePort != null && servicePort.isNumber()) { 252 | return servicePort.asInt(); 253 | } 254 | return endpointPort; 255 | } 256 | 257 | private static Map extractAdditionalPropertiesFrom(JsonValue endpointAddressJson) { 258 | Set knownFieldNames = new HashSet( 259 | asList("ip", "nodeName", "targetRef", "hostname", "hazelcast-service-port")); 260 | 261 | Map result = new HashMap(); 262 | for (JsonObject.Member member : endpointAddressJson.asObject()) { 263 | if (!knownFieldNames.contains(member.getName())) { 264 | result.put(member.getName(), toString(member.getValue())); 265 | } 266 | } 267 | return result; 268 | } 269 | 270 | private static String extractNodeName(JsonObject podJson) { 271 | return toString(podJson.get("spec").asObject().get("nodeName")); 272 | } 273 | 274 | private static String extractZone(JsonObject nodeJson) { 275 | JsonObject labels = nodeJson.get("metadata").asObject().get("labels").asObject(); 276 | List zoneLabels = asList("topology.kubernetes.io/zone", "failure-domain.kubernetes.io/zone", 277 | "failure-domain.beta.kubernetes.io/zone"); 278 | for (String zoneLabel : zoneLabels) { 279 | JsonValue zone = labels.get(zoneLabel); 280 | if (zone != null) { 281 | return toString(zone); 282 | } 283 | } 284 | return null; 285 | } 286 | 287 | /** 288 | * Tries to add public addresses to the endpoints. 289 | *

290 | * If it's not possible, then returns the input parameter. 291 | *

292 | * Assigning public IPs must meet one of the following requirements: 293 | *

    294 | *
  • Each POD must be exposed with a separate LoadBalancer service OR
  • 295 | *
  • Each POD must be exposed with a separate NodePort service and Kubernetes nodes must have external IPs
  • 296 | *
297 | *

298 | * The algorithm to fetch public IPs is as follows: 299 | *

    300 | *
  1. Use Kubernetes API (/endpoints) to find dedicated services for each POD
  2. 301 | *
  3. For each POD: 302 | *
      303 | *
    1. Use Kubernetes API (/services) to find the LoadBalancer External IP and Service Port
    2. 304 | *
    3. If not found, then use Kubernetes API (/nodes) to find External IP of the Node
    4. 305 | *
    306 | *
  4. 307 | *
308 | */ 309 | private List enrichWithPublicAddresses(List endpoints) { 310 | try { 311 | String endpointsUrl = String.format("%s/api/v1/namespaces/%s/endpoints", kubernetesMaster, namespace); 312 | JsonObject endpointsJson = callGet(endpointsUrl); 313 | 314 | List privateAddresses = privateAddresses(endpoints); 315 | Map services = extractServices(endpointsJson, privateAddresses); 316 | Map nodes = extractNodes(endpointsJson, privateAddresses); 317 | 318 | Map publicIps = new HashMap(); 319 | Map publicPorts = new HashMap(); 320 | Map cachedNodePublicIps = new HashMap(); 321 | 322 | for (Map.Entry serviceEntry : services.entrySet()) { 323 | EndpointAddress privateAddress = serviceEntry.getKey(); 324 | String service = serviceEntry.getValue(); 325 | String serviceUrl = String.format("%s/api/v1/namespaces/%s/services/%s", kubernetesMaster, namespace, service); 326 | JsonObject serviceJson = callGet(serviceUrl); 327 | try { 328 | String loadBalancerIp = extractLoadBalancerIp(serviceJson); 329 | Integer servicePort = extractServicePort(serviceJson); 330 | publicIps.put(privateAddress, loadBalancerIp); 331 | publicPorts.put(privateAddress, servicePort); 332 | } catch (Exception e) { 333 | // Load Balancer public IP cannot be found, try using NodePort. 334 | Integer nodePort = extractNodePort(serviceJson); 335 | String node = nodes.get(privateAddress); 336 | String nodePublicAddress; 337 | if (cachedNodePublicIps.containsKey(node)) { 338 | nodePublicAddress = cachedNodePublicIps.get(node); 339 | } else { 340 | nodePublicAddress = externalAddressForNode(node); 341 | cachedNodePublicIps.put(node, nodePublicAddress); 342 | } 343 | publicIps.put(privateAddress, nodePublicAddress); 344 | publicPorts.put(privateAddress, nodePort); 345 | } 346 | } 347 | 348 | return createEndpoints(endpoints, publicIps, publicPorts); 349 | } catch (Exception e) { 350 | LOGGER.finest(e); 351 | // Log warning only once. 352 | if (!isNoPublicIpAlreadyLogged) { 353 | LOGGER.warning( 354 | "Cannot fetch public IPs of Hazelcast Member PODs, you won't be able to use Hazelcast Smart Client from " 355 | + "outside of the Kubernetes network"); 356 | isNoPublicIpAlreadyLogged = true; 357 | } 358 | return endpoints; 359 | } 360 | } 361 | 362 | private static List privateAddresses(List endpoints) { 363 | List result = new ArrayList(); 364 | for (Endpoint endpoint : endpoints) { 365 | result.add(endpoint.getPrivateAddress()); 366 | } 367 | return result; 368 | } 369 | 370 | private static Map extractServices(JsonObject endpointsListJson, 371 | List privateAddresses) { 372 | Map result = new HashMap(); 373 | Set left = new HashSet(privateAddresses); 374 | for (JsonValue item : toJsonArray(endpointsListJson.get("items"))) { 375 | String service = toString(item.asObject().get("metadata").asObject().get("name")); 376 | List endpoints = parseEndpoints(item); 377 | // Service must point to exactly one endpoint address, otherwise the public IP would be ambiguous. 378 | if (endpoints.size() == 1) { 379 | EndpointAddress address = endpoints.get(0).getPrivateAddress(); 380 | if (left.contains(address)) { 381 | result.put(address, service); 382 | left.remove(address); 383 | } 384 | } 385 | } 386 | if (!left.isEmpty()) { 387 | // At least one Hazelcast Member POD does not have a corresponding service. 388 | throw new KubernetesClientException(String.format("Cannot fetch services dedicated to the following PODs: %s", left)); 389 | } 390 | return result; 391 | } 392 | 393 | private static Map extractNodes(JsonObject endpointsListJson, 394 | List privateAddresses) { 395 | Map result = new HashMap(); 396 | Set left = new HashSet(privateAddresses); 397 | for (JsonValue item : toJsonArray(endpointsListJson.get("items"))) { 398 | for (JsonValue subset : toJsonArray(item.asObject().get("subsets"))) { 399 | JsonObject subsetObject = subset.asObject(); 400 | List ports = new ArrayList(); 401 | for (JsonValue port : toJsonArray(subsetObject.get("ports"))) { 402 | ports.add(port.asObject().get("port").asInt()); 403 | } 404 | 405 | Map nodes = new HashMap(); 406 | nodes.putAll(extractNodes(subsetObject.get("addresses"), ports)); 407 | nodes.putAll(extractNodes(subsetObject.get("notReadyAddresses"), ports)); 408 | for (Map.Entry nodeEntry : nodes.entrySet()) { 409 | EndpointAddress address = nodeEntry.getKey(); 410 | if (privateAddresses.contains(address)) { 411 | result.put(address, nodes.get(address)); 412 | left.remove(address); 413 | } 414 | } 415 | } 416 | } 417 | if (!left.isEmpty()) { 418 | // At least one Hazelcast Member POD does not have 'nodeName' assigned. 419 | throw new KubernetesClientException(String.format("Cannot fetch nodeName from the following PODs: %s", left)); 420 | } 421 | return result; 422 | } 423 | 424 | private static Map extractNodes(JsonValue addressesJson, List ports) { 425 | Map result = new HashMap(); 426 | for (JsonValue address : toJsonArray(addressesJson)) { 427 | String ip = address.asObject().get("ip").asString(); 428 | String nodeName = toString(address.asObject().get("nodeName")); 429 | for (Integer port : ports) { 430 | result.put(new EndpointAddress(ip, port), nodeName); 431 | } 432 | } 433 | return result; 434 | } 435 | 436 | private static String extractLoadBalancerIp(JsonObject serviceResponse) { 437 | return serviceResponse.get("status").asObject() 438 | .get("loadBalancer").asObject() 439 | .get("ingress").asArray().get(0).asObject() 440 | .get("ip").asString(); 441 | } 442 | 443 | private static Integer extractServicePort(JsonObject serviceJson) { 444 | JsonArray ports = toJsonArray(serviceJson.get("spec").asObject().get("ports")); 445 | // Service must have one and only one Node Port assigned. 446 | if (ports.size() != 1) { 447 | throw new KubernetesClientException("Cannot fetch nodePort from the service"); 448 | } 449 | return ports.get(0).asObject().get("port").asInt(); 450 | } 451 | 452 | private static Integer extractNodePort(JsonObject serviceJson) { 453 | JsonArray ports = toJsonArray(serviceJson.get("spec").asObject().get("ports")); 454 | // Service must have one and only one Node Port assigned. 455 | if (ports.size() != 1) { 456 | throw new KubernetesClientException("Cannot fetch nodePort from the service"); 457 | } 458 | return ports.get(0).asObject().get("nodePort").asInt(); 459 | } 460 | 461 | private String externalAddressForNode(String node) { 462 | String nodeExternalAddress; 463 | if (useNodeNameAsExternalAddress) { 464 | LOGGER.info("Using node name instead of public IP for node, must be available from client: " + node); 465 | nodeExternalAddress = node; 466 | } else { 467 | String nodeUrl = String.format("%s/api/v1/nodes/%s", kubernetesMaster, node); 468 | nodeExternalAddress = extractNodePublicIp(callGet(nodeUrl)); 469 | } 470 | return nodeExternalAddress; 471 | } 472 | 473 | private static String extractNodePublicIp(JsonObject nodeJson) { 474 | for (JsonValue address : toJsonArray(nodeJson.get("status").asObject().get("addresses"))) { 475 | if ("ExternalIP".equals(address.asObject().get("type").asString())) { 476 | return address.asObject().get("address").asString(); 477 | } 478 | } 479 | throw new KubernetesClientException("Node does not have ExternalIP assigned"); 480 | } 481 | 482 | private static List createEndpoints(List endpoints, Map publicIps, 483 | Map publicPorts) { 484 | List result = new ArrayList(); 485 | for (Endpoint endpoint : endpoints) { 486 | EndpointAddress privateAddress = endpoint.getPrivateAddress(); 487 | EndpointAddress publicAddress = new EndpointAddress(publicIps.get(privateAddress), 488 | publicPorts.get(privateAddress)); 489 | result.add(new Endpoint(privateAddress, publicAddress, endpoint.isReady(), endpoint.getAdditionalProperties())); 490 | } 491 | return result; 492 | } 493 | 494 | /** 495 | * Makes a REST call to Kubernetes API and returns the result JSON. 496 | * 497 | * @param urlString Kubernetes API REST endpoint 498 | * @return parsed JSON 499 | * @throws KubernetesClientException if Kubernetes API didn't respond with 200 and a valid JSON content 500 | */ 501 | private JsonObject callGet(final String urlString) { 502 | return RetryUtils.retry(new Callable() { 503 | @Override 504 | public JsonObject call() { 505 | return Json 506 | .parse(RestClient.create(urlString).withHeader("Authorization", String.format("Bearer %s", apiToken)) 507 | .withCaCertificates(caCertificate) 508 | .get()) 509 | .asObject(); 510 | } 511 | }, retries, NON_RETRYABLE_KEYWORDS); 512 | } 513 | 514 | @SuppressWarnings("checkstyle:magicnumber") 515 | private List handleKnownException(RestClientException e) { 516 | if (e.getHttpErrorCode() == 401) { 517 | if (!isKnownExceptionAlreadyLogged) { 518 | LOGGER.warning("Kubernetes API authorization failure! To use Hazelcast Kubernetes discovery, " 519 | + "please check your 'api-token' property. Starting standalone."); 520 | isKnownExceptionAlreadyLogged = true; 521 | } 522 | } else if (e.getHttpErrorCode() == 403) { 523 | if (!isKnownExceptionAlreadyLogged) { 524 | LOGGER.warning("Kubernetes API access is forbidden! Starting standalone. To use Hazelcast Kubernetes discovery," 525 | + " configure the required RBAC. For 'default' service account in 'default' namespace execute: " 526 | + "`kubectl apply -f https://raw.githubusercontent.com/hazelcast/hazelcast-kubernetes/master/rbac.yaml`"); 527 | isKnownExceptionAlreadyLogged = true; 528 | } 529 | } else { 530 | throw e; 531 | } 532 | LOGGER.finest(e); 533 | return emptyList(); 534 | } 535 | 536 | private static JsonArray toJsonArray(JsonValue jsonValue) { 537 | if (jsonValue == null || jsonValue.isNull()) { 538 | return new JsonArray(); 539 | } else { 540 | return jsonValue.asArray(); 541 | } 542 | } 543 | 544 | private static String toString(JsonValue jsonValue) { 545 | if (jsonValue == null || jsonValue.isNull()) { 546 | return null; 547 | } else if (jsonValue.isString()) { 548 | return jsonValue.asString(); 549 | } else { 550 | return jsonValue.toString(); 551 | } 552 | } 553 | 554 | /** 555 | * Result which stores the information about a single endpoint. 556 | */ 557 | static final class Endpoint { 558 | private final EndpointAddress privateAddress; 559 | private final EndpointAddress publicAddress; 560 | private final boolean isReady; 561 | private final Map additionalProperties; 562 | 563 | Endpoint(EndpointAddress privateAddress, boolean isReady) { 564 | this.privateAddress = privateAddress; 565 | this.publicAddress = null; 566 | this.isReady = isReady; 567 | this.additionalProperties = Collections.emptyMap(); 568 | } 569 | 570 | Endpoint(EndpointAddress privateAddress, boolean isReady, Map additionalProperties) { 571 | this.privateAddress = privateAddress; 572 | this.publicAddress = null; 573 | this.isReady = isReady; 574 | this.additionalProperties = additionalProperties; 575 | } 576 | 577 | Endpoint(EndpointAddress privateAddress, EndpointAddress publicAddress, boolean isReady, 578 | Map additionalProperties) { 579 | this.privateAddress = privateAddress; 580 | this.publicAddress = publicAddress; 581 | this.isReady = isReady; 582 | this.additionalProperties = additionalProperties; 583 | } 584 | 585 | EndpointAddress getPublicAddress() { 586 | return publicAddress; 587 | } 588 | 589 | EndpointAddress getPrivateAddress() { 590 | return privateAddress; 591 | } 592 | 593 | boolean isReady() { 594 | return isReady; 595 | } 596 | 597 | Map getAdditionalProperties() { 598 | return additionalProperties; 599 | } 600 | } 601 | 602 | static final class EndpointAddress { 603 | private final String ip; 604 | private final Integer port; 605 | 606 | EndpointAddress(String ip, Integer port) { 607 | this.ip = ip; 608 | this.port = port; 609 | } 610 | 611 | String getIp() { 612 | return ip; 613 | } 614 | 615 | Integer getPort() { 616 | return port; 617 | } 618 | 619 | @Override 620 | public boolean equals(Object o) { 621 | if (this == o) { 622 | return true; 623 | } 624 | if (o == null || getClass() != o.getClass()) { 625 | return false; 626 | } 627 | 628 | EndpointAddress address = (EndpointAddress) o; 629 | 630 | if (ip != null ? !ip.equals(address.ip) : address.ip != null) { 631 | return false; 632 | } 633 | return port != null ? port.equals(address.port) : address.port == null; 634 | } 635 | 636 | @Override 637 | public int hashCode() { 638 | int result = ip != null ? ip.hashCode() : 0; 639 | result = 31 * result + (port != null ? port.hashCode() : 0); 640 | return result; 641 | } 642 | 643 | @Override 644 | public String toString() { 645 | return String.format("%s:%s", ip, port); 646 | } 647 | } 648 | } 649 | -------------------------------------------------------------------------------- /src/main/java/com/hazelcast/kubernetes/KubernetesClientException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * 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 | package com.hazelcast.kubernetes; 18 | 19 | import com.hazelcast.core.HazelcastException; 20 | 21 | /** 22 | * Exception to indicate any issues with {@link KubernetesClient}. 23 | */ 24 | class KubernetesClientException 25 | extends HazelcastException { 26 | KubernetesClientException(String message) { 27 | super(message); 28 | } 29 | 30 | KubernetesClientException(String message, Throwable cause) { 31 | super(message, cause); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/hazelcast/kubernetes/KubernetesConfig.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * 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 | package com.hazelcast.kubernetes; 18 | 19 | import com.hazelcast.config.InvalidConfigurationException; 20 | import com.hazelcast.config.properties.PropertyDefinition; 21 | import com.hazelcast.internal.nio.IOUtil; 22 | import com.hazelcast.internal.util.StringUtil; 23 | import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; 24 | 25 | import java.io.File; 26 | import java.io.FileInputStream; 27 | import java.io.IOException; 28 | import java.io.InputStream; 29 | import java.nio.charset.StandardCharsets; 30 | import java.util.Map; 31 | 32 | import static com.hazelcast.kubernetes.KubernetesProperties.KUBERNETES_API_RETIRES; 33 | import static com.hazelcast.kubernetes.KubernetesProperties.KUBERNETES_API_TOKEN; 34 | import static com.hazelcast.kubernetes.KubernetesProperties.KUBERNETES_CA_CERTIFICATE; 35 | import static com.hazelcast.kubernetes.KubernetesProperties.KUBERNETES_MASTER_URL; 36 | import static com.hazelcast.kubernetes.KubernetesProperties.KUBERNETES_SYSTEM_PREFIX; 37 | import static com.hazelcast.kubernetes.KubernetesProperties.NAMESPACE; 38 | import static com.hazelcast.kubernetes.KubernetesProperties.POD_LABEL_NAME; 39 | import static com.hazelcast.kubernetes.KubernetesProperties.POD_LABEL_VALUE; 40 | import static com.hazelcast.kubernetes.KubernetesProperties.RESOLVE_NOT_READY_ADDRESSES; 41 | import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_DNS; 42 | import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_DNS_TIMEOUT; 43 | import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_LABEL_NAME; 44 | import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_LABEL_VALUE; 45 | import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_NAME; 46 | import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_PORT; 47 | import static com.hazelcast.kubernetes.KubernetesProperties.USE_NODE_NAME_AS_EXTERNAL_ADDRESS; 48 | 49 | /** 50 | * Responsible for fetching, parsing, and validating Hazelcast Kubernetes Discovery Strategy input properties. 51 | */ 52 | @SuppressWarnings({"checkstyle:npathcomplexity", "checkstyle:cyclomaticcomplexity"}) 53 | final class KubernetesConfig { 54 | private static final String DEFAULT_MASTER_URL = "https://kubernetes.default.svc"; 55 | private static final int DEFAULT_SERVICE_DNS_TIMEOUT_SECONDS = 5; 56 | private static final int DEFAULT_KUBERNETES_API_RETRIES = 3; 57 | 58 | // Parameters for DNS Lookup mode 59 | private final String serviceDns; 60 | private final int serviceDnsTimeout; 61 | 62 | // Parameters for Kubernetes API mode 63 | private final String serviceName; 64 | private final String serviceLabelName; 65 | private final String serviceLabelValue; 66 | private final String namespace; 67 | private final String podLabelName; 68 | private final String podLabelValue; 69 | private final boolean resolveNotReadyAddresses; 70 | private final boolean useNodeNameAsExternalAddress; 71 | private final int kubernetesApiRetries; 72 | private final String kubernetesMasterUrl; 73 | private final String kubernetesApiToken; 74 | private final String kubernetesCaCertificate; 75 | 76 | // Parameters for both DNS Lookup and Kubernetes API modes 77 | private final int servicePort; 78 | 79 | KubernetesConfig(Map properties) { 80 | this.serviceDns = getOrNull(properties, KUBERNETES_SYSTEM_PREFIX, SERVICE_DNS); 81 | this.serviceDnsTimeout 82 | = getOrDefault(properties, KUBERNETES_SYSTEM_PREFIX, SERVICE_DNS_TIMEOUT, DEFAULT_SERVICE_DNS_TIMEOUT_SECONDS); 83 | this.serviceName = getOrNull(properties, KUBERNETES_SYSTEM_PREFIX, SERVICE_NAME); 84 | this.serviceLabelName = getOrNull(properties, KUBERNETES_SYSTEM_PREFIX, SERVICE_LABEL_NAME); 85 | this.serviceLabelValue = getOrDefault(properties, KUBERNETES_SYSTEM_PREFIX, SERVICE_LABEL_VALUE, "true"); 86 | this.podLabelName = getOrNull(properties, KUBERNETES_SYSTEM_PREFIX, POD_LABEL_NAME); 87 | this.podLabelValue = getOrNull(properties, KUBERNETES_SYSTEM_PREFIX, POD_LABEL_VALUE); 88 | this.resolveNotReadyAddresses = getOrDefault(properties, KUBERNETES_SYSTEM_PREFIX, RESOLVE_NOT_READY_ADDRESSES, true); 89 | this.useNodeNameAsExternalAddress 90 | = getOrDefault(properties, KUBERNETES_SYSTEM_PREFIX, USE_NODE_NAME_AS_EXTERNAL_ADDRESS, false); 91 | this.kubernetesApiRetries 92 | = getOrDefault(properties, KUBERNETES_SYSTEM_PREFIX, KUBERNETES_API_RETIRES, DEFAULT_KUBERNETES_API_RETRIES); 93 | this.kubernetesMasterUrl = getOrDefault(properties, KUBERNETES_SYSTEM_PREFIX, KUBERNETES_MASTER_URL, DEFAULT_MASTER_URL); 94 | this.kubernetesApiToken = getApiToken(properties); 95 | this.kubernetesCaCertificate = caCertificate(properties); 96 | this.servicePort = getOrDefault(properties, KUBERNETES_SYSTEM_PREFIX, SERVICE_PORT, 0); 97 | this.namespace = getNamespaceWithFallbacks(properties, KUBERNETES_SYSTEM_PREFIX, NAMESPACE); 98 | 99 | validateConfig(); 100 | } 101 | 102 | private String getNamespaceWithFallbacks(Map properties, 103 | String kubernetesSystemPrefix, 104 | PropertyDefinition propertyDefinition) { 105 | String namespace = getOrNull(properties, kubernetesSystemPrefix, propertyDefinition); 106 | 107 | if (namespace == null) { 108 | namespace = System.getenv("KUBERNETES_NAMESPACE"); 109 | } 110 | 111 | if (namespace == null) { 112 | namespace = System.getenv("OPENSHIFT_BUILD_NAMESPACE"); 113 | } 114 | 115 | if (namespace == null && getMode() == DiscoveryMode.KUBERNETES_API) { 116 | namespace = readNamespace(); 117 | } 118 | 119 | return namespace; 120 | } 121 | 122 | private String getApiToken(Map properties) { 123 | String apiToken = getOrNull(properties, KUBERNETES_SYSTEM_PREFIX, KUBERNETES_API_TOKEN); 124 | if (apiToken == null && getMode() == DiscoveryMode.KUBERNETES_API) { 125 | apiToken = readAccountToken(); 126 | } 127 | return apiToken; 128 | } 129 | 130 | private String caCertificate(Map properties) { 131 | String caCertificate = getOrNull(properties, KUBERNETES_SYSTEM_PREFIX, KUBERNETES_CA_CERTIFICATE); 132 | if (caCertificate == null && getMode() == DiscoveryMode.KUBERNETES_API) { 133 | caCertificate = readCaCertificate(); 134 | } 135 | return caCertificate; 136 | } 137 | 138 | @SuppressFBWarnings("DMI_HARDCODED_ABSOLUTE_FILENAME") 139 | private static String readAccountToken() { 140 | return readFileContents("/var/run/secrets/kubernetes.io/serviceaccount/token"); 141 | } 142 | 143 | @SuppressFBWarnings("DMI_HARDCODED_ABSOLUTE_FILENAME") 144 | private static String readCaCertificate() { 145 | return readFileContents("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"); 146 | } 147 | 148 | @SuppressFBWarnings("DMI_HARDCODED_ABSOLUTE_FILENAME") 149 | private static String readNamespace() { 150 | return readFileContents("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); 151 | } 152 | 153 | static String readFileContents(String fileName) { 154 | InputStream is = null; 155 | try { 156 | File file = new File(fileName); 157 | byte[] data = new byte[(int) file.length()]; 158 | is = new FileInputStream(file); 159 | is.read(data); 160 | return new String(data, StandardCharsets.UTF_8); 161 | } catch (IOException e) { 162 | throw new RuntimeException("Could not get " + fileName, e); 163 | } finally { 164 | IOUtil.closeResource(is); 165 | } 166 | } 167 | 168 | private T getOrNull(Map properties, String prefix, PropertyDefinition property) { 169 | return getOrDefault(properties, prefix, property, null); 170 | } 171 | 172 | private T getOrDefault(Map properties, String prefix, 173 | PropertyDefinition property, T defaultValue) { 174 | if (property == null) { 175 | return defaultValue; 176 | } 177 | 178 | Comparable value = readProperty(prefix, property); 179 | if (value == null) { 180 | value = properties.get(property.key()); 181 | } 182 | 183 | if (value == null) { 184 | return defaultValue; 185 | } 186 | 187 | return (T) value; 188 | } 189 | 190 | private Comparable readProperty(String prefix, PropertyDefinition property) { 191 | if (prefix != null) { 192 | String p = getProperty(prefix, property); 193 | String v = System.getProperty(p); 194 | if (StringUtil.isNullOrEmpty(v)) { 195 | v = System.getenv(p); 196 | if (StringUtil.isNullOrEmpty(v)) { 197 | v = System.getenv(cIdentifierLike(p)); 198 | } 199 | } 200 | 201 | if (!StringUtil.isNullOrEmpty(v)) { 202 | return property.typeConverter().convert(v); 203 | } 204 | } 205 | return null; 206 | } 207 | 208 | private String cIdentifierLike(String property) { 209 | property = property.toUpperCase(); 210 | property = property.replace(".", "_"); 211 | return property.replace("-", "_"); 212 | } 213 | 214 | private String getProperty(String prefix, PropertyDefinition property) { 215 | StringBuilder sb = new StringBuilder(prefix); 216 | if (prefix.charAt(prefix.length() - 1) != '.') { 217 | sb.append('.'); 218 | } 219 | return sb.append(property.key()).toString(); 220 | } 221 | 222 | private void validateConfig() { 223 | if (!StringUtil.isNullOrEmptyAfterTrim(serviceDns) && (!StringUtil.isNullOrEmptyAfterTrim(serviceName) 224 | || !StringUtil.isNullOrEmptyAfterTrim(serviceLabelName) || !StringUtil.isNullOrEmptyAfterTrim(podLabelName))) { 225 | throw new InvalidConfigurationException( 226 | String.format("Properties '%s' and ('%s' or '%s' or %s) cannot be defined at the same time", 227 | SERVICE_DNS.key(), SERVICE_NAME.key(), SERVICE_LABEL_NAME.key(), POD_LABEL_NAME.key())); 228 | } 229 | if (!StringUtil.isNullOrEmptyAfterTrim(serviceName) && !StringUtil.isNullOrEmptyAfterTrim(serviceLabelName)) { 230 | throw new InvalidConfigurationException( 231 | String.format("Properties '%s' and '%s' cannot be defined at the same time", 232 | SERVICE_NAME.key(), SERVICE_LABEL_NAME.key())); 233 | } 234 | if (!StringUtil.isNullOrEmptyAfterTrim(serviceName) && !StringUtil.isNullOrEmptyAfterTrim(podLabelName)) { 235 | throw new InvalidConfigurationException( 236 | String.format("Properties '%s' and '%s' cannot be defined at the same time", 237 | SERVICE_NAME.key(), POD_LABEL_NAME.key())); 238 | } 239 | if (!StringUtil.isNullOrEmptyAfterTrim(serviceLabelName) && !StringUtil.isNullOrEmptyAfterTrim(podLabelName)) { 240 | throw new InvalidConfigurationException( 241 | String.format("Properties '%s' and '%s' cannot be defined at the same time", 242 | SERVICE_LABEL_NAME.key(), POD_LABEL_NAME.key())); 243 | } 244 | if (serviceDnsTimeout < 0) { 245 | throw new InvalidConfigurationException( 246 | String.format("Property '%s' cannot be a negative number", SERVICE_DNS_TIMEOUT.key())); 247 | } 248 | if (kubernetesApiRetries < 0) { 249 | throw new InvalidConfigurationException( 250 | String.format("Property '%s' cannot be a negative number", KUBERNETES_API_RETIRES.key())); 251 | } 252 | if (servicePort < 0) { 253 | throw new InvalidConfigurationException( 254 | String.format("Property '%s' cannot be a negative number", SERVICE_PORT.key())); 255 | } 256 | } 257 | 258 | DiscoveryMode getMode() { 259 | if (!StringUtil.isNullOrEmptyAfterTrim(serviceDns)) { 260 | return DiscoveryMode.DNS_LOOKUP; 261 | } else { 262 | return DiscoveryMode.KUBERNETES_API; 263 | } 264 | } 265 | 266 | String getServiceDns() { 267 | return serviceDns; 268 | } 269 | 270 | int getServiceDnsTimeout() { 271 | return serviceDnsTimeout; 272 | } 273 | 274 | String getServiceName() { 275 | return serviceName; 276 | } 277 | 278 | String getServiceLabelName() { 279 | return serviceLabelName; 280 | } 281 | 282 | String getServiceLabelValue() { 283 | return serviceLabelValue; 284 | } 285 | 286 | String getNamespace() { 287 | return namespace; 288 | } 289 | 290 | public String getPodLabelName() { 291 | return podLabelName; 292 | } 293 | 294 | public String getPodLabelValue() { 295 | return podLabelValue; 296 | } 297 | 298 | boolean isResolveNotReadyAddresses() { 299 | return resolveNotReadyAddresses; 300 | } 301 | 302 | boolean isUseNodeNameAsExternalAddress() { 303 | return useNodeNameAsExternalAddress; 304 | } 305 | 306 | int getKubernetesApiRetries() { 307 | return kubernetesApiRetries; 308 | } 309 | 310 | String getKubernetesMasterUrl() { 311 | return kubernetesMasterUrl; 312 | } 313 | 314 | String getKubernetesApiToken() { 315 | return kubernetesApiToken; 316 | } 317 | 318 | String getKubernetesCaCertificate() { 319 | return kubernetesCaCertificate; 320 | } 321 | 322 | int getServicePort() { 323 | return servicePort; 324 | } 325 | 326 | @Override 327 | public String toString() { 328 | return "Kubernetes Discovery properties: { " 329 | + "service-dns: " + serviceDns + ", " 330 | + "service-dns-timeout: " + serviceDnsTimeout + ", " 331 | + "service-name: " + serviceName + ", " 332 | + "service-port: " + servicePort + ", " 333 | + "service-label: " + serviceLabelName + ", " 334 | + "service-label-value: " + serviceLabelValue + ", " 335 | + "namespace: " + namespace + ", " 336 | + "pod-label: " + podLabelName + ", " 337 | + "pod-label-value: " + podLabelValue + ", " 338 | + "resolve-not-ready-addresses: " + resolveNotReadyAddresses + ", " 339 | + "use-node-name-as-external-address: " + useNodeNameAsExternalAddress + ", " 340 | + "kubernetes-api-retries: " + kubernetesApiRetries + ", " 341 | + "kubernetes-master: " + kubernetesMasterUrl + "}"; 342 | } 343 | 344 | enum DiscoveryMode { 345 | DNS_LOOKUP, 346 | KUBERNETES_API 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /src/main/java/com/hazelcast/kubernetes/KubernetesProperties.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * 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 | package com.hazelcast.kubernetes; 18 | 19 | import com.hazelcast.config.properties.PropertyDefinition; 20 | import com.hazelcast.config.properties.SimplePropertyDefinition; 21 | import com.hazelcast.core.TypeConverter; 22 | 23 | import static com.hazelcast.config.properties.PropertyTypeConverter.BOOLEAN; 24 | import static com.hazelcast.config.properties.PropertyTypeConverter.INTEGER; 25 | import static com.hazelcast.config.properties.PropertyTypeConverter.STRING; 26 | 27 | /** 28 | *

Configuration class of the Hazelcast Discovery Plugin for Kubernetes.

29 | *

For possible configuration properties please refer to the public constants of this class.

30 | */ 31 | public final class KubernetesProperties { 32 | 33 | /** 34 | *

Configuration System Environment Prefix: hazelcast.kubernetes.

35 | * Defines the prefix for system environment variables and JVM command line parameters.
36 | * Defining or overriding properties as JVM parameters or using the system environment, those 37 | * properties need to be prefixed to prevent collision on property names.
38 | * Example: {@link #SERVICE_DNS} will be: 39 | *
 40 |      *     -Dhazelcast.kubernetes.service-dns=value
 41 |      * 
42 | * For kubernetes and openshift there is a special rule where the environment variables are 43 | * provided in C-identifier style, therefore the prefix is converted to uppercase and dots 44 | * and dashed will be replaced with underscores: 45 | *
 46 |      *     HAZELCAST_KUBERNETES_SERVICE_DNS=value
 47 |      * 
48 | */ 49 | public static final String KUBERNETES_SYSTEM_PREFIX = "hazelcast.kubernetes."; 50 | 51 | /** 52 | *

Configuration key: service-dns

53 | * Defines the DNS service lookup domain. This is defined as something similar 54 | * to my-svc.my-namespace.svc.cluster.local.
55 | * For more information please refer to the official documentation of the Kubernetes DNS addon, 56 | * here. 57 | */ 58 | public static final PropertyDefinition SERVICE_DNS = property("service-dns", STRING); 59 | 60 | /** 61 | *

Configuration key: service-dns-timeout

62 | * Defines the DNS service lookup timeout in seconds. Defaults to: 5 secs. 63 | */ 64 | public static final PropertyDefinition SERVICE_DNS_TIMEOUT = property("service-dns-timeout", INTEGER); 65 | 66 | /** 67 | *

Configuration key: service-name

68 | * Defines the service name of the POD to lookup through the Service Discovery REST API of Kubernetes. 69 | */ 70 | public static final PropertyDefinition SERVICE_NAME = property("service-name", STRING); 71 | /** 72 | *

Configuration key: service-label-name

73 | * Defines the service label to lookup through the Service Discovery REST API of Kubernetes. 74 | */ 75 | public static final PropertyDefinition SERVICE_LABEL_NAME = property("service-label-name", STRING); 76 | /** 77 | *

Configuration key: service-label-value

78 | * Defines the service label value to lookup through the Service Discovery REST API of Kubernetes. 79 | */ 80 | public static final PropertyDefinition SERVICE_LABEL_VALUE = property("service-label-value", STRING); 81 | 82 | /** 83 | *

Configuration key: namespace

84 | * Defines the namespace of the application POD through the Service Discovery REST API of Kubernetes. 85 | */ 86 | public static final PropertyDefinition NAMESPACE = property("namespace", STRING); 87 | 88 | /** 89 | *

Configuration key: pod-label-name

90 | * Defines the pod label to lookup through the Service Discovery REST API of Kubernetes. 91 | */ 92 | public static final PropertyDefinition POD_LABEL_NAME = property("pod-label-name", STRING); 93 | /** 94 | *

Configuration key: pod-label-value

95 | * Defines the pod label value to lookup through the Service Discovery REST API of Kubernetes. 96 | */ 97 | public static final PropertyDefinition POD_LABEL_VALUE = property("pod-label-value", STRING); 98 | 99 | /** 100 | *

Configuration key: resolve-not-ready-addresses

101 | * Defines if not ready addresses should be evaluated to be discovered on startup. 102 | */ 103 | public static final PropertyDefinition RESOLVE_NOT_READY_ADDRESSES = property("resolve-not-ready-addresses", BOOLEAN); 104 | 105 | /** 106 | *

Configuration key: use-node-name-as-external-address

107 | * Defines if the node name should be used as external address, instead of looking up the external IP using 108 | * the /nodes resource. Default is false. 109 | */ 110 | public static final PropertyDefinition USE_NODE_NAME_AS_EXTERNAL_ADDRESS = property("use-node-name-as-external-address", 111 | BOOLEAN); 112 | 113 | /** 114 | *

Configuration key: kubernetes-api-retries

115 | * Defines the number of retries to Kubernetes API. Defaults to: 3. 116 | */ 117 | public static final PropertyDefinition KUBERNETES_API_RETIRES = property("kubernetes-api-retries", INTEGER); 118 | 119 | /** 120 | *

Configuration key: kubernetes-master

121 | * Defines an alternative address for the kubernetes master. Defaults to: https://kubernetes.default.svc 122 | */ 123 | public static final PropertyDefinition KUBERNETES_MASTER_URL = property("kubernetes-master", STRING); 124 | 125 | /** 126 | *

Configuration key: api-token

127 | * Defines an oauth token for the kubernetes client to access the kubernetes REST API. Defaults to reading the 128 | * token from the auto-injected file at: /var/run/secrets/kubernetes.io/serviceaccount/token 129 | */ 130 | public static final PropertyDefinition KUBERNETES_API_TOKEN = property("api-token", STRING); 131 | 132 | /** 133 | * Configuration key: ca-certificate 134 | * CA Authority certificate from Kubernetes Master, defaults to reading the certificate from the auto-injected file at: 135 | * /var/run/secrets/kubernetes.io/serviceaccount/ca.crt 136 | */ 137 | public static final PropertyDefinition KUBERNETES_CA_CERTIFICATE = property("ca-certificate", STRING); 138 | 139 | /** 140 | *

Configuration key: service-port

141 | * If specified with a value greater than 0, its value defines the endpoint port of the service (overriding the default). 142 | */ 143 | public static final PropertyDefinition SERVICE_PORT = property("service-port", INTEGER); 144 | 145 | // Prevent instantiation 146 | private KubernetesProperties() { 147 | } 148 | 149 | private static PropertyDefinition property(String key, TypeConverter typeConverter) { 150 | return new SimplePropertyDefinition(key, true, typeConverter); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/main/java/com/hazelcast/kubernetes/RestClient.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * 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 | package com.hazelcast.kubernetes; 18 | 19 | import com.hazelcast.logging.ILogger; 20 | import com.hazelcast.logging.Logger; 21 | import com.hazelcast.internal.nio.IOUtil; 22 | 23 | import javax.net.ssl.HttpsURLConnection; 24 | import javax.net.ssl.SSLContext; 25 | import javax.net.ssl.SSLSocketFactory; 26 | import javax.net.ssl.TrustManagerFactory; 27 | import java.io.ByteArrayInputStream; 28 | import java.io.DataOutputStream; 29 | import java.io.IOException; 30 | import java.io.InputStream; 31 | import java.net.HttpURLConnection; 32 | import java.net.URL; 33 | import java.nio.charset.StandardCharsets; 34 | import java.security.KeyStore; 35 | import java.security.cert.Certificate; 36 | import java.security.cert.CertificateException; 37 | import java.security.cert.CertificateFactory; 38 | import java.util.ArrayList; 39 | import java.util.Collection; 40 | import java.util.List; 41 | import java.util.Scanner; 42 | 43 | /** 44 | * Utility class for making REST calls. 45 | */ 46 | final class RestClient { 47 | private static final ILogger LOGGER = Logger.getLogger(RestClient.class); 48 | 49 | private static final int HTTP_OK = 200; 50 | 51 | private final String url; 52 | private final List
headers = new ArrayList
(); 53 | private String body; 54 | private String caCertificate; 55 | 56 | private RestClient(String url) { 57 | this.url = url; 58 | } 59 | 60 | static RestClient create(String url) { 61 | return new RestClient(url); 62 | } 63 | 64 | RestClient withHeader(String key, String value) { 65 | headers.add(new Header(key, value)); 66 | return this; 67 | } 68 | 69 | RestClient withBody(String body) { 70 | this.body = body; 71 | return this; 72 | } 73 | 74 | RestClient withCaCertificates(String caCertificate) { 75 | this.caCertificate = caCertificate; 76 | return this; 77 | } 78 | 79 | String get() { 80 | return call("GET"); 81 | } 82 | 83 | String post() { 84 | return call("POST"); 85 | } 86 | 87 | private String call(String method) { 88 | HttpURLConnection connection = null; 89 | DataOutputStream outputStream = null; 90 | try { 91 | URL urlToConnect = new URL(url); 92 | connection = (HttpURLConnection) urlToConnect.openConnection(); 93 | if (connection instanceof HttpsURLConnection) { 94 | ((HttpsURLConnection) connection).setSSLSocketFactory(buildSslSocketFactory()); 95 | } 96 | connection.setRequestMethod(method); 97 | for (Header header : headers) { 98 | connection.setRequestProperty(header.getKey(), header.getValue()); 99 | } 100 | if (body != null) { 101 | byte[] bodyData = body.getBytes(StandardCharsets.UTF_8); 102 | 103 | connection.setDoOutput(true); 104 | connection.setRequestProperty("charset", "utf-8"); 105 | connection.setRequestProperty("Content-Length", Integer.toString(bodyData.length)); 106 | 107 | outputStream = new DataOutputStream(connection.getOutputStream()); 108 | outputStream.write(bodyData); 109 | outputStream.flush(); 110 | } 111 | 112 | checkHttpOk(method, connection); 113 | return read(connection.getInputStream()); 114 | } catch (IOException e) { 115 | throw new RestClientException("Failure in executing REST call", e); 116 | } finally { 117 | if (connection != null) { 118 | connection.disconnect(); 119 | } 120 | if (outputStream != null) { 121 | try { 122 | outputStream.close(); 123 | } catch (IOException e) { 124 | LOGGER.finest("Error while closing HTTP output stream", e); 125 | } 126 | } 127 | } 128 | } 129 | 130 | private void checkHttpOk(String method, HttpURLConnection connection) 131 | throws IOException { 132 | if (connection.getResponseCode() != HTTP_OK) { 133 | String errorMessage; 134 | try { 135 | errorMessage = read(connection.getErrorStream()); 136 | } catch (Exception e) { 137 | throw new RestClientException( 138 | String.format("Failure executing: %s at: %s", method, url), connection.getResponseCode()); 139 | } 140 | throw new RestClientException(String.format("Failure executing: %s at: %s. Message: %s", method, url, errorMessage), 141 | connection.getResponseCode()); 142 | 143 | } 144 | } 145 | 146 | private static String read(InputStream stream) { 147 | if (stream == null) { 148 | return ""; 149 | } 150 | Scanner scanner = new Scanner(stream, "UTF-8"); 151 | scanner.useDelimiter("\\Z"); 152 | return scanner.next(); 153 | } 154 | 155 | private static final class Header { 156 | private final String key; 157 | private final String value; 158 | 159 | private Header(String key, String value) { 160 | this.key = key; 161 | this.value = value; 162 | } 163 | 164 | private String getKey() { 165 | return key; 166 | } 167 | 168 | private String getValue() { 169 | return value; 170 | } 171 | } 172 | 173 | /** 174 | * Builds SSL Socket Factory with the public CA Certificate from Kubernetes Master. 175 | */ 176 | private SSLSocketFactory buildSslSocketFactory() { 177 | try { 178 | KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); 179 | keyStore.load(null, null); 180 | 181 | int i = 0; 182 | for (Certificate certificate : generateCertificates()) { 183 | String alias = String.format("ca-%d", i++); 184 | keyStore.setCertificateEntry(alias, certificate); 185 | } 186 | 187 | TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); 188 | tmf.init(keyStore); 189 | 190 | SSLContext context = SSLContext.getInstance("TLSv1.2"); 191 | context.init(null, tmf.getTrustManagers(), null); 192 | return context.getSocketFactory(); 193 | 194 | } catch (Exception e) { 195 | throw new KubernetesClientException("Failure in generating SSLSocketFactory", e); 196 | } 197 | } 198 | 199 | /** 200 | * Generates CA Certificate from the default CA Cert file or from the externally provided "ca-certificate" property. 201 | */ 202 | private Collection generateCertificates() 203 | throws IOException, CertificateException { 204 | InputStream caInput = null; 205 | try { 206 | CertificateFactory cf = CertificateFactory.getInstance("X.509"); 207 | caInput = new ByteArrayInputStream(caCertificate.getBytes(StandardCharsets.UTF_8)); 208 | return cf.generateCertificates(caInput); 209 | } finally { 210 | IOUtil.closeResource(caInput); 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/main/java/com/hazelcast/kubernetes/RestClientException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * 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 | package com.hazelcast.kubernetes; 18 | 19 | /** 20 | * Exception to indicate any issues while executing a REST call. 21 | */ 22 | class RestClientException 23 | extends RuntimeException { 24 | private int httpErrorCode; 25 | 26 | RestClientException(String message, int httpErrorCode) { 27 | super(String.format("%s. HTTP Error Code: %s", message, httpErrorCode)); 28 | this.httpErrorCode = httpErrorCode; 29 | } 30 | 31 | RestClientException(String message, Throwable cause) { 32 | super(message, cause); 33 | } 34 | 35 | int getHttpErrorCode() { 36 | return httpErrorCode; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/hazelcast/kubernetes/RetryUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * 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 | package com.hazelcast.kubernetes; 18 | 19 | import com.hazelcast.core.HazelcastException; 20 | import com.hazelcast.logging.ILogger; 21 | import com.hazelcast.logging.Logger; 22 | 23 | import java.util.List; 24 | import java.util.concurrent.Callable; 25 | 26 | /** 27 | * Static utility class to retry operations related to connecting to Kubernetes master. 28 | */ 29 | final class RetryUtils { 30 | static final long INITIAL_BACKOFF_MS = 1500L; 31 | static final long MAX_BACKOFF_MS = 5 * 60 * 1000L; 32 | static final double BACKOFF_MULTIPLIER = 1.5; 33 | 34 | private static final ILogger LOGGER = Logger.getLogger(RetryUtils.class); 35 | 36 | private static final long MS_IN_SECOND = 1000L; 37 | 38 | private RetryUtils() { 39 | } 40 | 41 | /** 42 | * Calls {@code callable.call()} until it does not throw an exception (but no more than {@code retries} times). 43 | *

44 | * Note that {@code callable} should be an idempotent operation which is a call to the Kubernetes master. 45 | *

46 | * If {@code callable} throws an unchecked exception, it is wrapped into {@link HazelcastException}. 47 | */ 48 | public static T retry(Callable callable, int retries, List nonRetryableKeywords) { 49 | int retryCount = 0; 50 | while (true) { 51 | try { 52 | return callable.call(); 53 | } catch (Exception e) { 54 | retryCount++; 55 | if (retryCount > retries || containsAnyOf(e, nonRetryableKeywords)) { 56 | throw unchecked(e); 57 | } 58 | long waitIntervalMs = backoffIntervalForRetry(retryCount); 59 | LOGGER.warning( 60 | String.format("Couldn't discover Hazelcast members using Kubernetes API, [%s] retrying in %s seconds...", 61 | retryCount, waitIntervalMs / MS_IN_SECOND)); 62 | sleep(waitIntervalMs); 63 | } 64 | } 65 | } 66 | 67 | private static RuntimeException unchecked(Exception e) { 68 | if (e instanceof RuntimeException) { 69 | return (RuntimeException) e; 70 | } 71 | return new HazelcastException(e); 72 | } 73 | 74 | private static boolean containsAnyOf(Exception e, List nonRetryableKeywords) { 75 | Throwable currentException = e; 76 | while (currentException != null) { 77 | String exceptionMessage = currentException.getMessage(); 78 | for (String keyword : nonRetryableKeywords) { 79 | if (exceptionMessage != null && exceptionMessage.contains(keyword)) { 80 | return true; 81 | } 82 | } 83 | currentException = currentException.getCause(); 84 | } 85 | return false; 86 | } 87 | 88 | private static long backoffIntervalForRetry(int retryCount) { 89 | long result = INITIAL_BACKOFF_MS; 90 | for (int i = 1; i < retryCount; i++) { 91 | result *= BACKOFF_MULTIPLIER; 92 | if (result > MAX_BACKOFF_MS) { 93 | return MAX_BACKOFF_MS; 94 | } 95 | } 96 | return result; 97 | } 98 | 99 | private static void sleep(long millis) { 100 | try { 101 | Thread.sleep(millis); 102 | } catch (InterruptedException e) { 103 | Thread.currentThread().interrupt(); 104 | throw new HazelcastException(e); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/hazelcast/kubernetes/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * 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 | * Provides interfaces/classes for Hazelcast Kubernetes Discovery Plugin 19 | */ 20 | package com.hazelcast.kubernetes; 21 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.hazelcast.spi.discovery.DiscoveryStrategyFactory: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # 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 | com.hazelcast.kubernetes.HazelcastKubernetesDiscoveryStrategyFactory -------------------------------------------------------------------------------- /src/test/java/com/hazelcast/kubernetes/DnsEndpointResolverTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * 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 | package com.hazelcast.kubernetes; 18 | 19 | import com.hazelcast.logging.ILogger; 20 | import com.hazelcast.logging.NoLogFactory; 21 | import com.hazelcast.spi.discovery.DiscoveryNode; 22 | import org.junit.Before; 23 | import org.junit.Test; 24 | import org.junit.runner.RunWith; 25 | import org.mockito.invocation.InvocationOnMock; 26 | import org.mockito.stubbing.Answer; 27 | import org.powermock.api.mockito.PowerMockito; 28 | import org.powermock.core.classloader.annotations.PrepareForTest; 29 | import org.powermock.modules.junit4.PowerMockRunner; 30 | 31 | import java.net.InetAddress; 32 | import java.net.UnknownHostException; 33 | import java.util.HashSet; 34 | import java.util.List; 35 | import java.util.Set; 36 | 37 | import static org.junit.Assert.assertEquals; 38 | import static org.mockito.Mockito.any; 39 | import static org.mockito.Mockito.anyString; 40 | import static org.mockito.Mockito.mock; 41 | import static org.mockito.Mockito.never; 42 | import static org.mockito.Mockito.verify; 43 | import static org.mockito.Mockito.when; 44 | 45 | @RunWith(PowerMockRunner.class) 46 | @PrepareForTest(DnsEndpointResolver.class) 47 | public class DnsEndpointResolverTest { 48 | private static final ILogger LOGGER = new NoLogFactory().getLogger("no"); 49 | 50 | private static final String SERVICE_DNS = "my-release-hazelcast.default.svc.cluster.local"; 51 | private static final int DEFAULT_SERVICE_DNS_TIMEOUT_SECONDS = 5; 52 | private static final int TEST_DNS_TIMEOUT_SECONDS = 1; 53 | private static final int UNSET_PORT = 0; 54 | private static final int DEFAULT_PORT = 5701; 55 | private static final int CUSTOM_PORT = 5702; 56 | private static final String IP_SERVER_1 = "192.168.0.5"; 57 | private static final String IP_SERVER_2 = "192.168.0.6"; 58 | 59 | @Before 60 | public void setUp() 61 | throws Exception { 62 | PowerMockito.mockStatic(InetAddress.class); 63 | 64 | InetAddress address1 = mock(InetAddress.class); 65 | InetAddress address2 = mock(InetAddress.class); 66 | when(address1.getHostAddress()).thenReturn(IP_SERVER_1); 67 | when(address2.getHostAddress()).thenReturn(IP_SERVER_2); 68 | PowerMockito.when(InetAddress.getAllByName(SERVICE_DNS)).thenReturn(new InetAddress[]{address1, address2}); 69 | } 70 | 71 | @Test 72 | public void resolve() { 73 | // given 74 | DnsEndpointResolver dnsEndpointResolver = new DnsEndpointResolver(LOGGER, SERVICE_DNS, UNSET_PORT, DEFAULT_SERVICE_DNS_TIMEOUT_SECONDS); 75 | 76 | // when 77 | List result = dnsEndpointResolver.resolve(); 78 | 79 | // then 80 | 81 | Set resultAddresses = setOf(result.get(0).getPrivateAddress().getHost(), result.get(1).getPrivateAddress().getHost()); 82 | Set resultPorts = setOf(result.get(0).getPrivateAddress().getPort(), result.get(1).getPrivateAddress().getPort()); 83 | assertEquals(setOf(IP_SERVER_1, IP_SERVER_2), resultAddresses); 84 | assertEquals(setOf(DEFAULT_PORT), resultPorts); 85 | } 86 | 87 | @Test 88 | public void resolveCustomPort() { 89 | // given 90 | DnsEndpointResolver dnsEndpointResolver = new DnsEndpointResolver(LOGGER, SERVICE_DNS, CUSTOM_PORT, DEFAULT_SERVICE_DNS_TIMEOUT_SECONDS); 91 | 92 | // when 93 | List result = dnsEndpointResolver.resolve(); 94 | 95 | // then 96 | 97 | Set resultAddresses = setOf(result.get(0).getPrivateAddress().getHost(), result.get(1).getPrivateAddress().getHost()); 98 | Set resultPorts = setOf(result.get(0).getPrivateAddress().getPort(), result.get(1).getPrivateAddress().getPort()); 99 | assertEquals(setOf(IP_SERVER_1, IP_SERVER_2), resultAddresses); 100 | assertEquals(setOf(CUSTOM_PORT), resultPorts); 101 | } 102 | 103 | @Test 104 | public void resolveException() 105 | throws Exception { 106 | // given 107 | ILogger logger = mock(ILogger.class); 108 | PowerMockito.when(InetAddress.getAllByName(SERVICE_DNS)).thenThrow(new UnknownHostException()); 109 | DnsEndpointResolver dnsEndpointResolver = new DnsEndpointResolver(logger, SERVICE_DNS, UNSET_PORT, DEFAULT_SERVICE_DNS_TIMEOUT_SECONDS); 110 | 111 | // when 112 | List result = dnsEndpointResolver.resolve(); 113 | 114 | // then 115 | assertEquals(0, result.size()); 116 | verify(logger).warning(String.format("DNS lookup for serviceDns '%s' failed: unknown host", SERVICE_DNS)); 117 | verify(logger, never()).warning(anyString(), any(Throwable.class)); 118 | } 119 | 120 | @Test 121 | public void resolveNotFound() 122 | throws Exception { 123 | // given 124 | PowerMockito.when(InetAddress.getAllByName(SERVICE_DNS)).thenReturn(new InetAddress[0]); 125 | DnsEndpointResolver dnsEndpointResolver = new DnsEndpointResolver(LOGGER, SERVICE_DNS, UNSET_PORT, DEFAULT_SERVICE_DNS_TIMEOUT_SECONDS); 126 | 127 | // when 128 | List result = dnsEndpointResolver.resolve(); 129 | 130 | // then 131 | assertEquals(0, result.size()); 132 | } 133 | 134 | @Test 135 | public void resolveTimeout() 136 | throws Exception { 137 | // given 138 | ILogger logger = mock(ILogger.class); 139 | PowerMockito.when(InetAddress.getAllByName(SERVICE_DNS)).then(waitAndAnswer()); 140 | DnsEndpointResolver dnsEndpointResolver = new DnsEndpointResolver(logger, SERVICE_DNS, UNSET_PORT, TEST_DNS_TIMEOUT_SECONDS); 141 | 142 | // when 143 | List result = dnsEndpointResolver.resolve(); 144 | 145 | // then 146 | assertEquals(0, result.size()); 147 | verify(logger).warning(String.format("DNS lookup for serviceDns '%s' failed: DNS resolution timeout", SERVICE_DNS)); 148 | verify(logger, never()).warning(anyString(), any(Throwable.class)); 149 | } 150 | 151 | private static Answer waitAndAnswer() { 152 | return new Answer() { 153 | @Override 154 | public InetAddress[] answer(InvocationOnMock invocation) throws Throwable { 155 | Thread.sleep(TEST_DNS_TIMEOUT_SECONDS * 5 * 1000); 156 | return new InetAddress[0]; 157 | } 158 | }; 159 | } 160 | 161 | private static Set setOf(Object... objects) { 162 | Set result = new HashSet(); 163 | for (Object object : objects) { 164 | result.add(object); 165 | } 166 | return result; 167 | } 168 | } -------------------------------------------------------------------------------- /src/test/java/com/hazelcast/kubernetes/HazelcastKubernetesDiscoveryStrategyFactoryTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * 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 | package com.hazelcast.kubernetes; 18 | 19 | import com.hazelcast.config.properties.PropertyDefinition; 20 | import com.hazelcast.logging.ILogger; 21 | import com.hazelcast.logging.NoLogFactory; 22 | import com.hazelcast.spi.discovery.DiscoveryNode; 23 | import com.hazelcast.spi.discovery.DiscoveryStrategy; 24 | import com.hazelcast.spi.discovery.DiscoveryStrategyFactory.DiscoveryStrategyLevel; 25 | import org.junit.Before; 26 | import org.junit.Test; 27 | import org.junit.runner.RunWith; 28 | import org.mockito.Mock; 29 | import org.mockito.Mockito; 30 | import org.powermock.api.mockito.PowerMockito; 31 | import org.powermock.core.classloader.annotations.PrepareForTest; 32 | import org.powermock.modules.junit4.PowerMockRunner; 33 | 34 | import java.io.File; 35 | import java.util.Collection; 36 | import java.util.HashMap; 37 | 38 | import static org.junit.Assert.assertEquals; 39 | import static org.junit.Assert.assertTrue; 40 | import static org.powermock.api.mockito.PowerMockito.mock; 41 | 42 | @RunWith(PowerMockRunner.class) 43 | @PrepareForTest({KubernetesApiEndpointResolver.class, HazelcastKubernetesDiscoveryStrategyFactory.class}) 44 | public class HazelcastKubernetesDiscoveryStrategyFactoryTest { 45 | 46 | private static final ILogger LOGGER = new NoLogFactory().getLogger("no"); 47 | private static final String API_TOKEN = "token"; 48 | private static final String CA_CERTIFICATE = "ca-certificate"; 49 | 50 | @Mock 51 | DiscoveryNode discoveryNode; 52 | 53 | @Mock 54 | private KubernetesClient client; 55 | 56 | @Before 57 | public void setup() 58 | throws Exception { 59 | PowerMockito.whenNew(KubernetesClient.class).withAnyArguments().thenReturn(client); 60 | } 61 | 62 | @Test 63 | public void checkDiscoveryStrategyType() { 64 | HazelcastKubernetesDiscoveryStrategyFactory factory = new HazelcastKubernetesDiscoveryStrategyFactory(); 65 | Class strategyType = factory.getDiscoveryStrategyType(); 66 | assertEquals(HazelcastKubernetesDiscoveryStrategy.class.getCanonicalName(), strategyType.getCanonicalName()); 67 | } 68 | 69 | @Test 70 | public void checkProperties() { 71 | HazelcastKubernetesDiscoveryStrategyFactory factory = new HazelcastKubernetesDiscoveryStrategyFactory(); 72 | Collection propertyDefinitions = factory.getConfigurationProperties(); 73 | assertTrue(propertyDefinitions.contains(KubernetesProperties.SERVICE_DNS)); 74 | assertTrue(propertyDefinitions.contains(KubernetesProperties.SERVICE_PORT)); 75 | } 76 | 77 | @Test 78 | public void createDiscoveryStrategy() { 79 | HashMap properties = new HashMap(); 80 | properties.put(KubernetesProperties.KUBERNETES_API_TOKEN.key(), API_TOKEN); 81 | properties.put(KubernetesProperties.KUBERNETES_CA_CERTIFICATE.key(), CA_CERTIFICATE); 82 | properties.put(String.valueOf(KubernetesProperties.SERVICE_PORT), 333); 83 | properties.put(KubernetesProperties.NAMESPACE.key(), "default"); 84 | HazelcastKubernetesDiscoveryStrategyFactory factory = new HazelcastKubernetesDiscoveryStrategyFactory(); 85 | DiscoveryStrategy strategy = factory.newDiscoveryStrategy(discoveryNode, LOGGER, properties); 86 | assertTrue(strategy instanceof HazelcastKubernetesDiscoveryStrategy); 87 | strategy.start(); 88 | strategy.destroy(); 89 | } 90 | 91 | @Test 92 | public void autoDetection() throws Exception { 93 | // given 94 | File mockFile = mock(File.class); 95 | Mockito.doReturn(true).when(mockFile).exists(); 96 | PowerMockito.whenNew(File.class).withArguments("/var/run/secrets/kubernetes.io/serviceaccount/token") 97 | .thenReturn(mockFile); 98 | HazelcastKubernetesDiscoveryStrategyFactory factory = new HazelcastKubernetesDiscoveryStrategyFactory(); 99 | 100 | // when & then 101 | assertTrue(factory.tokenFileExists()); 102 | assertEquals(DiscoveryStrategyLevel.PLATFORM, factory.discoveryStrategyLevel()); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/test/java/com/hazelcast/kubernetes/KubernetesApiEndpointResolverTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * 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 | package com.hazelcast.kubernetes; 18 | 19 | import com.hazelcast.kubernetes.KubernetesClient.Endpoint; 20 | import com.hazelcast.logging.ILogger; 21 | import com.hazelcast.logging.NoLogFactory; 22 | import com.hazelcast.spi.discovery.DiscoveryNode; 23 | import org.junit.Before; 24 | import org.junit.Test; 25 | import org.junit.runner.RunWith; 26 | import org.mockito.Mock; 27 | import org.powermock.api.mockito.PowerMockito; 28 | import org.powermock.core.classloader.annotations.PrepareForTest; 29 | import org.powermock.modules.junit4.PowerMockRunner; 30 | 31 | import java.util.Collections; 32 | import java.util.List; 33 | 34 | import static java.util.Arrays.asList; 35 | import static org.junit.Assert.assertEquals; 36 | import static org.mockito.BDDMockito.given; 37 | 38 | @RunWith(PowerMockRunner.class) 39 | @PrepareForTest(KubernetesApiEndpointResolver.class) 40 | public class KubernetesApiEndpointResolverTest { 41 | private static final ILogger LOGGER = new NoLogFactory().getLogger("no"); 42 | private static final String SERVICE_NAME = "serviceName"; 43 | private static final String SERVICE_LABEL = "serviceLabel"; 44 | private static final String SERVICE_LABEL_VALUE = "serviceLabelValue"; 45 | private static final String POD_LABEL = "podLabel"; 46 | private static final String POD_LABEL_VALUE = "podLabelValue"; 47 | private static final Boolean RESOLVE_NOT_READY_ADDRESSES = true; 48 | 49 | @Mock 50 | private KubernetesClient client; 51 | 52 | @Before 53 | public void setup() 54 | throws Exception { 55 | PowerMockito.whenNew(KubernetesClient.class).withAnyArguments().thenReturn(client); 56 | } 57 | 58 | @Test 59 | public void resolveWhenNodeInFound() { 60 | // given 61 | List endpoints = Collections.emptyList(); 62 | given(client.endpoints()).willReturn(endpoints); 63 | 64 | KubernetesApiEndpointResolver sut = new KubernetesApiEndpointResolver(LOGGER, null, 0, null, null, null, null, null, client); 65 | 66 | // when 67 | List nodes = sut.resolve(); 68 | 69 | // then 70 | assertEquals(0, nodes.size()); 71 | } 72 | 73 | @Test 74 | public void resolveWithServiceNameWhenNodeInNamespace() { 75 | resolveWithServiceNameWhenNodeInNamespace(0, 1); // expected port 1 is the kubernetes discovery endpoint port 76 | } 77 | 78 | @Test 79 | public void resolveWithServiceNameWhenNodeInNamespaceAndCustomPort() { 80 | resolveWithServiceNameWhenNodeInNamespace(333, 333); 81 | } 82 | 83 | private void resolveWithServiceNameWhenNodeInNamespace(final int port, final int expectedPort) { 84 | // given 85 | List endpoints = createEndpoints(1); 86 | given(client.endpointsByName(SERVICE_NAME)).willReturn(endpoints); 87 | 88 | KubernetesApiEndpointResolver sut = new KubernetesApiEndpointResolver(LOGGER, SERVICE_NAME, port, null, null, null, null, null, 89 | client); 90 | 91 | // when 92 | List nodes = sut.resolve(); 93 | 94 | // then 95 | assertEquals(1, nodes.size()); 96 | assertEquals(expectedPort, nodes.get(0).getPrivateAddress().getPort()); 97 | } 98 | 99 | @Test 100 | public void resolveWithServiceLabelWhenNodeWithServiceLabel() { 101 | // given 102 | List endpoints = createEndpoints(2); 103 | given(client.endpointsByServiceLabel(SERVICE_LABEL, SERVICE_LABEL_VALUE)).willReturn(endpoints); 104 | 105 | KubernetesApiEndpointResolver sut = new KubernetesApiEndpointResolver(LOGGER, null, 0, SERVICE_LABEL, SERVICE_LABEL_VALUE, 106 | null, null, null, client); 107 | 108 | // when 109 | List nodes = sut.resolve(); 110 | 111 | // then 112 | assertEquals(1, nodes.size()); 113 | assertEquals(2, nodes.get(0).getPrivateAddress().getPort()); 114 | } 115 | 116 | @Test 117 | public void resolveWithPodLabelWhenNodeWithPodLabel() { 118 | // given 119 | List endpoints = createEndpoints(2); 120 | given(client.endpointsByPodLabel(POD_LABEL, POD_LABEL_VALUE)).willReturn(endpoints); 121 | 122 | KubernetesApiEndpointResolver sut = new KubernetesApiEndpointResolver(LOGGER, null, 0, null, null, 123 | POD_LABEL, POD_LABEL_VALUE, null, client); 124 | 125 | // when 126 | List nodes = sut.resolve(); 127 | 128 | // then 129 | assertEquals(1, nodes.size()); 130 | assertEquals(2, nodes.get(0).getPrivateAddress().getPort()); 131 | } 132 | 133 | @Test 134 | public void resolveWithServiceNameWhenNotReadyAddressesAndNotReadyEnabled() { 135 | // given 136 | List endpoints = createNotReadyEndpoints(2); 137 | given(client.endpointsByName(SERVICE_NAME)).willReturn(endpoints); 138 | 139 | KubernetesApiEndpointResolver sut = new KubernetesApiEndpointResolver(LOGGER, SERVICE_NAME, 0, null, null, 140 | null, null, RESOLVE_NOT_READY_ADDRESSES, client); 141 | 142 | // when 143 | List nodes = sut.resolve(); 144 | 145 | // then 146 | assertEquals(1, nodes.size()); 147 | } 148 | 149 | @Test 150 | public void resolveWithServiceNameWhenNotReadyAddressesAndNotReadyDisabled() { 151 | // given 152 | List endpoints = createNotReadyEndpoints(2); 153 | given(client.endpointsByName(SERVICE_NAME)).willReturn(endpoints); 154 | 155 | KubernetesApiEndpointResolver sut = new KubernetesApiEndpointResolver(LOGGER, SERVICE_NAME, 0, null, null, null, null, null, 156 | client); 157 | 158 | // when 159 | List nodes = sut.resolve(); 160 | 161 | // then 162 | assertEquals(0, nodes.size()); 163 | } 164 | 165 | private static List createEndpoints(int customPort) { 166 | return asList(createEntrypointAddress(customPort, true)); 167 | } 168 | 169 | private static List createNotReadyEndpoints(int customPort) { 170 | return asList(createEntrypointAddress(customPort, false)); 171 | } 172 | 173 | private static Endpoint createEntrypointAddress(int customPort, boolean isReady) { 174 | String ip = "1.1.1.1"; 175 | return new Endpoint(new KubernetesClient.EndpointAddress(ip, customPort), isReady); 176 | } 177 | } -------------------------------------------------------------------------------- /src/test/java/com/hazelcast/kubernetes/KubernetesClientTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * 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 | package com.hazelcast.kubernetes; 18 | 19 | import com.github.tomakehurst.wiremock.client.MappingBuilder; 20 | import com.github.tomakehurst.wiremock.junit.WireMockRule; 21 | import com.hazelcast.kubernetes.KubernetesClient.Endpoint; 22 | import org.junit.Before; 23 | import org.junit.Rule; 24 | import org.junit.Test; 25 | 26 | import java.io.File; 27 | import java.util.ArrayList; 28 | import java.util.List; 29 | import java.util.Map; 30 | 31 | import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; 32 | import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; 33 | import static com.github.tomakehurst.wiremock.client.WireMock.get; 34 | import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; 35 | import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; 36 | import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching; 37 | import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching; 38 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; 39 | import static java.util.Collections.emptyList; 40 | import static java.util.Collections.singletonMap; 41 | import static org.hamcrest.MatcherAssert.assertThat; 42 | import static org.hamcrest.Matchers.containsInAnyOrder; 43 | import static org.junit.Assert.assertEquals; 44 | import static org.junit.Assert.assertTrue; 45 | 46 | public class KubernetesClientTest { 47 | private static final String KUBERNETES_MASTER_IP = "localhost"; 48 | 49 | private static final String TOKEN = "sample-token"; 50 | private static final String CA_CERTIFICATE = "sample-ca-certificate"; 51 | private static final String NAMESPACE = "sample-namespace"; 52 | private static final int RETRIES = 3; 53 | 54 | @Rule 55 | public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort()); 56 | 57 | private KubernetesClient kubernetesClient; 58 | 59 | @Before 60 | public void setUp() { 61 | kubernetesClient = newKubernetesClient(false); 62 | stubFor(get(urlMatching("/api/.*")).atPriority(5) 63 | .willReturn(aResponse().withStatus(401).withBody("\"reason\":\"Forbidden\""))); 64 | } 65 | 66 | @Test 67 | public void endpointsByNamespace() { 68 | // given 69 | //language=JSON 70 | String podsListResponse = "{\n" 71 | + " \"items\": [\n" 72 | + " {\n" 73 | + " \"spec\": {\n" 74 | + " \"containers\": [\n" 75 | + " {\n" 76 | + " \"ports\": [\n" 77 | + " {\n" 78 | + " \"containerPort\": 5701\n" 79 | + " }\n" 80 | + " ]\n" 81 | + " }\n" 82 | + " ]\n" 83 | + " },\n" 84 | + " \"status\": {\n" 85 | + " \"podIP\": \"192.168.0.25\",\n" 86 | + " \"containerStatuses\": [\n" 87 | + " {\n" 88 | + " \"ready\": true\n" 89 | + " }\n" 90 | + " ]\n" 91 | + " }\n" 92 | + " },\n" 93 | + " {\n" 94 | + " \"spec\": {\n" 95 | + " \"containers\": [\n" 96 | + " {\n" 97 | + " \"ports\": [\n" 98 | + " {\n" 99 | + " \"containerPort\": 5702\n" 100 | + " }\n" 101 | + " ]\n" 102 | + " }\n" 103 | + " ]\n" 104 | + " },\n" 105 | + " \"status\": {\n" 106 | + " \"podIP\": \"172.17.0.5\",\n" 107 | + " \"containerStatuses\": [\n" 108 | + " {\n" 109 | + " \"ready\": true\n" 110 | + " }\n" 111 | + " ]\n" 112 | + " }\n" 113 | + " },\n" 114 | + " {\n" 115 | + " \"spec\": {\n" 116 | + " \"containers\": [\n" 117 | + " {\n" 118 | + " \"ports\": [\n" 119 | + " {\n" 120 | + " }\n" 121 | + " ]\n" 122 | + " }\n" 123 | + " ]\n" 124 | + " },\n" 125 | + " \"status\": {\n" 126 | + " \"podIP\": \"172.17.0.6\",\n" 127 | + " \"containerStatuses\": [\n" 128 | + " {\n" 129 | + " \"ready\": false\n" 130 | + " }\n" 131 | + " ]\n" 132 | + " }\n" 133 | + " }\n" 134 | + " ]\n" 135 | + "}"; 136 | stub(String.format("/api/v1/namespaces/%s/pods", NAMESPACE), podsListResponse); 137 | 138 | // when 139 | List result = kubernetesClient.endpoints(); 140 | 141 | // then 142 | assertThat(format(result), 143 | containsInAnyOrder(ready("192.168.0.25", 5701), ready("172.17.0.5", 5702), notReady("172.17.0.6", null))); 144 | } 145 | 146 | @Test 147 | public void endpointsByNamespaceAndServiceLabel() { 148 | // given 149 | //language=JSON 150 | String endpointsListResponse = "{\n" 151 | + " \"kind\": \"EndpointsList\",\n" 152 | + " \"items\": [\n" 153 | + " {\n" 154 | + " \"subsets\": [\n" 155 | + " {\n" 156 | + " \"addresses\": [\n" 157 | + " {\n" 158 | + " \"ip\": \"192.168.0.25\",\n" 159 | + " \"hazelcast-service-port\": 5701\n" 160 | + " }\n" 161 | + " ]\n" 162 | + " }\n" 163 | + " ]\n" 164 | + " },\n" 165 | + " {\n" 166 | + " \"subsets\": [\n" 167 | + " {\n" 168 | + " \"addresses\": [\n" 169 | + " {\n" 170 | + " \"ip\": \"172.17.0.5\",\n" 171 | + " \"hazelcast-service-port\": 5702\n" 172 | + " }\n" 173 | + " ],\n" 174 | + " \"notReadyAddresses\": [\n" 175 | + " {\n" 176 | + " \"ip\": \"172.17.0.6\"\n" 177 | + " }\n" 178 | + " ],\n" 179 | + " \"ports\": [\n" 180 | + " {\n" 181 | + " \"port\": 5701\n" 182 | + " }\n" 183 | + " ]\n" 184 | + " }\n" 185 | + " ]\n" 186 | + " }\n" 187 | + " ]\n" 188 | + "}"; 189 | String serviceLabel = "sample-service-label"; 190 | String serviceLabelValue = "sample-service-label-value"; 191 | Map queryParams = singletonMap("labelSelector", String.format("%s=%s", serviceLabel, serviceLabelValue)); 192 | stub(String.format("/api/v1/namespaces/%s/endpoints", NAMESPACE), queryParams, endpointsListResponse); 193 | 194 | // when 195 | List result = kubernetesClient.endpointsByServiceLabel(serviceLabel, serviceLabelValue); 196 | 197 | // then 198 | assertThat(format(result), 199 | containsInAnyOrder(ready("192.168.0.25", 5701), ready("172.17.0.5", 5702), notReady("172.17.0.6", 5701))); 200 | } 201 | 202 | @Test 203 | public void endpointsByNamespaceAndServiceName() { 204 | // given 205 | //language=JSON 206 | String endpointResponse = "{\n" 207 | + " \"kind\": \"Endpoints\",\n" 208 | + " \"subsets\": [\n" 209 | + " {\n" 210 | + " \"addresses\": [\n" 211 | + " {\n" 212 | + " \"ip\": \"192.168.0.25\",\n" 213 | + " \"hazelcast-service-port\": 5701\n" 214 | + " },\n" 215 | + " {\n" 216 | + " \"ip\": \"172.17.0.5\",\n" 217 | + " \"hazelcast-service-port\": 5702\n" 218 | + " }\n" 219 | + " ]\n" 220 | + " }\n" 221 | + " ]\n" 222 | + "}"; 223 | String serviceName = "service-name"; 224 | stub(String.format("/api/v1/namespaces/%s/endpoints/%s", NAMESPACE, serviceName), endpointResponse); 225 | 226 | // when 227 | List result = kubernetesClient.endpointsByName(serviceName); 228 | 229 | // then 230 | assertThat(format(result), containsInAnyOrder(ready("192.168.0.25", 5701), ready("172.17.0.5", 5702))); 231 | } 232 | 233 | @Test 234 | public void endpointsByNamespaceAndPodLabel() { 235 | // given 236 | //language=JSON 237 | String podsListResponse = "{\n" 238 | + " \"kind\": \"PodList\",\n" 239 | + " \"items\": [\n" 240 | + " {\n" 241 | + " \"spec\": {\n" 242 | + " \"containers\": [\n" 243 | + " {\n" 244 | + " \"ports\": [\n" 245 | + " {\n" 246 | + " \"containerPort\": 5701\n" 247 | + " }\n" 248 | + " ]\n" 249 | + " }\n" 250 | + " ]\n" 251 | + " },\n" 252 | + " \"status\": {\n" 253 | + " \"podIP\": \"192.168.0.25\",\n" 254 | + " \"containerStatuses\": [\n" 255 | + " {\n" 256 | + " \"ready\": true\n" 257 | + " }\n" 258 | + " ]\n" 259 | + " }\n" 260 | + " },\n" 261 | + " {\n" 262 | + " \"spec\": {\n" 263 | + " \"containers\": [\n" 264 | + " {\n" 265 | + " \"ports\": [\n" 266 | + " {\n" 267 | + " \"containerPort\": 5702\n" 268 | + " }\n" 269 | + " ]\n" 270 | + " }\n" 271 | + " ]\n" 272 | + " },\n" 273 | + " \"status\": {\n" 274 | + " \"podIP\": \"172.17.0.5\",\n" 275 | + " \"containerStatuses\": [\n" 276 | + " {\n" 277 | + " \"ready\": true\n" 278 | + " }\n" 279 | + " ]\n" 280 | + " }\n" 281 | + " }\n" 282 | + " ]\n" 283 | + "}"; 284 | 285 | String podLabel = "sample-pod-label"; 286 | String podLabelValue = "sample-pod-label-value"; 287 | Map queryParams = singletonMap("labelSelector", String.format("%s=%s", podLabel, podLabelValue)); 288 | stub(String.format("/api/v1/namespaces/%s/pods", NAMESPACE, podLabel), queryParams, podsListResponse); 289 | 290 | // when 291 | List result = kubernetesClient.endpointsByPodLabel(podLabel, podLabelValue); 292 | 293 | // then 294 | assertThat(format(result), 295 | containsInAnyOrder(ready("192.168.0.25", 5701), ready("172.17.0.5", 5702))); 296 | } 297 | 298 | @Test 299 | public void zoneBeta() { 300 | // given 301 | String podName = "pod-name"; 302 | 303 | //language=JSON 304 | String podResponse = "{\n" 305 | + " \"kind\": \"Pod\",\n" 306 | + " \"spec\": {\n" 307 | + " \"nodeName\": \"node-name\"\n" 308 | + " }\n" 309 | + "}"; 310 | stub(String.format("/api/v1/namespaces/%s/pods/%s", NAMESPACE, podName), podResponse); 311 | 312 | //language=JSON 313 | String nodeResponse = "{\n" 314 | + " \"kind\": \"Node\",\n" 315 | + " \"metadata\": {\n" 316 | + " \"labels\": {\n" 317 | + " \"failure-domain.beta.kubernetes.io/region\": \"us-central1\",\n" 318 | + " \"failure-domain.beta.kubernetes.io/zone\": \"us-central1-a\"\n" 319 | + " }\n" 320 | + " }\n" 321 | + "}"; 322 | stub("/api/v1/nodes/node-name", nodeResponse); 323 | 324 | // when 325 | String zone = kubernetesClient.zone(podName); 326 | 327 | // then 328 | assertEquals("us-central1-a", zone); 329 | } 330 | 331 | @Test 332 | public void zoneFailureDomain() { 333 | // given 334 | String podName = "pod-name"; 335 | 336 | //language=JSON 337 | String podResponse = "{\n" 338 | + " \"kind\": \"Pod\",\n" 339 | + " \"spec\": {\n" 340 | + " \"nodeName\": \"node-name\"\n" 341 | + " }\n" 342 | + "}"; 343 | stub(String.format("/api/v1/namespaces/%s/pods/%s", NAMESPACE, podName), podResponse); 344 | 345 | //language=JSON 346 | String nodeResponse = "{\n" 347 | + " \"kind\": \"Node\",\n" 348 | + " \"metadata\": {\n" 349 | + " \"labels\": {\n" 350 | + " \"failure-domain.beta.kubernetes.io/region\": \"deprecated-region\",\n" 351 | + " \"failure-domain.beta.kubernetes.io/zone\": \"deprecated-zone\",\n" 352 | + " \"failure-domain.kubernetes.io/region\": \"us-central1\",\n" 353 | + " \"failure-domain.kubernetes.io/zone\": \"us-central1-a\"\n" 354 | + " }\n" 355 | + " }\n" 356 | + "}"; 357 | stub("/api/v1/nodes/node-name", nodeResponse); 358 | 359 | // when 360 | String zone = kubernetesClient.zone(podName); 361 | 362 | // then 363 | assertEquals("us-central1-a", zone); 364 | } 365 | 366 | 367 | @Test 368 | public void nodeName() { 369 | // given 370 | String podName = "pod-name"; 371 | 372 | //language=JSON 373 | String podResponse = "{\n" 374 | + " \"kind\": \"Pod\",\n" 375 | + " \"spec\": {\n" 376 | + " \"nodeName\": \"kubernetes-node-f0bbd602-f7cw\"\n" 377 | + " }\n" 378 | + "}"; 379 | stub(String.format("/api/v1/namespaces/%s/pods/%s", NAMESPACE, podName), podResponse); 380 | 381 | // when 382 | String nodeName = kubernetesClient.nodeName(podName); 383 | 384 | // then 385 | assertEquals("kubernetes-node-f0bbd602-f7cw", nodeName); 386 | } 387 | 388 | @Test 389 | public void zone() { 390 | // given 391 | String podName = "pod-name"; 392 | 393 | //language=JSON 394 | String podResponse = "{\n" 395 | + " \"kind\": \"Pod\",\n" 396 | + " \"spec\": {\n" 397 | + " \"nodeName\": \"node-name\"\n" 398 | + " }\n" 399 | + "}"; 400 | stub(String.format("/api/v1/namespaces/%s/pods/%s", NAMESPACE, podName), podResponse); 401 | 402 | //language=JSON 403 | String nodeResponse = "{\n" 404 | + " \"kind\": \"Node\",\n" 405 | + " \"metadata\": {\n" 406 | + " \"labels\": {\n" 407 | + " \"failure-domain.beta.kubernetes.io/region\": \"deprecated-region\",\n" 408 | + " \"failure-domain.beta.kubernetes.io/zone\": \"deprecated-zone\",\n" 409 | + " \"topology.kubernetes.io/region\": \"us-central1\",\n" 410 | + " \"topology.kubernetes.io/zone\": \"us-central1-a\"\n" 411 | + " }\n" 412 | + " }\n" 413 | + "}"; 414 | stub("/api/v1/nodes/node-name", nodeResponse); 415 | 416 | // when 417 | String zone = kubernetesClient.zone(podName); 418 | 419 | // then 420 | assertEquals("us-central1-a", zone); 421 | } 422 | 423 | @Test 424 | public void endpointsByNamespaceWithLoadBalancerPublicIp() { 425 | // given 426 | stub(String.format("/api/v1/namespaces/%s/pods", NAMESPACE), podsListResponse()); 427 | stub(String.format("/api/v1/namespaces/%s/endpoints", NAMESPACE), endpointsListResponse()); 428 | 429 | //language=JSON 430 | String serviceResponse1 = "{\n" 431 | + " \"kind\": \"Service\",\n" 432 | + " \"spec\": {\n" 433 | + " \"ports\": [\n" 434 | + " {\n" 435 | + " \"port\": 32123,\n" 436 | + " \"targetPort\": 5701,\n" 437 | + " \"nodePort\": 31916\n" 438 | + " }\n" 439 | + " ]\n" 440 | + " },\n" 441 | + " \"status\": {\n" 442 | + " \"loadBalancer\": {\n" 443 | + " \"ingress\": [\n" 444 | + " {\n" 445 | + " \"ip\": \"35.232.226.200\"\n" 446 | + " }\n" 447 | + " ]\n" 448 | + " }\n" 449 | + " }\n" 450 | + "}\n"; 451 | stub(String.format("/api/v1/namespaces/%s/services/service-0", NAMESPACE), serviceResponse1); 452 | 453 | //language=JSON 454 | String serviceResponse2 = "{\n" 455 | + " \"kind\": \"Service\",\n" 456 | + " \"spec\": {\n" 457 | + " \"ports\": [\n" 458 | + " {\n" 459 | + " \"port\": 32124,\n" 460 | + " \"targetPort\": 5701,\n" 461 | + " \"nodePort\": 31916\n" 462 | + " }\n" 463 | + " ]\n" 464 | + " },\n" 465 | + " \"status\": {\n" 466 | + " \"loadBalancer\": {\n" 467 | + " \"ingress\": [\n" 468 | + " {\n" 469 | + " \"ip\": \"35.232.226.201\"\n" 470 | + " }\n" 471 | + " ]\n" 472 | + " }\n" 473 | + " }\n" 474 | + "}"; 475 | stub(String.format("/api/v1/namespaces/%s/services/service-1", NAMESPACE), serviceResponse2); 476 | 477 | // when 478 | List result = kubernetesClient.endpoints(); 479 | 480 | // then 481 | assertThat(format(result), containsInAnyOrder(ready("192.168.0.25", 5701), ready("172.17.0.5", 5702))); 482 | assertThat(formatPublic(result), containsInAnyOrder(ready("35.232.226.200", 32123), ready("35.232.226.201", 32124))); 483 | } 484 | 485 | @Test 486 | public void endpointsByNamespaceWithNodePublicIp() { 487 | // given 488 | stub(String.format("/api/v1/namespaces/%s/pods", NAMESPACE), podsListResponse()); 489 | stub(String.format("/api/v1/namespaces/%s/endpoints", NAMESPACE), endpointsListResponse()); 490 | 491 | stub(String.format("/api/v1/namespaces/%s/services/service-0", NAMESPACE), nodePortService1Response()); 492 | stub(String.format("/api/v1/namespaces/%s/services/service-1", NAMESPACE), nodePortService2Response()); 493 | 494 | //language=JSON 495 | String nodeResponse1 = "{\n" 496 | + " \"kind\": \"Node\",\n" 497 | + " \"status\": {\n" 498 | + " \"addresses\": [\n" 499 | + " {\n" 500 | + " \"type\": \"InternalIP\",\n" 501 | + " \"address\": \"10.240.0.21\"\n" 502 | + " },\n" 503 | + " {\n" 504 | + " \"type\": \"ExternalIP\",\n" 505 | + " \"address\": \"35.232.226.200\"\n" 506 | + " }\n" 507 | + " ]\n" 508 | + " }\n" 509 | + "}\n"; 510 | stub("/api/v1/nodes/node-name-1", nodeResponse1); 511 | 512 | String nodeResponse2 = "{\n" 513 | + " \"kind\": \"Node\",\n" 514 | + " \"status\": {\n" 515 | + " \"addresses\": [\n" 516 | + " {\n" 517 | + " \"type\": \"InternalIP\",\n" 518 | + " \"address\": \"10.240.0.22\"\n" 519 | + " },\n" 520 | + " {\n" 521 | + " \"type\": \"ExternalIP\",\n" 522 | + " \"address\": \"35.232.226.201\"\n" 523 | + " }\n" 524 | + " ]\n" 525 | + " }\n" 526 | + "}\n"; 527 | stub("/api/v1/nodes/node-name-2", nodeResponse2); 528 | 529 | // when 530 | List result = kubernetesClient.endpoints(); 531 | 532 | // then 533 | assertThat(format(result), containsInAnyOrder(ready("192.168.0.25", 5701), ready("172.17.0.5", 5702))); 534 | assertThat(formatPublic(result), containsInAnyOrder(ready("35.232.226.200", 31916), ready("35.232.226.201", 31917))); 535 | } 536 | 537 | @Test 538 | public void endpointsByNamespaceWithNodeName() { 539 | // given 540 | // create KubernetesClient with useNodeNameAsExternalAddress=true 541 | kubernetesClient = newKubernetesClient(true); 542 | 543 | stub(String.format("/api/v1/namespaces/%s/pods", NAMESPACE), podsListResponse()); 544 | stub(String.format("/api/v1/namespaces/%s/endpoints", NAMESPACE), endpointsListResponse()); 545 | 546 | stub(String.format("/api/v1/namespaces/%s/services/service-0", NAMESPACE), nodePortService1Response()); 547 | stub(String.format("/api/v1/namespaces/%s/services/service-1", NAMESPACE), nodePortService2Response()); 548 | 549 | String forbiddenBody = "\"reason\":\"Forbidden\""; 550 | stub("/api/v1/nodes/node-name-1", 403, forbiddenBody); 551 | stub("/api/v1/nodes/node-name-2", 403, forbiddenBody); 552 | 553 | // when 554 | List result = kubernetesClient.endpoints(); 555 | 556 | // then 557 | assertThat(format(result), containsInAnyOrder(ready("192.168.0.25", 5701), ready("172.17.0.5", 5702))); 558 | assertThat(formatPublic(result), containsInAnyOrder(ready("node-name-1", 31916), ready("node-name-2", 31917))); 559 | } 560 | 561 | private static String podsListResponse() { 562 | //language=JSON 563 | return "{\n" 564 | + " \"kind\": \"PodList\",\n" 565 | + " \"items\": [\n" 566 | + " {\n" 567 | + " \"spec\": {\n" 568 | + " \"containers\": [\n" 569 | + " {\n" 570 | + " \"ports\": [\n" 571 | + " {\n" 572 | + " \"containerPort\": 5701\n" 573 | + " }\n" 574 | + " ]\n" 575 | + " }\n" 576 | + " ]\n" 577 | + " },\n" 578 | + " \"status\": {\n" 579 | + " \"podIP\": \"192.168.0.25\",\n" 580 | + " \"containerStatuses\": [\n" 581 | + " {\n" 582 | + " \"ready\": true\n" 583 | + " }\n" 584 | + " ]\n" 585 | + " }\n" 586 | + " },\n" 587 | + " {\n" 588 | + " \"spec\": {\n" 589 | + " \"containers\": [\n" 590 | + " {\n" 591 | + " \"ports\": [\n" 592 | + " {\n" 593 | + " \"containerPort\": 5702\n" 594 | + " }\n" 595 | + " ]\n" 596 | + " }\n" 597 | + " ]\n" 598 | + " },\n" 599 | + " \"status\": {\n" 600 | + " \"podIP\": \"172.17.0.5\",\n" 601 | + " \"containerStatuses\": [\n" 602 | + " {\n" 603 | + " \"ready\": true\n" 604 | + " }\n" 605 | + " ]\n" 606 | + " }\n" 607 | + " }\n" 608 | + " ]\n" 609 | + "}"; 610 | } 611 | 612 | private static String endpointsListResponse() { 613 | //language=JSON 614 | return "{\n" 615 | + " \"kind\": \"EndpointsList\",\n" 616 | + " \"items\": [\n" 617 | + " {\n" 618 | + " \"metadata\": {\n" 619 | + " \"name\": \"my-release-hazelcast\"\n" 620 | + " },\n" 621 | + " \"subsets\": [\n" 622 | + " {\n" 623 | + " \"addresses\": [\n" 624 | + " {\n" 625 | + " \"ip\": \"192.168.0.25\",\n" 626 | + " \"nodeName\": \"node-name-1\"\n" 627 | + " },\n" 628 | + " {\n" 629 | + " \"ip\": \"172.17.0.5\",\n" 630 | + " \"nodeName\": \"node-name-2\"\n" 631 | + " }\n" 632 | + " ],\n" 633 | + " \"ports\": [\n" 634 | + " {\n" 635 | + " \"port\": 5701\n" 636 | + " }\n" 637 | + " ]\n" 638 | + " }\n" 639 | + " ]\n" 640 | + " },\n" 641 | + " {\n" 642 | + " \"metadata\": {\n" 643 | + " \"name\": \"service-0\"\n" 644 | + " },\n" 645 | + " \"subsets\": [\n" 646 | + " {\n" 647 | + " \"addresses\": [\n" 648 | + " {\n" 649 | + " \"ip\": \"192.168.0.25\",\n" 650 | + " \"nodeName\": \"node-name-1\"\n" 651 | + " }\n" 652 | + " ],\n" 653 | + " \"ports\": [\n" 654 | + " {\n" 655 | + " \"port\": 5701\n" 656 | + " }\n" 657 | + " ]\n" 658 | + " }\n" 659 | + " ]\n" 660 | + " },\n" 661 | + " {\n" 662 | + " \"metadata\": {\n" 663 | + " \"name\": \"service-1\"\n" 664 | + " },\n" 665 | + " \"subsets\": [\n" 666 | + " {\n" 667 | + " \"addresses\": [\n" 668 | + " {\n" 669 | + " \"ip\": \"172.17.0.5\",\n" 670 | + " \"nodeName\": \"node-name-2\"\n" 671 | + " }\n" 672 | + " ],\n" 673 | + " \"ports\": [\n" 674 | + " {\n" 675 | + " \"port\": 5702\n" 676 | + " }\n" 677 | + " ]\n" 678 | + " }\n" 679 | + " ]\n" 680 | + " }\n" 681 | + " ]\n" 682 | + "}"; 683 | } 684 | 685 | private static String nodePortService1Response() { 686 | //language=JSON 687 | return "{\n" 688 | + " \"kind\": \"Service\",\n" 689 | + " \"spec\": {\n" 690 | + " \"ports\": [\n" 691 | + " {\n" 692 | + " \"port\": 32123,\n" 693 | + " \"targetPort\": 5701,\n" 694 | + " \"nodePort\": 31916\n" 695 | + " }\n" 696 | + " ]\n" 697 | + " }\n" 698 | + "}\n"; 699 | } 700 | 701 | private static String nodePortService2Response() { 702 | //language=JSON 703 | return "{\n" 704 | + " \"kind\": \"Service\",\n" 705 | + " \"spec\": {\n" 706 | + " \"ports\": [\n" 707 | + " {\n" 708 | + " \"port\": 32124,\n" 709 | + " \"targetPort\": 5701,\n" 710 | + " \"nodePort\": 31917\n" 711 | + " }\n" 712 | + " ]\n" 713 | + " }\n" 714 | + "}"; 715 | } 716 | 717 | @Test 718 | public void forbidden() { 719 | // given 720 | String forbiddenBody = "\"reason\":\"Forbidden\""; 721 | stub(String.format("/api/v1/namespaces/%s/pods", NAMESPACE), 403, forbiddenBody); 722 | 723 | // when 724 | List result = kubernetesClient.endpoints(); 725 | 726 | // then 727 | assertEquals(emptyList(), result); 728 | } 729 | 730 | @Test 731 | public void wrongApiToken() { 732 | // given 733 | String unauthorizedBody = "\"reason\":\"Unauthorized\""; 734 | stub(String.format("/api/v1/namespaces/%s/pods", NAMESPACE), 401, unauthorizedBody); 735 | 736 | // when 737 | List result = kubernetesClient.endpoints(); 738 | 739 | // then 740 | assertEquals(emptyList(), result); 741 | } 742 | 743 | @Test(expected = RestClientException.class) 744 | public void unknownException() { 745 | // given 746 | String notRetriedErrorBody = "\"reason\":\"Forbidden\""; 747 | stub(String.format("/api/v1/namespaces/%s/pods", NAMESPACE), 501, notRetriedErrorBody); 748 | 749 | // when 750 | kubernetesClient.endpoints(); 751 | } 752 | 753 | private KubernetesClient newKubernetesClient(boolean useNodeNameAsExternalAddress) { 754 | String kubernetesMasterUrl = String.format("http://%s:%d", KUBERNETES_MASTER_IP, wireMockRule.port()); 755 | return new KubernetesClient(NAMESPACE, kubernetesMasterUrl, TOKEN, CA_CERTIFICATE, RETRIES, useNodeNameAsExternalAddress); 756 | } 757 | 758 | private static List format(List addresses) { 759 | List result = new ArrayList(); 760 | for (Endpoint address : addresses) { 761 | String ip = address.getPrivateAddress().getIp(); 762 | Integer port = address.getPrivateAddress().getPort(); 763 | boolean isReady = address.isReady(); 764 | result.add(toString(ip, port, isReady)); 765 | } 766 | return result; 767 | } 768 | 769 | private static List formatPublic(List addresses) { 770 | List result = new ArrayList(); 771 | for (Endpoint address : addresses) { 772 | String ip = address.getPublicAddress().getIp(); 773 | Integer port = address.getPublicAddress().getPort(); 774 | boolean isReady = address.isReady(); 775 | result.add(toString(ip, port, isReady)); 776 | } 777 | return result; 778 | } 779 | 780 | private static void stub(String url, String response) { 781 | stub(url, 200, response); 782 | } 783 | 784 | private static void stub(String url, int status, String response) { 785 | stubFor(get(urlEqualTo(url)) 786 | .withHeader("Authorization", equalTo(String.format("Bearer %s", TOKEN))) 787 | .willReturn(aResponse().withStatus(status).withBody(response))); 788 | } 789 | 790 | private static void stub(String url, Map queryParams, String response) { 791 | MappingBuilder mappingBuilder = get(urlPathMatching(url)); 792 | for (String key : queryParams.keySet()) { 793 | mappingBuilder = mappingBuilder.withQueryParam(key, equalTo(queryParams.get(key))); 794 | } 795 | stubFor(mappingBuilder 796 | .withHeader("Authorization", equalTo(String.format("Bearer %s", TOKEN))) 797 | .willReturn(aResponse().withStatus(200).withBody(response))); 798 | } 799 | 800 | private static String ready(String ip, Integer port) { 801 | return toString(ip, port, true); 802 | } 803 | 804 | private static String notReady(String ip, Integer port) { 805 | return toString(ip, port, false); 806 | } 807 | 808 | private static String toString(String ip, Integer port, boolean isReady) { 809 | return String.format("%s:%s:%s", ip, port, isReady); 810 | } 811 | 812 | @Test 813 | public void rbacYamlFileExists() { 814 | // rbac.yaml file is mentioned in logs, so the file must exist in the repo 815 | assertTrue(new File("rbac.yaml").exists()); 816 | } 817 | } 818 | -------------------------------------------------------------------------------- /src/test/java/com/hazelcast/kubernetes/KubernetesConfigTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * 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 | package com.hazelcast.kubernetes; 18 | 19 | import com.hazelcast.config.InvalidConfigurationException; 20 | import com.hazelcast.internal.nio.IOUtil; 21 | import org.junit.Test; 22 | 23 | import java.io.BufferedWriter; 24 | import java.io.File; 25 | import java.io.FileOutputStream; 26 | import java.io.IOException; 27 | import java.io.OutputStreamWriter; 28 | import java.nio.charset.Charset; 29 | import java.nio.charset.StandardCharsets; 30 | import java.util.HashMap; 31 | import java.util.Map; 32 | import org.junit.runner.RunWith; 33 | import org.powermock.api.mockito.PowerMockito; 34 | import org.powermock.core.classloader.annotations.PrepareForTest; 35 | import org.powermock.modules.junit4.PowerMockRunner; 36 | 37 | import static com.hazelcast.kubernetes.KubernetesConfig.DiscoveryMode; 38 | import static com.hazelcast.kubernetes.KubernetesProperties.KUBERNETES_API_RETIRES; 39 | import static com.hazelcast.kubernetes.KubernetesProperties.KUBERNETES_API_TOKEN; 40 | import static com.hazelcast.kubernetes.KubernetesProperties.KUBERNETES_CA_CERTIFICATE; 41 | import static com.hazelcast.kubernetes.KubernetesProperties.NAMESPACE; 42 | import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_DNS; 43 | import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_DNS_TIMEOUT; 44 | import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_LABEL_NAME; 45 | import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_LABEL_VALUE; 46 | import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_NAME; 47 | import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_PORT; 48 | import static org.junit.Assert.assertEquals; 49 | import static org.junit.Assert.assertNull; 50 | import static org.powermock.api.mockito.PowerMockito.doReturn; 51 | 52 | @RunWith(PowerMockRunner.class) 53 | @PrepareForTest({KubernetesConfig.class}) 54 | public class KubernetesConfigTest { 55 | private static final String TEST_API_TOKEN = "api-token"; 56 | private static final String TEST_CA_CERTIFICATE = "ca-certificate"; 57 | private static final String TEST_NAMESPACE = "test"; 58 | 59 | @Test 60 | public void dnsLookupMode() { 61 | // given 62 | String serviceDns = "hazelcast.default.svc.cluster.local"; 63 | int serviceDnsTimeout = 10; 64 | int servicePort = 5703; 65 | 66 | Map properties = createProperties(); 67 | properties.put(SERVICE_DNS.key(), serviceDns); 68 | properties.put(SERVICE_DNS_TIMEOUT.key(), serviceDnsTimeout); 69 | properties.put(SERVICE_PORT.key(), servicePort); 70 | 71 | // when 72 | KubernetesConfig config = new KubernetesConfig(properties); 73 | 74 | // then 75 | assertEquals(DiscoveryMode.DNS_LOOKUP, config.getMode()); 76 | assertEquals(serviceDns, config.getServiceDns()); 77 | assertEquals(serviceDnsTimeout, config.getServiceDnsTimeout()); 78 | assertEquals(servicePort, config.getServicePort()); 79 | } 80 | 81 | @Test 82 | public void dnsLookupModeWithoutServiceAccountToken() { 83 | // given 84 | String serviceDns = "hazelcast.default.svc.cluster.local"; 85 | int serviceDnsTimeout = 10; 86 | int servicePort = 5703; 87 | 88 | Map properties = new HashMap<>(); 89 | properties.put(SERVICE_DNS.key(), serviceDns); 90 | properties.put(SERVICE_DNS_TIMEOUT.key(), serviceDnsTimeout); 91 | properties.put(SERVICE_PORT.key(), servicePort); 92 | 93 | // when 94 | KubernetesConfig config = new KubernetesConfig(properties); 95 | 96 | // then 97 | assertEquals(DiscoveryMode.DNS_LOOKUP, config.getMode()); 98 | assertEquals(serviceDns, config.getServiceDns()); 99 | assertEquals(serviceDnsTimeout, config.getServiceDnsTimeout()); 100 | assertEquals(servicePort, config.getServicePort()); 101 | assertNull(config.getKubernetesApiToken()); 102 | assertNull(config.getKubernetesCaCertificate()); 103 | } 104 | 105 | @Test 106 | public void kubernetesApiModeDefault() throws Exception { 107 | // given 108 | Map properties = createProperties(); 109 | 110 | // when 111 | KubernetesConfig config = new KubernetesConfig(properties); 112 | 113 | // then 114 | assertEquals(DiscoveryMode.KUBERNETES_API, config.getMode()); 115 | assertEquals("test", config.getNamespace()); 116 | assertEquals(true, config.isResolveNotReadyAddresses()); 117 | assertEquals(false, config.isUseNodeNameAsExternalAddress()); 118 | assertEquals(TEST_API_TOKEN, config.getKubernetesApiToken()); 119 | assertEquals(TEST_CA_CERTIFICATE, config.getKubernetesCaCertificate()); 120 | } 121 | 122 | @Test 123 | public void kubernetesApiModeServiceName() { 124 | // given 125 | String serviceName = "service-name"; 126 | Map properties = createProperties(); 127 | properties.put(SERVICE_NAME.key(), serviceName); 128 | 129 | // when 130 | KubernetesConfig config = new KubernetesConfig(properties); 131 | 132 | // then 133 | assertEquals(DiscoveryMode.KUBERNETES_API, config.getMode()); 134 | assertEquals(serviceName, config.getServiceName()); 135 | } 136 | 137 | @Test 138 | public void kubernetesApiModeServiceLabel() { 139 | // given 140 | String serviceLabelName = "service-label-name"; 141 | String serviceLabelValue = "service-label-value"; 142 | Map properties = createProperties(); 143 | properties.put(KubernetesProperties.SERVICE_LABEL_NAME.key(), serviceLabelName); 144 | properties.put(SERVICE_LABEL_VALUE.key(), serviceLabelValue); 145 | 146 | // when 147 | KubernetesConfig config = new KubernetesConfig(properties); 148 | 149 | // then 150 | assertEquals(DiscoveryMode.KUBERNETES_API, config.getMode()); 151 | assertEquals(serviceLabelName, config.getServiceLabelName()); 152 | assertEquals(serviceLabelValue, config.getServiceLabelValue()); 153 | } 154 | 155 | @Test 156 | public void kubernetesApiNodeNameAsExternalAddress() { 157 | // given 158 | Map properties = createProperties(); 159 | properties.put(KubernetesProperties.USE_NODE_NAME_AS_EXTERNAL_ADDRESS.key(), true); 160 | 161 | // when 162 | KubernetesConfig config = new KubernetesConfig(properties); 163 | 164 | // then 165 | assertEquals(true, config.isUseNodeNameAsExternalAddress()); 166 | } 167 | 168 | @Test 169 | public void readTokenCertificateAndNamespaceFromFilesWhenPropertiesNotSet() throws Exception { 170 | // given 171 | PowerMockito.spy(KubernetesConfig.class); 172 | doReturn("token-xyz") 173 | .when(KubernetesConfig.class, "readFileContents", "/var/run/secrets/kubernetes.io/serviceaccount/token"); 174 | doReturn("certificate-xyz") 175 | .when(KubernetesConfig.class, "readFileContents", "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"); 176 | doReturn("namespace-xyz") 177 | .when(KubernetesConfig.class, "readFileContents", "/var/run/secrets/kubernetes.io/serviceaccount/namespace"); 178 | Map properties = new HashMap(); 179 | 180 | // when 181 | KubernetesConfig config = new KubernetesConfig(properties); 182 | 183 | // then 184 | assertEquals("certificate-xyz", config.getKubernetesCaCertificate()); 185 | assertEquals("token-xyz", config.getKubernetesApiToken()); 186 | assertEquals("namespace-xyz", config.getNamespace()); 187 | } 188 | 189 | @Test(expected = InvalidConfigurationException.class) 190 | public void invalidConfigurationBothModesConfigured() { 191 | // given 192 | Map properties = createProperties(); 193 | properties.put(SERVICE_NAME.key(), "service-name"); 194 | properties.put(SERVICE_DNS.key(), "service-dns"); 195 | 196 | // when 197 | new KubernetesConfig(properties); 198 | 199 | // then 200 | // throws exception 201 | } 202 | 203 | @Test(expected = InvalidConfigurationException.class) 204 | public void invalidConfigurationBothModesConfiguredServiceLabel() { 205 | // given 206 | Map properties = createProperties(); 207 | properties.put(SERVICE_LABEL_NAME.key(), "service-label-name"); 208 | properties.put(SERVICE_LABEL_VALUE.key(), "service-label-value"); 209 | properties.put(SERVICE_DNS.key(), "service-dns"); 210 | 211 | // when 212 | new KubernetesConfig(properties); 213 | 214 | // then 215 | // throws exception 216 | } 217 | 218 | @Test(expected = InvalidConfigurationException.class) 219 | public void invalidConfigurationBothServiceNameAndLabel() { 220 | // given 221 | Map properties = createProperties(); 222 | properties.put(SERVICE_NAME.key(), "service-name"); 223 | properties.put(SERVICE_LABEL_NAME.key(), "service-label-name"); 224 | properties.put(SERVICE_LABEL_VALUE.key(), "service-label-value"); 225 | 226 | // when 227 | new KubernetesConfig(properties); 228 | 229 | // then 230 | // throws exception 231 | } 232 | 233 | @Test(expected = InvalidConfigurationException.class) 234 | public void invalidServiceDnsTimeout() { 235 | // given 236 | Map properties = createProperties(); 237 | properties.put(SERVICE_DNS.key(), "service-dns"); 238 | properties.put(SERVICE_DNS_TIMEOUT.key(), -1); 239 | 240 | // when 241 | new KubernetesConfig(properties); 242 | 243 | // then 244 | // throws exception 245 | } 246 | 247 | @Test(expected = InvalidConfigurationException.class) 248 | public void invalidKubernetesApiRetries() { 249 | // given 250 | Map properties = createProperties(); 251 | properties.put(KUBERNETES_API_RETIRES.key(), -1); 252 | 253 | // when 254 | new KubernetesConfig(properties); 255 | 256 | // then 257 | // throws exception 258 | } 259 | 260 | @Test(expected = InvalidConfigurationException.class) 261 | public void invalidServicePort() { 262 | // given 263 | Map properties = createProperties(); 264 | properties.put(SERVICE_PORT.key(), -1); 265 | 266 | // when 267 | new KubernetesConfig(properties); 268 | 269 | // then 270 | // throws exception 271 | } 272 | 273 | private static Map createProperties() { 274 | Map properties = new HashMap(); 275 | // Predefined test properties 276 | properties.put(KUBERNETES_API_TOKEN.key(), TEST_API_TOKEN); 277 | properties.put(KUBERNETES_CA_CERTIFICATE.key(), TEST_CA_CERTIFICATE); 278 | properties.put(NAMESPACE.key(), TEST_NAMESPACE); 279 | return properties; 280 | } 281 | 282 | @Test 283 | public void readFileContents() 284 | throws IOException { 285 | String expectedContents = "Hello, world!\nThis is a test with Unicode ✓."; 286 | String testFile = createTestFile(expectedContents); 287 | String actualContents = KubernetesConfig.readFileContents(testFile); 288 | assertEquals(expectedContents, actualContents); 289 | } 290 | 291 | private static String createTestFile(String expectedContents) 292 | throws IOException { 293 | File temp = File.createTempFile("test", ".tmp"); 294 | temp.deleteOnExit(); 295 | BufferedWriter bufferedWriter = null; 296 | try { 297 | bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(temp), StandardCharsets.UTF_8)); 298 | bufferedWriter.write(expectedContents); 299 | } finally { 300 | IOUtil.closeResource(bufferedWriter); 301 | } 302 | return temp.getAbsolutePath(); 303 | } 304 | 305 | @Test 306 | public void propertyServiceNameIsEmpty() { 307 | // given 308 | Map properties = createProperties(); 309 | properties.put(SERVICE_NAME.key(), " "); 310 | String serviceDns = "service-dns"; 311 | properties.put(SERVICE_DNS.key(), serviceDns); 312 | 313 | //when 314 | KubernetesConfig config = new KubernetesConfig(properties); 315 | 316 | //then 317 | assertEquals(serviceDns, config.getServiceDns()); 318 | 319 | } 320 | 321 | @Test 322 | public void propertyServiceDnsIsNull() { 323 | // given 324 | Map properties = createProperties(); 325 | String serviceName = "service-name"; 326 | properties.put(SERVICE_NAME.key(), serviceName); 327 | properties.put(SERVICE_DNS.key(), null); 328 | 329 | //when 330 | KubernetesConfig config = new KubernetesConfig(properties); 331 | 332 | //then 333 | assertEquals(serviceName, config.getServiceName()); 334 | 335 | } 336 | 337 | @Test 338 | public void emptyProperties() { 339 | // given 340 | Map properties = createProperties(); 341 | properties.put(SERVICE_LABEL_NAME.key(), " "); 342 | String serviceLabelValue = "service-label-value"; 343 | properties.put(SERVICE_LABEL_VALUE.key(), serviceLabelValue); 344 | properties.put(SERVICE_DNS.key(), ""); 345 | 346 | //when 347 | KubernetesConfig config = new KubernetesConfig(properties); 348 | 349 | //then 350 | assertEquals(serviceLabelValue, config.getServiceLabelValue()); 351 | } 352 | } 353 | -------------------------------------------------------------------------------- /src/test/java/com/hazelcast/kubernetes/KubernetesPropertiesTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * 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 | package com.hazelcast.kubernetes; 18 | 19 | import com.hazelcast.test.HazelcastTestSupport; 20 | import org.junit.Test; 21 | 22 | public class KubernetesPropertiesTest 23 | extends HazelcastTestSupport { 24 | 25 | @Test 26 | public void privateConstructorTest() { 27 | assertUtilityConstructor(KubernetesProperties.class); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/com/hazelcast/kubernetes/RestClientTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * 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 | package com.hazelcast.kubernetes; 18 | 19 | import com.github.tomakehurst.wiremock.junit.WireMockRule; 20 | import org.junit.Before; 21 | import org.junit.Rule; 22 | import org.junit.Test; 23 | 24 | import javax.net.ssl.HostnameVerifier; 25 | import javax.net.ssl.HttpsURLConnection; 26 | import javax.net.ssl.SSLSession; 27 | import java.io.File; 28 | 29 | import static com.github.tomakehurst.wiremock.client.WireMock.*; 30 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig; 31 | import static com.hazelcast.kubernetes.KubernetesConfig.readFileContents; 32 | import static org.junit.Assert.assertEquals; 33 | 34 | public class RestClientTest { 35 | private static final String API_ENDPOINT = "/some/endpoint"; 36 | private static final String BODY_REQUEST = "some body request"; 37 | private static final String BODY_RESPONSE = "some body response"; 38 | 39 | @Rule 40 | public WireMockRule wireMockRule = new WireMockRule(wireMockConfig() 41 | .dynamicHttpsPort() 42 | .keystorePath(pathTo("keystore.jks")) 43 | ); 44 | 45 | private String address; 46 | 47 | @Before 48 | public void setUp() { 49 | // disable hostname HTTPS verification for testing 50 | HttpsURLConnection.setDefaultHostnameVerifier( 51 | new HostnameVerifier() { 52 | public boolean verify(String hostname, SSLSession sslSession) { 53 | return true; 54 | } 55 | }); 56 | address = String.format("https://localhost:%s", wireMockRule.httpsPort()); 57 | } 58 | 59 | @Test 60 | public void getSuccess() { 61 | // given 62 | stubFor(get(urlEqualTo(API_ENDPOINT)) 63 | .willReturn(aResponse().withStatus(200).withBody(BODY_RESPONSE))); 64 | 65 | // when 66 | String result = RestClient.create(String.format("%s%s", address, API_ENDPOINT)) 67 | .withCaCertificates(readFile("ca.crt")) 68 | .get(); 69 | 70 | // then 71 | assertEquals(BODY_RESPONSE, result); 72 | } 73 | 74 | @Test 75 | public void getWithHeaderSuccess() { 76 | // given 77 | String headerKey = "Metadata-Flavor"; 78 | String headerValue = "Google"; 79 | stubFor(get(urlEqualTo(API_ENDPOINT)) 80 | .withHeader(headerKey, equalTo(headerValue)) 81 | .willReturn(aResponse().withStatus(200).withBody(BODY_RESPONSE))); 82 | 83 | // when 84 | String result = RestClient.create(String.format("%s%s", address, API_ENDPOINT)) 85 | .withHeader(headerKey, headerValue) 86 | .withCaCertificates(readFile("ca.crt")) 87 | .get(); 88 | 89 | // then 90 | assertEquals(BODY_RESPONSE, result); 91 | } 92 | 93 | @Test(expected = RestClientException.class) 94 | public void getFailure() { 95 | // given 96 | stubFor(get(urlEqualTo(API_ENDPOINT)) 97 | .willReturn(aResponse().withStatus(500).withBody("Internal error"))); 98 | 99 | // when 100 | RestClient.create(String.format("%s%s", address, API_ENDPOINT)) 101 | .withCaCertificates(readFile("ca.crt")) 102 | .get(); 103 | 104 | // then 105 | // throw exception 106 | } 107 | 108 | @Test 109 | public void postSuccess() { 110 | // given 111 | stubFor(post(urlEqualTo(API_ENDPOINT)) 112 | .withRequestBody(equalTo(BODY_REQUEST)) 113 | .willReturn(aResponse().withStatus(200).withBody(BODY_RESPONSE))); 114 | 115 | // when 116 | String result = RestClient.create(String.format("%s%s", address, API_ENDPOINT)) 117 | .withBody(BODY_REQUEST) 118 | .withCaCertificates(readFile("ca.crt")) 119 | .post(); 120 | 121 | // then 122 | assertEquals(BODY_RESPONSE, result); 123 | } 124 | 125 | private String readFile(String filename) { 126 | return readFileContents(pathTo(filename)); 127 | } 128 | 129 | private String pathTo(String filename) { 130 | return new File(getClass().getClassLoader().getResource(filename).getFile()).getAbsolutePath(); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/test/java/com/hazelcast/kubernetes/RetryUtilsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2008-2020, Hazelcast, Inc. All Rights Reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * 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 | package com.hazelcast.kubernetes; 18 | 19 | import com.hazelcast.core.HazelcastException; 20 | import org.junit.Test; 21 | 22 | import java.util.Collections; 23 | import java.util.concurrent.Callable; 24 | 25 | import static com.hazelcast.kubernetes.RetryUtils.BACKOFF_MULTIPLIER; 26 | import static com.hazelcast.kubernetes.RetryUtils.INITIAL_BACKOFF_MS; 27 | import static java.util.Arrays.asList; 28 | import static org.junit.Assert.assertEquals; 29 | import static org.junit.Assert.assertTrue; 30 | import static org.mockito.BDDMockito.given; 31 | import static org.mockito.Mockito.mock; 32 | import static org.mockito.Mockito.times; 33 | import static org.mockito.Mockito.verify; 34 | 35 | public class RetryUtilsTest { 36 | private static final Integer RETRIES = 1; 37 | private static final String RESULT = "result string"; 38 | private static final String NON_RETRYABLE_KEYWORD = "\"reason\":\"Forbidden\""; 39 | 40 | private Callable callable = mock(Callable.class); 41 | 42 | @Test 43 | public void retryNoRetries() 44 | throws Exception { 45 | // given 46 | given(callable.call()).willReturn(RESULT); 47 | 48 | // when 49 | String result = RetryUtils.retry(callable, RETRIES, Collections.emptyList()); 50 | 51 | // then 52 | assertEquals(RESULT, result); 53 | verify(callable).call(); 54 | } 55 | 56 | @Test 57 | public void retryRetriesSuccessful() 58 | throws Exception { 59 | // given 60 | given(callable.call()).willThrow(new RuntimeException()).willReturn(RESULT); 61 | 62 | // when 63 | String result = RetryUtils.retry(callable, RETRIES, Collections.emptyList()); 64 | 65 | // then 66 | assertEquals(RESULT, result); 67 | verify(callable, times(2)).call(); 68 | } 69 | 70 | @Test(expected = RuntimeException.class) 71 | public void retryRetriesFailed() 72 | throws Exception { 73 | // given 74 | given(callable.call()).willThrow(new RuntimeException()).willThrow(new RuntimeException()).willReturn(RESULT); 75 | 76 | // when 77 | RetryUtils.retry(callable, RETRIES, Collections.emptyList()); 78 | 79 | // then 80 | // throws exception 81 | } 82 | 83 | @Test(expected = HazelcastException.class) 84 | public void retryRetriesFailedUncheckedException() 85 | throws Exception { 86 | // given 87 | given(callable.call()).willThrow(new Exception()).willThrow(new Exception()).willReturn(RESULT); 88 | 89 | // when 90 | RetryUtils.retry(callable, RETRIES, Collections.emptyList()); 91 | 92 | // then 93 | // throws exception 94 | } 95 | 96 | @Test 97 | public void retryRetriesWaitExponentialBackoff() 98 | throws Exception { 99 | // given 100 | double twoBackoffIntervalsMs = INITIAL_BACKOFF_MS + (BACKOFF_MULTIPLIER * INITIAL_BACKOFF_MS); 101 | given(callable.call()).willThrow(new RuntimeException()).willThrow(new RuntimeException()).willReturn(RESULT); 102 | 103 | // when 104 | long startTimeMs = System.currentTimeMillis(); 105 | RetryUtils.retry(callable, 5, Collections.emptyList()); 106 | long endTimeMs = System.currentTimeMillis(); 107 | 108 | // then 109 | assertTrue(twoBackoffIntervalsMs < (endTimeMs - startTimeMs)); 110 | } 111 | 112 | @Test(expected = NonRetryableException.class) 113 | public void retryNonRetryableKeyword() 114 | throws Exception { 115 | // given 116 | given(callable.call()).willThrow(new NonRetryableException()).willReturn(RESULT); 117 | 118 | // when 119 | RetryUtils.retry(callable, RETRIES, asList(NON_RETRYABLE_KEYWORD)); 120 | 121 | // then 122 | // throws exception 123 | } 124 | 125 | @Test(expected = RuntimeException.class) 126 | public void retryNonRetryableKeywordOnCause() 127 | throws Exception { 128 | // given 129 | given(callable.call()).willThrow(new RuntimeException(new NonRetryableException())).willReturn(RESULT); 130 | 131 | // when 132 | RetryUtils.retry(callable, RETRIES, asList(NON_RETRYABLE_KEYWORD)); 133 | 134 | // then 135 | // throws exception 136 | } 137 | 138 | private static class NonRetryableException 139 | extends RuntimeException { 140 | private NonRetryableException() { 141 | super(String.format("Message: %s", NON_RETRYABLE_KEYWORD)); 142 | } 143 | } 144 | } -------------------------------------------------------------------------------- /src/test/resources/ca.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDMjCCAhqgAwIBAgIIbACAU0ZsknAwDQYJKoZIhvcNAQELBQAwNzESMBAGA1UE 3 | CxMJb3BlbnNoaWZ0MSEwHwYDVQQDExhrdWJlLWFwaXNlcnZlci1sYi1zaWduZXIw 4 | HhcNMjAwMTIyMjE0MzE4WhcNMzAwMTE5MjE0MzE4WjA3MRIwEAYDVQQLEwlvcGVu 5 | c2hpZnQxITAfBgNVBAMTGGt1YmUtYXBpc2VydmVyLWxiLXNpZ25lcjCCASIwDQYJ 6 | KoZIhvcNAQEBBQADggEPADCCAQoCggEBANX8oMkxb4skmWGLXQxTbjYO50EZJ0uD 7 | AgCQs/LDCWmNSbUdjUsCSLqQCadSEiH7f4VqtYyCfQMfcf3vtaebLL/qZ4RSQy67 8 | xw6bYyN7Qh/ryNSPoczr0GXkrho4CYehRfnLaehUj6bGRe0Rhz5gisRMQ3jftKRj 9 | rY0FP18R4kbSyMVDZWtc82WGTUxXc2gyklTWzPUVLihHAJw/ip39TmmewvYqCYhh 10 | GGAZDbKVfVzZ9145/9K0EbVBRDn6lAWfh/44yrWy9cZAUidXFxLTo2YWoNzb5QJn 11 | YPt/EXhaANS3E5SPgQrlZsbxXKMfACTPWc2Y9Wb4ES89NNP1bl4DC4sCAwEAAaNC 12 | MEAwDgYDVR0PAQH/BAQDAgKkMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFJWe 13 | blS+7Fg7apqyu3sMraGZ34OoMA0GCSqGSIb3DQEBCwUAA4IBAQCu3BbKy3H85zEY 14 | hJTmCsRd4ZUyFjQd2D4Z0bZjdprbrW83P7UoHSA3giebvABrIzdrSANTfRLYTXCT 15 | LuxBwcY9IabxggL9Ki8p80NoKC4sWwLO3qFLGNFUTCOVU7gl7RHaHLVpHUoGFRh1 16 | kFPWnGnH1RniuY0vte8dYbUgKCzHdOW70ZdfxADwVwj1G31/KO93yO3BbX7d8IYa 17 | mDvHco/FQWxDK1MigAOCRBpPAEdj+wz1DfnCx9ZqVy5AV1XChoEkK1ZROzHIH+mc 18 | u5HFZn0ce0++aTZfnYVja7hGaFVf5rGi5i2NnACYbtT7rCXq9y/CsMb78HRly/En 19 | adDce2Y3 20 | -----END CERTIFICATE----- 21 | 22 | -----BEGIN CERTIFICATE----- 23 | MIICeDCCAeGgAwIBAgIECFbWTjANBgkqhkiG9w0BAQsFADBuMRAwDgYDVQQGEwdV 24 | bmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD 25 | VQQKEwdVbmtub3duMRAwDgYDVQQLEwdVbmtub3duMRIwEAYDVQQDEwlsb2NhbGhv 26 | c3QwIBcNMjAwMTI4MTU1MDQ4WhgPMjEyMDAxMDQxNTUwNDhaMG4xEDAOBgNVBAYT 27 | B1Vua25vd24xEDAOBgNVBAgTB1Vua25vd24xEDAOBgNVBAcTB1Vua25vd24xEDAO 28 | BgNVBAoTB1Vua25vd24xEDAOBgNVBAsTB1Vua25vd24xEjAQBgNVBAMTCWxvY2Fs 29 | aG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAqUga3JPPycHEsHS7LCRi 30 | zZ9nruarwTzKSGoLUtJEH5SgmFucK+cJs0SvXWVTJ4BRlsyOjHYxnJN1gBSbzLt9 31 | MkaoxzYfqPoQYuePOjFiBsmbbLBEk4yZhUB/ik4yLzu/aGUwuyJQayMNo3Th1WQ4 32 | 3qxzSdWAASPgBenOugiWTFECAwEAAaMhMB8wHQYDVR0OBBYEFARNaKaAcaGEuezm 33 | meCv76mqmPVdMA0GCSqGSIb3DQEBCwUAA4GBAGznOs7mw+Swnzy3jtmLms1ajRlb 34 | qhCQtd3d/I5NJYpq7z8n/5fkNx1orLcQ1cLQpoHQ4TIKjlrOlJnDp0m3i3fFyLv3 35 | /zguGfAg9Fh//uY2fqd1H0f6tCd4631rTqH1aN15OFfyRGIItxp2xO+NEl2yMyc8 36 | CyJtWtjG31l3ayBB 37 | -----END CERTIFICATE----- 38 | 39 | -----BEGIN CERTIFICATE----- 40 | MIIDQDCCAiigAwIBAgIIaDKbna6B3DMwDQYJKoZIhvcNAQELBQAwPjESMBAGA1UE 41 | CxMJb3BlbnNoaWZ0MSgwJgYDVQQDEx9rdWJlLWFwaXNlcnZlci1sb2NhbGhvc3Qt 42 | c2lnbmVyMB4XDTIwMDEyMjIxNDMxOFoXDTMwMDExOTIxNDMxOFowPjESMBAGA1UE 43 | CxMJb3BlbnNoaWZ0MSgwJgYDVQQDEx9rdWJlLWFwaXNlcnZlci1sb2NhbGhvc3Qt 44 | c2lnbmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEApAxgtdFW7XX3 45 | KPeWAIFK7+mT2YMRxZo/WEMVEU56duWQJQ5vSOr2nxvMvuvmTRbMDMouDk2xllsk 46 | AkB8osibpH4onK44RNXROLylnsEvANv7UhN1vAIfB5G6eoR/Lh6HMaeL/7jMUyxY 47 | bKjQgUqgtzaNunNfgrpgB6TN8Kl6PD0kN+/SXZF1+VBvoptei0utBa1PGbHmGSid 48 | 2dy96CUKnlJBISLwW4kPdI6j9bl8+wbf50YB4f5cyiaQY7AwhBwZX1QsZNy74mXw 49 | wNdre8YZF7ZwVpJ1Is7khFduEL5ya6PaU7/wNo5oejwkQ+4hDSptVl5/n3gJDhgg 50 | 3JMxufArrwIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAqQwDwYDVR0TAQH/BAUwAwEB 51 | /zAdBgNVHQ4EFgQUmGd2zDD68DdK2qOvVaOdL7jRGacwDQYJKoZIhvcNAQELBQAD 52 | ggEBAAejSalM9im0Q4BIXw1JBwVCXvkL9z1USE+wB2rf3oS/GScNnpP2eMTpSoF0 53 | U1ZYoHn/ARKKQX8SLmBmgUjasi7ZTZPpESh+hyMvAVyp7F8a1y0DiL4UIAksds4h 54 | 8jiW98paKuhQR3wAX54Q9n48LvusrRQVdEWyXsH30S1FkMawuzZgh5G0DvpaNgpP 55 | b0syMDonXJ2yOkAGCr3Cm7zfhbfX09hsgoWh8ynMahtE4vXEDE7k+S7Brukn7uac 56 | G/mUQBQYIjzRKgzPN54H0tCfTj+vDRYw9JuKEYPjOfK1+udMVx3zO/TjlnjOpH73 57 | phFZaYFEgK7MESMl9oqpHYe/GJQ= 58 | -----END CERTIFICATE----- 59 | -------------------------------------------------------------------------------- /src/test/resources/keystore.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hazelcast/hazelcast-kubernetes/3f3d1bc1df461b938947dd50722bba5ad8efe42e/src/test/resources/keystore.jks --------------------------------------------------------------------------------