├── .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 extends DiscoveryStrategy> 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 | *
Use Kubernetes API (/endpoints) to find dedicated services for each POD
301 | *
For each POD:
302 | *
303 | *
Use Kubernetes API (/services) to find the LoadBalancer External IP and Service Port
304 | *
If not found, then use Kubernetes API (/nodes) to find External IP of the Node
305 | *
306 | *
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 | *
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 | *
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 | *
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 extends Certificate> 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