Repository: hazelcast/hazelcast-kubernetes
Pull request #123 (2018-12-28 10:57:29)

Update README after release 1.4
This PR should be merged after releasing Kubernetes with ZONE_AWARE.

devOpsHazelcast: Test PASSed.

kairojya: It should be "distributed **equally**" instead of "equality".

> `ZONE_AWARE` feature works correctly when Hazelcast members are distributed equality in all zones, so your Kubernetes cluster must orchestrate PODs equally
leszko: Right @kairojya , thanks. I've changed it.
devOpsHazelcast: Test PASSed.

devOpsHazelcast: Test PASSed.

leszko: > I have some review comments. Can you please check them?

I addressed your comments. PTAL.

Patch:
diff --git a/README.md b/README.md
index 9ea0942..6b47a38 100644
--- a/README.md
+++ b/README.md
@@ -123,6 +123,10 @@ There are 4 properties to configure the plugin, all of them are optional.
  * `namespace`: Kubernetes Namespace where Hazelcast is running; if not specified, the value is taken from the environment variables `KUBERNETES_NAMESPACE` or `OPENSHIFT_BUILD_NAMESPACE`
  * `service-name`: service name used to scan only PODs connected to the given service; if not specified, then all PODs in the namespace are checked
  * `service-label-name`, `service-label-value`: service label and value used to tag services that should form the Hazelcast cluster together
+ * `resolve-not-ready-addresses`: if set to `true`, it checks also the addresses of PODs which are not ready; `false` by default 
+ * `kubernetes-master`: URL of Kubernetes Master; `https://kubernetes.default.svc` by default
+ * `api-token`: API Token to Kubernetes API; if not specified, the value is taken from the file `/var/run/secrets/kubernetes.io/serviceaccount/token`
+ * `ca-certificate`: CA Certificate for Kubernetes API; if not specified, the value is taken from the file `/var/run/secrets/kubernetes.io/serviceaccount/ca.crt`
  
 You should use either `service-name` or (`service-label-name` and `service-label-value`), specifying all 3 parameters does not make sense.
 
@@ -179,24 +183,49 @@ There are 2 properties to configure the plugin:
 
 **Note**: In this README, only XML configurations are presented, however you can achieve exactly the same effect using Java-based configurations.
 
-## Scaling Hazelcast cluster in Kubernetes
+### Zone Aware
 
-Hazelcast cluster is easily scalable within Kubernetes. You can use the standard `kubectl scale` command to change the cluster size.
+When using `ZONE_AWARE` configuration, backups are created in the other availability zone.
 
-Note however that, by default, Hazelcast does not shutdown gracefully. It means that if you suddenly scale down by more than your `backup-count` property (1 by default), you may lose the cluster data. To prevent that from happening, set the following properties:
-- `terminationGracePeriodSeconds`:  in your StatefulSet (or Deployment) configuration; the value should be high enough to cover the data migration process
-- `-Dhazelcast.shutdownhook.policy=GRACEFUL`: in the JVM parameters
-- `-Dhazelcast.graceful.shutdown.max.wait`: in the JVM parameters; the value should be high enough to cover the data migration process
+**Note**: Your Kubernetes cluster must orchestrate Hazelcast Member PODs equally between the availability zones, otherwise Zone Aware feature may not work correctly.
 
-The graceful shutdown configuration is already included in [Hazelcast Helm Charts](#helm-chart).
+#### XML Configuration
 
-## Plugin Usages
+```xml
+<partition-group enabled="true" group-type="ZONE_AWARE" />
+```
 
-Apart from embedding Hazelcast in your application as described above, there are multiple other scenarios of how to use the Hazelcast Kubernetes plugin.
+#### Java-based Configuration
 
-### Embedded Hazelcast Client
+```java
+config.getPartitionGroupConfig()
+    .setEnabled(true)
+    .setGroupType(MemberGroupType.ZONE_AWARE);
+```
+
+Note the following aspects of `ZONE_AWARE`:
+ * Kubernetes cluster must provide the [well-known Kubernetes annotations](https://kubernetes.io/docs/reference/kubernetes-api/labels-annotations-taints/#failure-domainbetakubernetesiozone)
+ * Retrieving Zone Name uses Kubernetes API, so RBAC must be configured as described [here](#granting-permissions-to-use-kubernetes-api)
+ * `ZONE_AWARE` feature works correctly when Hazelcast members are distributed equally in all zones, so your Kubernetes cluster must orchestrate PODs equally
+ 
+ Note also that retrieving Zone Name assumes that your container's hostname is the same as POD Name, which is almost always true. If you happen to change your hostname in the container, then please define the following environment variable:
+ 
+ ```yaml
+ 
+env:
+  - name: POD_NAME
+    valueFrom:
+      fieldRef:
+        fieldPath: metadata.name
+ ``` 
+
+## Hazelcast Client Configuration
 
-If you have a Hazelcast cluster deployed on Kubernetes, then you can configure Hazelcast Client (deployed on the same Kubernetes cluster). To do it, use exactly the same Maven/Gradle dependencies and the same Discovery Strategy extract in your Hazelcast Client configuration.
+Depending on whether your Hazelcast Client runs inside or outside the Kubernetes cluster, its configuration looks different.
+
+### Inside Kubernetes Cluster
+
+If you have a Hazelcast cluster deployed on Kubernetes and you want to configure Hazelcast Client deployed on the same Kubernetes cluster, then the configuration looks very similar to Hazelcast Member.
 
 Here's an example in case of the **Kubernetes API** mode.
 
@@ -217,6 +246,69 @@ clientConfig.getNetworkConfig().getKubernetesConfig().setEnabled(true)
             .setProperty("service-name", "MY-SERVICE-NAME");
 ```
 
+### Outside Kubernetes Cluster
+
+If your Hazelcast cluster is deployed on Kubernetes, but Hazelcast Client is in a completely different network, then they can connect only through the public Internet network.
+
+To use **Hazelcast Dummy Client** (`<smart-routing>false</smart-routing>`) you don't need any plugin, it's enough to expose your Hazelcast cluster with a LoadBalancer (or NodePort) service and set its IP/port as the TCP/IP Hazelcast Client configuration. Dummy Client, however, compromises the performance, since all the communication happens against a single Hazelcast member.
+
+To configure **Hazelcast Smart Client**, you need to perform the following steps:
+* Expose each Hazelcast Member POD with a separate LoadBalancer or NodePort service (the simplest way to do it is to install [Metacontroller](https://metacontroller.app/) and [service-per-pod](https://github.com/GoogleCloudPlatform/metacontroller/tree/master/examples/service-per-pod) Decorator Controller)
+* Configure ServiceAccount with ClusterRole having at least `get` and `list` permissions to the following resources: `endpoints`, `pods`, `nodes`, `services`
+* Use credentials from the created ServiceAccount in the Hazelcast Client configuration (credentials can be fetched with `kubectl get secret <sevice-account-secret> -o jsonpath={.data.token} | base64 --decode` and `kubectl get secret <sevice-account-secret> -o jsonpath={.data.ca\\.crt} | base64 --decode`)
+
+```xml
+<hazelcast-client>
+    <network>
+        <kubernetes enabled="true">
+            <namespace>MY-KUBERNETES-NAMESPACE</namespace>
+            <service-name>MY-SERVICE-NAME</service-name>
+            <use-public-ip>true</use-public-ip>
+            <kubernetes-master>https://35.226.182.228</kubernetes-master>
+            <api-token>eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJkZWZhdWx0Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZWNyZXQubmFtZSI6InNhbXBsZS1zZXJ2aWNlLWFjY291bnQtdG9rZW4tNnM5NGgiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC5uYW1lIjoic2FtcGxlLXNlcnZpY2UtYWNjb3VudCIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjI5OTI1NzBmLTI1NDQtMTFlOS1iNjg3LTQyMDEwYTgwMDI4YiIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDpkZWZhdWx0OnNhbXBsZS1zZXJ2aWNlLWFjY291bnQifQ.o-j4e-ducrMmQc23xYDnPr6TIyzlAs3pLNAmGLqPe9Vq1mwsxOh3ujcVKR90HAdkfHIF_Sw66qC9hXIDvxfqN_rLXlOKbvTX3gjDrAnyY_93Y3MpmSBj8yR9yHMb4O29a9UIwN5F2_VoCsc0IGumScU_EhPYc9mvEXlwp2bATQOEU-SVAGYPqvVPs9h5wjWZ7WUQa_-RBLMF6KRc9EP2i3c7dPSRVL9ZQ6k6OyUUOVEaPa1tqIxP7vOgx9Tg2C1KmYF5RDrlzrWkhEcjd4BLTiYDKEyaoBff9RqdPYlPwu0YcEH-F7yU8tTDN74KX5jvah3amg_zTiXeNoe5ZFcVdg</api-token>
+            <ca-certificate>
+                -----BEGIN CERTIFICATE-----
+                MIIDCzCCAfOgAwIBAgIQVcTHv3jK6g1l7Ph9Xyd9DTANBgkqhkiG9w0BAQsFADAv
+                MS0wKwYDVQQDEyQ4YjRhNjgwMS04NzJhLTQ2NDEtYjIwOC0zYjEyNDEwYWVkMTcw
+                HhcNMTkwMTMxMDcyNDMxWhcNMjQwMTMwMDgyNDMxWjAvMS0wKwYDVQQDEyQ4YjRh
+                NjgwMS04NzJhLTQ2NDEtYjIwOC0zYjEyNDEwYWVkMTcwggEiMA0GCSqGSIb3DQEB
+                AQUAA4IBDwAwggEKAoIBAQCaty8l9aHeWE1r9yLWKJMa3YQotVclYoEHegB8y6Ke
+                +zKqa06JKKrz3Qony97VdWR/NMpRYXouSF0owDv9BIoLTC682wlQtNB1c4pTVW7a
+                AikoNtyNIT8gtA5w0MyjFrbNslUblXvuo0HIeSmJREUmT7BC3VaKgkg64mVdf0DJ
+                NyrcL+qyCs1m03mi12hgzI72O3qgEtP91tu/oCUdOh39u13TB0fj5tgWURMFgkxo
+                T0xiNfPueV3pe8uYxBntzFn/74ibiizLRP6d/hsuRdS7IA+bvRLKG/paYwyZuMFb
+                BDA+kXXAIkOvCpIQCkAKMpyyDz9lBVCtl3eRSAJQLBefAgMBAAGjIzAhMA4GA1Ud
+                DwEB/wQEAwICBDAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBP
+                TBRY1IbkFJuboMKLW9tdpIzW7hf2qsTLOhtlaJbMXrWaXCTrl8qUgBUZ1sWAW9Uk
+                qETwRoCMl1Ht7PhbnXEGDNt3Sw3Y3feR4PsffhcgWH0BK8pZVY0Q1zbZ6dVNbU82
+                EUrrcnV0uiB/JFsJ3rg8qJurutro3uIzAhb9ixYRqYnXUR4q0bxahO04iSUHvtYQ
+                JmWp1GCb/ny9MyeTkwh2Q+WIQBHsX4LfrKjPwJd6qZME7BmwryYBTkGa0FinmhRg
+                SdSPEQKmuXmghPU5GLudiI2ooOaqOXIjVPfM/cw4uU9FCGM49qufccOOt6utk0SM
+                DwupAKLLiaYs47a8JgUa
+                -----END CERTIFICATE-----
+            </ca-certificate>
+        </kubernetes>
+    </network>
+</hazelcast-client>
+```
+
+**Note:** Hazelcast Client outside Kubernetes cluster works only in the **Kubernetes API** mode (it does not work in the **DNS Lookup** mode).
+
+## Scaling Hazelcast cluster in Kubernetes
+
+Hazelcast cluster is easily scalable within Kubernetes. You can use the standard `kubectl scale` command to change the cluster size.
+
+Note however that, by default, Hazelcast does not shutdown gracefully. It means that if you suddenly scale down by more than your `backup-count` property (1 by default), you may lose the cluster data. To prevent that from happening, set the following properties:
+- `terminationGracePeriodSeconds`:  in your StatefulSet (or Deployment) configuration; the value should be high enough to cover the data migration process
+- `-Dhazelcast.shutdownhook.policy=GRACEFUL`: in the JVM parameters
+- `-Dhazelcast.graceful.shutdown.max.wait`: in the JVM parameters; the value should be high enough to cover the data migration process
+
+The graceful shutdown configuration is already included in [Hazelcast Helm Charts](#helm-chart).
+
+## Plugin Usages
+
+Apart from embedding Hazelcast in your application as described above, there are multiple other scenarios of how to use the Hazelcast Kubernetes plugin.
+
 ### Docker images
 
 This plugin is included in the official Hazelcast Docker images:
diff --git a/pom.xml b/pom.xml
index faa5db4..96fb3c9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -29,7 +29,7 @@
 
   <groupId>com.hazelcast</groupId>
   <artifactId>hazelcast-kubernetes</artifactId>
-  <version>1.3.2-SNAPSHOT</version>
+  <version>1.4.1-SNAPSHOT</version>
   <name>Kubernetes Discovery Plugin for Hazelcast</name>
   <description>Kubernetes Service Discovery for Hazelcast Discovery SPI</description>
   <url>http://github.com/hazelcast/hazelcast-kubernetes</url>
@@ -556,4 +556,4 @@
       </build>
     </profile>
   </profiles>
-</project>
+</project>
\ No newline at end of file
diff --git a/src/main/java/com/hazelcast/kubernetes/DefaultKubernetesClient.java b/src/main/java/com/hazelcast/kubernetes/DefaultKubernetesClient.java
deleted file mode 100644
index 8e660c7..0000000
--- a/src/main/java/com/hazelcast/kubernetes/DefaultKubernetesClient.java
+++ /dev/null
@@ -1,310 +0,0 @@
-/*
- * Copyright (c) 2008-2018, Hazelcast, Inc. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.hazelcast.kubernetes;
-
-import com.hazelcast.internal.json.Json;
-import com.hazelcast.internal.json.JsonArray;
-import com.hazelcast.internal.json.JsonObject;
-import com.hazelcast.internal.json.JsonValue;
-import com.hazelcast.nio.IOUtil;
-
-import javax.net.ssl.HttpsURLConnection;
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.SSLSocketFactory;
-import javax.net.ssl.TrustManagerFactory;
-import java.io.ByteArrayInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.security.KeyStore;
-import java.security.cert.Certificate;
-import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.Scanner;
-import java.util.Set;
-
-import static java.util.Collections.EMPTY_MAP;
-
-/**
- * Responsible for connecting to the Kubernetes API.
- * <p>
- * Note: This client should always be used from inside Kubernetes since it depends on the CA Cert file, which exists
- * in the POD filesystem.
- *
- * @see <a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/">Kubernetes API</a>
- */
-class DefaultKubernetesClient
-        implements KubernetesClient {
-    private static final int HTTP_OK = 200;
-
-    private final String kubernetesMaster;
-    private final String apiToken;
-    private final String caCertificate;
-
-    DefaultKubernetesClient(String kubernetesMaster, String apiToken, String caCertificate) {
-        this.kubernetesMaster = kubernetesMaster;
-        this.apiToken = apiToken;
-        this.caCertificate = caCertificate;
-    }
-
-    @Override
-    public Endpoints endpoints(String namespace) {
-        String urlString = String.format("%s/api/v1/namespaces/%s/pods", kubernetesMaster, namespace);
-        return parsePodsList(callGet(urlString));
-
-    }
-
-    @Override
-    public Endpoints endpointsByLabel(String namespace, String serviceLabel, String serviceLabelValue) {
-        String param = String.format("labelSelector=%s=%s", serviceLabel, serviceLabelValue);
-        String urlString = String.format("%s/api/v1/namespaces/%s/endpoints?%s", kubernetesMaster, namespace, param);
-        return parseEndpointsList(callGet(urlString));
-    }
-
-    @Override
-    public Endpoints endpointsByName(String namespace, String endpointName) {
-        String urlString = String.format("%s/api/v1/namespaces/%s/endpoints/%s", kubernetesMaster, namespace, endpointName);
-        JsonObject json = callGet(urlString);
-        return parseEndpoint(json);
-    }
-
-    @Override
-    public String zone(String namespace, String podName) {
-        String podUrlString = String.format("%s/api/v1/namespaces/%s/pods/%s", kubernetesMaster, namespace, podName);
-        JsonObject podJson = callGet(podUrlString);
-        String nodeName = parseNodeName(podJson);
-
-        String nodeUrlString = String.format("%s/api/v1/nodes/%s", kubernetesMaster, nodeName);
-        JsonObject nodeJson = callGet(nodeUrlString);
-        return parseZone(nodeJson);
-    }
-
-    private JsonObject callGet(String urlString) {
-        HttpURLConnection connection = null;
-        try {
-            URL url = new URL(urlString);
-            connection = (HttpURLConnection) url.openConnection();
-            if (connection instanceof HttpsURLConnection) {
-                ((HttpsURLConnection) connection).setSSLSocketFactory(buildSslSocketFactory());
-            }
-            connection.setRequestMethod("GET");
-            connection.setRequestProperty("Authorization", String.format("Bearer %s", apiToken));
-
-            if (connection.getResponseCode() != HTTP_OK) {
-                throw new KubernetesClientException(String.format("Failure executing: GET at: %s. Message: %s,", urlString,
-                        read(connection.getErrorStream())));
-            }
-            return Json.parse(read(connection.getInputStream())).asObject();
-        } catch (Exception e) {
-            throw new KubernetesClientException("Failure in KubernetesClient", e);
-        } finally {
-            if (connection != null) {
-                connection.disconnect();
-            }
-        }
-    }
-
-    private static String read(InputStream stream) {
-        Scanner scanner = new Scanner(stream, "UTF-8");
-        scanner.useDelimiter("\\Z");
-        return scanner.next();
-    }
-
-    private static Endpoints parsePodsList(JsonObject json) {
-        List<EntrypointAddress> addresses = new ArrayList<EntrypointAddress>();
-        List<EntrypointAddress> notReadyAddresses = new ArrayList<EntrypointAddress>();
-
-        for (JsonValue item : toJsonArray(json.get("items"))) {
-            JsonObject status = item.asObject().get("status").asObject();
-            String ip = toString(status.get("podIP"));
-            if (ip != null) {
-                Integer port = getPortFromPodItem(item);
-                EntrypointAddress address = new EntrypointAddress(ip, port, EMPTY_MAP);
-
-                if (isReady(status)) {
-                    addresses.add(address);
-                } else {
-                    notReadyAddresses.add(address);
-                }
-            }
-        }
-
-        return new Endpoints(addresses, notReadyAddresses);
-    }
-
-    private static Integer getPortFromPodItem(JsonValue item) {
-        JsonArray containers = toJsonArray(item.asObject().get("spec").asObject().get("containers"));
-        // If multiple containers are in one POD, then use the default Hazelcast port from the configuration.
-        if (containers.size() == 1) {
-            JsonValue container = containers.get(0);
-            JsonArray ports = toJsonArray(container.asObject().get("ports"));
-            // If multiple ports are exposed by a container, then use the default Hazelcast port from the configuration.
-            if (ports.size() == 1) {
-                JsonValue port = ports.get(0);
-                JsonValue containerPort = port.asObject().get("containerPort");
-                if (containerPort != null && containerPort.isNumber()) {
-                    return containerPort.asInt();
-                }
-            }
-        }
-        return null;
-    }
-
-    private static boolean isReady(JsonObject status) {
-        for (JsonValue containerStatus : toJsonArray(status.get("containerStatuses"))) {
-            // If multiple containers are in one POD, then each needs to be ready.
-            if (!containerStatus.asObject().get("ready").asBoolean()) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    private static Endpoints parseEndpointsList(JsonObject json) {
-        List<EntrypointAddress> addresses = new ArrayList<EntrypointAddress>();
-        List<EntrypointAddress> notReadyAddresses = new ArrayList<EntrypointAddress>();
-
-        for (JsonValue object : toJsonArray(json.get("items"))) {
-            Endpoints endpoints = parseEndpoint(object);
-            addresses.addAll(endpoints.getAddresses());
-            notReadyAddresses.addAll(endpoints.getNotReadyAddresses());
-        }
-
-        return new Endpoints(addresses, notReadyAddresses);
-    }
-
-    private static Endpoints parseEndpoint(JsonValue endpointsJson) {
-        List<EntrypointAddress> addresses = new ArrayList<EntrypointAddress>();
-        List<EntrypointAddress> notReadyAddresses = new ArrayList<EntrypointAddress>();
-
-        for (JsonValue subset : toJsonArray(endpointsJson.asObject().get("subsets"))) {
-            for (JsonValue address : toJsonArray(subset.asObject().get("addresses"))) {
-                addresses.add(parseEntrypointAddress(address));
-            }
-            for (JsonValue notReadyAddress : toJsonArray(subset.asObject().get("notReadyAddresses"))) {
-                notReadyAddresses.add(parseEntrypointAddress(notReadyAddress));
-            }
-        }
-        return new Endpoints(addresses, notReadyAddresses);
-    }
-
-    private static EntrypointAddress parseEntrypointAddress(JsonValue endpointAddressJson) {
-        String ip = endpointAddressJson.asObject().get("ip").asString();
-        Integer port = getPortFromEndpointAddress(endpointAddressJson);
-        Map<String, Object> additionalProperties = parseAdditionalProperties(endpointAddressJson);
-        return new EntrypointAddress(ip, port, additionalProperties);
-    }
-
-    private static Integer getPortFromEndpointAddress(JsonValue endpointAddressJson) {
-        JsonValue servicePort = endpointAddressJson.asObject().get("hazelcast-service-port");
-        if (servicePort != null && servicePort.isNumber()) {
-            return servicePort.asInt();
-        }
-        return null;
-    }
-
-    private static Map<String, Object> parseAdditionalProperties(JsonValue endpointAddressJson) {
-        Set<String> knownFieldNames = new HashSet<String>(
-                Arrays.asList("ip", "nodeName", "targetRef", "hostname", "hazelcast-service-port"));
-
-        Map<String, Object> result = new HashMap<String, Object>();
-        Iterator<JsonObject.Member> iter = endpointAddressJson.asObject().iterator();
-        while (iter.hasNext()) {
-            JsonObject.Member member = iter.next();
-            if (!knownFieldNames.contains(member.getName())) {
-                result.put(member.getName(), toString(member.getValue()));
-            }
-        }
-        return result;
-    }
-
-    private static String parseNodeName(JsonObject json) {
-        return toString(json.get("spec").asObject().get("nodeName"));
-    }
-
-    private static String parseZone(JsonObject json) {
-        JsonObject labels = json.get("metadata").asObject().get("labels").asObject();
-        JsonValue zone = labels.get("failure-domain.kubernetes.io/zone");
-        if (zone != null) {
-            return toString(zone);
-        }
-        return toString(labels.get("failure-domain.beta.kubernetes.io/zone"));
-    }
-
-    private static JsonArray toJsonArray(JsonValue jsonValue) {
-        if (jsonValue == null || jsonValue.isNull()) {
-            return new JsonArray();
-        } else {
-            return jsonValue.asArray();
-        }
-    }
-
-    private static String toString(JsonValue jsonValue) {
-        if (jsonValue == null || jsonValue.isNull()) {
-            return null;
-        } else if (jsonValue.isString()) {
-            return jsonValue.asString();
-        } else {
-            return jsonValue.toString();
-        }
-    }
-
-    /**
-     * Builds SSL Socket Factory with the public CA Certificate from Kubernetes Master.
-     */
-    private SSLSocketFactory buildSslSocketFactory() {
-        try {
-            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
-            keyStore.load(null, null);
-            keyStore.setCertificateEntry("ca", generateCertificate());
-
-            TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
-            tmf.init(keyStore);
-
-            SSLContext context = SSLContext.getInstance("TLSv1.2");
-            context.init(null, tmf.getTrustManagers(), null);
-            return context.getSocketFactory();
-
-        } catch (Exception e) {
-            throw new KubernetesClientException("Failure in generating SSLSocketFactory", e);
-        }
-    }
-
-    /**
-     * Generates CA Certificate from the default CA Cert file or from the externally provided "ca-certificate" property.
-     */
-    private Certificate generateCertificate()
-            throws IOException, CertificateException {
-        InputStream caInput = null;
-        try {
-            CertificateFactory cf = CertificateFactory.getInstance("X.509");
-            caInput = new ByteArrayInputStream(caCertificate.getBytes("UTF-8"));
-            return cf.generateCertificate(caInput);
-        } finally {
-            IOUtil.closeResource(caInput);
-        }
-    }
-
-}
diff --git a/src/main/java/com/hazelcast/kubernetes/HazelcastKubernetesDiscoveryStrategy.java b/src/main/java/com/hazelcast/kubernetes/HazelcastKubernetesDiscoveryStrategy.java
index d6e2bb2..19cc2f3 100644
--- a/src/main/java/com/hazelcast/kubernetes/HazelcastKubernetesDiscoveryStrategy.java
+++ b/src/main/java/com/hazelcast/kubernetes/HazelcastKubernetesDiscoveryStrategy.java
@@ -94,12 +94,12 @@ final class HazelcastKubernetesDiscoveryStrategy
                 + "resolve-not-ready-addresses: " + resolveNotReadyAddresses + ", "
                 + "kubernetes-master: " + kubernetesMaster + "}");
 
-        client = buildKubernetesClient(kubernetesMaster, apiToken, caCertificate);
+        client = buildKubernetesClient(namespace, kubernetesMaster, apiToken, caCertificate);
         if (serviceDns != null) {
             endpointResolver = new DnsEndpointResolver(logger, serviceDns, port, serviceDnsTimeout);
         } else {
             endpointResolver = new KubernetesApiEndpointResolver(logger, serviceName, port, serviceLabel, serviceLabelValue,
-                    namespace, resolveNotReadyAddresses, client);
+                    resolveNotReadyAddresses, client);
         }
         logger.info("Kubernetes Discovery activated resolver: " + endpointResolver.getClass().getSimpleName());
     }
@@ -136,7 +136,7 @@ final class HazelcastKubernetesDiscoveryStrategy
             if (podName == null) {
                 podName = InetAddress.getLocalHost().getHostName();
             }
-            String zone = client.zone(namespace, podName);
+            String zone = client.zone(podName);
             if (zone != null) {
                 return zone;
             }
@@ -239,8 +239,9 @@ final class HazelcastKubernetesDiscoveryStrategy
         }
     }
 
-    private KubernetesClient buildKubernetesClient(String kubernetesMaster, String accessToken, String caCertificate) {
-        return new RetryKubernetesClient(new DefaultKubernetesClient(kubernetesMaster, accessToken, caCertificate));
+    private KubernetesClient buildKubernetesClient(String namespace, String kubernetesMaster, String accessToken,
+                                                   String caCertificate) {
+        return new KubernetesClient(namespace, kubernetesMaster, accessToken, caCertificate);
     }
 
     @SuppressFBWarnings("DMI_HARDCODED_ABSOLUTE_FILENAME")
diff --git a/src/main/java/com/hazelcast/kubernetes/KubernetesApiEndpointResolver.java b/src/main/java/com/hazelcast/kubernetes/KubernetesApiEndpointResolver.java
index df37789..7085aaf 100644
--- a/src/main/java/com/hazelcast/kubernetes/KubernetesApiEndpointResolver.java
+++ b/src/main/java/com/hazelcast/kubernetes/KubernetesApiEndpointResolver.java
@@ -17,8 +17,7 @@
 package com.hazelcast.kubernetes;
 
 import com.hazelcast.config.NetworkConfig;
-import com.hazelcast.kubernetes.KubernetesClient.Endpoints;
-import com.hazelcast.kubernetes.KubernetesClient.EntrypointAddress;
+import com.hazelcast.kubernetes.KubernetesClient.Endpoint;
 import com.hazelcast.logging.ILogger;
 import com.hazelcast.nio.Address;
 import com.hazelcast.spi.discovery.DiscoveryNode;
@@ -34,19 +33,17 @@ class KubernetesApiEndpointResolver
     private final String serviceName;
     private final String serviceLabel;
     private final String serviceLabelValue;
-    private final String namespace;
     private final Boolean resolveNotReadyAddresses;
     private final int port;
     private final KubernetesClient client;
 
     KubernetesApiEndpointResolver(ILogger logger, String serviceName, int port, String serviceLabel, String serviceLabelValue,
-                                  String namespace, Boolean resolveNotReadyAddresses, KubernetesClient client) {
+                                  Boolean resolveNotReadyAddresses, KubernetesClient client) {
 
         super(logger);
 
         this.serviceName = serviceName;
         this.port = port;
-        this.namespace = namespace;
         this.serviceLabel = serviceLabel;
         this.serviceLabelValue = serviceLabelValue;
         this.resolveNotReadyAddresses = resolveNotReadyAddresses;
@@ -56,49 +53,50 @@ class KubernetesApiEndpointResolver
     @Override
     List<DiscoveryNode> resolve() {
         if (serviceName != null && !serviceName.isEmpty()) {
-            return getSimpleDiscoveryNodes(client.endpointsByName(namespace, serviceName));
+            return getSimpleDiscoveryNodes(client.endpointsByName(serviceName));
         } else if (serviceLabel != null && !serviceLabel.isEmpty()) {
-            return getSimpleDiscoveryNodes(client.endpointsByLabel(namespace, serviceLabel, serviceLabelValue));
+            return getSimpleDiscoveryNodes(client.endpointsByLabel(serviceLabel, serviceLabelValue));
         }
-        return getSimpleDiscoveryNodes(client.endpoints(namespace));
+        return getSimpleDiscoveryNodes(client.endpoints());
     }
 
-    private List<DiscoveryNode> getSimpleDiscoveryNodes(Endpoints endpoints) {
+    private List<DiscoveryNode> getSimpleDiscoveryNodes(List<Endpoint> endpoints) {
         List<DiscoveryNode> discoveredNodes = new ArrayList<DiscoveryNode>();
-        resolveNotReadyAddresses(discoveredNodes, endpoints.getNotReadyAddresses());
-        resolveAddresses(discoveredNodes, endpoints.getAddresses());
+        for (Endpoint address : endpoints) {
+            addAddress(discoveredNodes, address);
+        }
         return discoveredNodes;
     }
 
-    private void resolveNotReadyAddresses(List<DiscoveryNode> discoveredNodes, List<EntrypointAddress> notReadyAddresses) {
-        if (Boolean.TRUE.equals(resolveNotReadyAddresses)) {
-            resolveAddresses(discoveredNodes, notReadyAddresses);
+    private void addAddress(List<DiscoveryNode> discoveredNodes, Endpoint endpoint) {
+        if (Boolean.TRUE.equals(resolveNotReadyAddresses) || endpoint.isReady()) {
+            Address privateAddress = createAddress(endpoint.getPrivateAddress());
+            Address publicAddress = createAddress(endpoint.getPublicAddress());
+            discoveredNodes
+                    .add(new SimpleDiscoveryNode(privateAddress, publicAddress, endpoint.getAdditionalProperties()));
+            if (logger.isFinestEnabled()) {
+                logger.finest(String.format("Found node service with addresses (private, public): %s, %s ", privateAddress,
+                        publicAddress));
+            }
         }
     }
 
-    private void resolveAddresses(List<DiscoveryNode> discoveredNodes, List<EntrypointAddress> addresses) {
-        for (EntrypointAddress address : addresses) {
-            addAddress(discoveredNodes, address);
+    private Address createAddress(KubernetesClient.EndpointAddress address) {
+        if (address == null) {
+            return null;
         }
-    }
-
-    private void addAddress(List<DiscoveryNode> discoveredNodes, EntrypointAddress entrypointAddress) {
-        String ip = entrypointAddress.getIp();
+        String ip = address.getIp();
         InetAddress inetAddress = mapAddress(ip);
-        int port = port(entrypointAddress);
-        Address address = new Address(inetAddress, port);
-        discoveredNodes.add(new SimpleDiscoveryNode(address, entrypointAddress.getAdditionalProperties()));
-        if (logger.isFinestEnabled()) {
-            logger.finest("Found node service with address: " + address);
-        }
+        int port = port(address);
+        return new Address(inetAddress, port);
     }
 
-    private int port(EntrypointAddress entrypointAddress) {
+    private int port(KubernetesClient.EndpointAddress address) {
         if (this.port > 0) {
             return this.port;
         }
-        if (entrypointAddress.getPort() != null) {
-            return entrypointAddress.getPort();
+        if (address.getPort() != null) {
+            return address.getPort();
         }
         return NetworkConfig.DEFAULT_PORT;
     }
diff --git a/src/main/java/com/hazelcast/kubernetes/KubernetesClient.java b/src/main/java/com/hazelcast/kubernetes/KubernetesClient.java
index 1f48efa..b2141be 100644
--- a/src/main/java/com/hazelcast/kubernetes/KubernetesClient.java
+++ b/src/main/java/com/hazelcast/kubernetes/KubernetesClient.java
@@ -16,97 +16,540 @@
 
 package com.hazelcast.kubernetes;
 
+import com.hazelcast.internal.json.Json;
+import com.hazelcast.internal.json.JsonArray;
+import com.hazelcast.internal.json.JsonObject;
+import com.hazelcast.internal.json.JsonValue;
+import com.hazelcast.logging.ILogger;
+import com.hazelcast.logging.Logger;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.Callable;
+
+import static java.util.Arrays.asList;
+import static java.util.Collections.emptyList;
 
 /**
  * Responsible for connecting to the Kubernetes API.
+ *
+ * @see <a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/">Kubernetes API</a>
  */
-interface KubernetesClient {
+class KubernetesClient {
+    private static final ILogger LOGGER = Logger.getLogger(KubernetesClient.class);
+
+    private static final int RETRIES = 10;
+    private static final List<String> NON_RETRYABLE_KEYWORDS = asList(
+            "\"reason\":\"Forbidden\"",
+            "\"reason\":\"Unauthorized\"",
+            "Failure in generating SSLSocketFactory");
+
+    private final String namespace;
+    private final String kubernetesMaster;
+    private final String apiToken;
+    private final String caCertificate;
+
+    private boolean isNoPublicIpAlreadyLogged;
+
+    KubernetesClient(String namespace, String kubernetesMaster, String apiToken, String caCertificate) {
+        this.namespace = namespace;
+        this.kubernetesMaster = kubernetesMaster;
+        this.apiToken = apiToken;
+        this.caCertificate = caCertificate;
+    }
 
     /**
-     * Retrieves POD addresses for all services in the given {@code namespace}.
+     * Retrieves POD addresses in the specified {@code namespace}.
      *
-     * @param namespace namespace name
-     * @return all POD addresses from the given {@code namespace}
+     * @return all POD addresses
      * @see <a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/#list-143">Kubernetes Endpoint API</a>
      */
-    Endpoints endpoints(String namespace);
+    List<Endpoint> endpoints() {
+        try {
+            String urlString = String.format("%s/api/v1/namespaces/%s/pods", kubernetesMaster, namespace);
+            return enrichWithPublicAddresses(parsePodsList(callGet(urlString)));
+        } catch (RestClientException e) {
+            return handleKnownException(e);
+        }
+    }
 
     /**
-     * Retrieves POD addresses for all services in the given {@code namespace} filtered by {@code serviceLabel}
+     * Retrieves POD addresses for all services in the specified {@code namespace} filtered by {@code serviceLabel}
      * and {@code serviceLabelValue}.
      *
-     * @param namespace         namespace name
      * @param serviceLabel      label used to filter responses
      * @param serviceLabelValue label value used to filter responses
-     * @return all POD addresses from the given {@code namespace} filtered by the label
+     * @return all POD addresses from the specified {@code namespace} filtered by the label
      * @see <a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/#list-143">Kubernetes Endpoint API</a>
      */
-    Endpoints endpointsByLabel(String namespace, String serviceLabel, String serviceLabelValue);
+    List<Endpoint> endpointsByLabel(String serviceLabel, String serviceLabelValue) {
+        try {
+            String param = String.format("labelSelector=%s=%s", serviceLabel, serviceLabelValue);
+            String urlString = String.format("%s/api/v1/namespaces/%s/endpoints?%s", kubernetesMaster, namespace, param);
+            return enrichWithPublicAddresses(parseEndpointsList(callGet(urlString)));
+        } catch (RestClientException e) {
+            return handleKnownException(e);
+        }
+    }
 
     /**
-     * Retrieves POD addresses from the given {@code namespace} and the given {@code endpointName}.
+     * Retrieves POD addresses from the specified {@code namespace} and the given {@code endpointName}.
      *
-     * @param namespace    namespace name
      * @param endpointName endpoint name
-     * @return all POD addresses from the given {@code namespace} and the given {@code endpointName}
+     * @return all POD addresses from the specified {@code namespace} and the given {@code endpointName}
      * @see <a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11/#list-143">Kubernetes Endpoint API</a>
      */
-    Endpoints endpointsByName(String namespace, String endpointName);
+    List<Endpoint> endpointsByName(String endpointName) {
+        try {
+            String urlString = String.format("%s/api/v1/namespaces/%s/endpoints/%s", kubernetesMaster, namespace, endpointName);
+            return enrichWithPublicAddresses(parseEndpoints(callGet(urlString)));
+        } catch (RestClientException e) {
+            return handleKnownException(e);
+        }
+    }
 
     /**
-     * Retrieves zone name for the given {@code namespace} and the given {@code podName}.
+     * Retrieves zone name for the specified {@code namespace} and the given {@code podName}.
      * <p>
-     * Note that the Kubernetes environment must provide such information as defined
+     * Note that the Kubernetes environment provides such information as defined
      * <a href="https://kubernetes.io/docs/reference/kubernetes-api/labels-annotations-taints">here</a>.
      *
-     * @param namespace namespace name
-     * @param podName   POD name
+     * @param podName POD name
      * @return zone name
      * @see <a href="https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.11">Kubernetes Endpoint API</a>
      */
-    String zone(String namespace, String podName);
+    String zone(String podName) {
+        String podUrlString = String.format("%s/api/v1/namespaces/%s/pods/%s", kubernetesMaster, namespace, podName);
+        String nodeName = extractNodeName(callGet(podUrlString));
+
+        String nodeUrlString = String.format("%s/api/v1/nodes/%s", kubernetesMaster, nodeName);
+        return extractZone(callGet(nodeUrlString));
+    }
+
+    private static List<Endpoint> parsePodsList(JsonObject podsListJson) {
+        List<Endpoint> addresses = new ArrayList<Endpoint>();
+
+        for (JsonValue item : toJsonArray(podsListJson.get("items"))) {
+            JsonObject status = item.asObject().get("status").asObject();
+            String ip = toString(status.get("podIP"));
+            if (ip != null) {
+                Integer port = extractContainerPort(item);
+                addresses.add(new Endpoint(new EndpointAddress(ip, port), isReady(status)));
+            }
+        }
+        return addresses;
+    }
+
+    private static Integer extractContainerPort(JsonValue podItemJson) {
+        JsonArray containers = toJsonArray(podItemJson.asObject().get("spec").asObject().get("containers"));
+        // If multiple containers are in one POD, then use the default Hazelcast port from the configuration.
+        if (containers.size() == 1) {
+            JsonValue container = containers.get(0);
+            JsonArray ports = toJsonArray(container.asObject().get("ports"));
+            // If multiple ports are exposed by a container, then use the default Hazelcast port from the configuration.
+            if (ports.size() == 1) {
+                JsonValue port = ports.get(0);
+                JsonValue containerPort = port.asObject().get("containerPort");
+                if (containerPort != null && containerPort.isNumber()) {
+                    return containerPort.asInt();
+                }
+            }
+        }
+        return null;
+    }
+
+    private static boolean isReady(JsonObject podItemStatusJson) {
+        for (JsonValue containerStatus : toJsonArray(podItemStatusJson.get("containerStatuses"))) {
+            // If multiple containers are in one POD, then each needs to be ready.
+            if (!containerStatus.asObject().get("ready").asBoolean()) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static List<Endpoint> parseEndpointsList(JsonObject endpointsListJson) {
+        List<Endpoint> endpoints = new ArrayList<Endpoint>();
+        for (JsonValue item : toJsonArray(endpointsListJson.get("items"))) {
+            endpoints.addAll(parseEndpoints(item));
+        }
+        return endpoints;
+    }
+
+    private static List<Endpoint> parseEndpoints(JsonValue endpointItemJson) {
+        List<Endpoint> addresses = new ArrayList<Endpoint>();
+
+        for (JsonValue subset : toJsonArray(endpointItemJson.asObject().get("subsets"))) {
+            Integer endpointPort = extractPort(subset);
+            for (JsonValue address : toJsonArray(subset.asObject().get("addresses"))) {
+                addresses.add(extractEntrypointAddress(address, endpointPort, true));
+            }
+            for (JsonValue address : toJsonArray(subset.asObject().get("notReadyAddresses"))) {
+                addresses.add(extractEntrypointAddress(address, endpointPort, false));
+            }
+        }
+        return addresses;
+    }
+
+    private static Integer extractPort(JsonValue subsetJson) {
+        JsonArray ports = toJsonArray(subsetJson.asObject().get("ports"));
+        if (ports.size() == 1) {
+            JsonValue port = ports.get(0);
+            return port.asObject().get("port").asInt();
+        }
+        return null;
+    }
+
+    private static Endpoint extractEntrypointAddress(JsonValue endpointAddressJson, Integer endpointPort, boolean isReady) {
+        String ip = endpointAddressJson.asObject().get("ip").asString();
+        Integer port = extractHazelcastServicePortFrom(endpointAddressJson, endpointPort);
+        Map<String, Object> additionalProperties = extractAdditionalPropertiesFrom(endpointAddressJson);
+        return new Endpoint(new EndpointAddress(ip, port), isReady, additionalProperties);
+    }
+
+    private static Integer extractHazelcastServicePortFrom(JsonValue endpointAddressJson, Integer endpointPort) {
+        JsonValue servicePort = endpointAddressJson.asObject().get("hazelcast-service-port");
+        if (servicePort != null && servicePort.isNumber()) {
+            return servicePort.asInt();
+        }
+        return endpointPort;
+    }
+
+    private static Map<String, Object> extractAdditionalPropertiesFrom(JsonValue endpointAddressJson) {
+        Set<String> knownFieldNames = new HashSet<String>(
+                asList("ip", "nodeName", "targetRef", "hostname", "hazelcast-service-port"));
+
+        Map<String, Object> result = new HashMap<String, Object>();
+        for (JsonObject.Member member : endpointAddressJson.asObject()) {
+            if (!knownFieldNames.contains(member.getName())) {
+                result.put(member.getName(), toString(member.getValue()));
+            }
+        }
+        return result;
+    }
+
+    private static String extractNodeName(JsonObject podJson) {
+        return toString(podJson.get("spec").asObject().get("nodeName"));
+    }
+
+    private static String extractZone(JsonObject nodeJson) {
+        JsonObject labels = nodeJson.get("metadata").asObject().get("labels").asObject();
+        JsonValue zone = labels.get("failure-domain.kubernetes.io/zone");
+        if (zone != null) {
+            return toString(zone);
+        }
+        return toString(labels.get("failure-domain.beta.kubernetes.io/zone"));
+    }
+
+    /**
+     * Tries to add public addresses to the endpoints.
+     * <p>
+     * If it's not possible, then returns the input parameter.
+     * <p>
+     * Assigning public IPs must meet one of the following requirements:
+     * <ul>
+     * <li>Each POD must be exposed with a separate LoadBalancer service OR</li>
+     * <li>Each POD must be exposed with a separate NodePort service and Kubernetes nodes must have external IPs</li>
+     * </ul>
+     * <p>
+     * The algorithm to fetch public IPs is as follows:
+     * <ol>
+     * <li>Use Kubernetes API (/endpoints) to find dedicated services for each POD</li>
+     * <li>For each POD:
+     * <ol>
+     * <li>Use Kubernetes API (/services) to find the LoadBalancer External IP and Service Port</li>
+     * <li>If not found, then use Kubernetes API (/nodes) to find External IP of the Node</li>
+     * </ol>
+     * </li>
+     * </ol>
+     */
+    private List<Endpoint> enrichWithPublicAddresses(List<Endpoint> endpoints) {
+        try {
+            String endpointsUrl = String.format("%s/api/v1/namespaces/%s/endpoints", kubernetesMaster, namespace);
+            JsonObject endpointsJson = callGet(endpointsUrl);
+
+            List<EndpointAddress> privateAddresses = privateAddresses(endpoints);
+            Map<EndpointAddress, String> services = extractServices(endpointsJson, privateAddresses);
+            Map<EndpointAddress, String> nodes = extractNodes(endpointsJson, privateAddresses);
+
+            Map<EndpointAddress, String> publicIps = new HashMap<EndpointAddress, String>();
+            Map<EndpointAddress, Integer> publicPorts = new HashMap<EndpointAddress, Integer>();
+            Map<String, String> cachedNodePublicIps = new HashMap<String, String>();
+
+            for (Map.Entry<EndpointAddress, String> serviceEntry : services.entrySet()) {
+                EndpointAddress privateAddress = serviceEntry.getKey();
+                String service = serviceEntry.getValue();
+                String serviceUrl = String.format("%s/api/v1/namespaces/%s/services/%s", kubernetesMaster, namespace, service);
+                JsonObject serviceJson = callGet(serviceUrl);
+                try {
+                    String loadBalancerIp = extractLoadBalancerIp(serviceJson);
+                    Integer servicePort = extractServicePort(serviceJson);
+                    publicIps.put(privateAddress, loadBalancerIp);
+                    publicPorts.put(privateAddress, servicePort);
+                } catch (Exception e) {
+                    // Load Balancer public IP cannot be found, try using NodePort.
+                    Integer nodePort = extractNodePort(serviceJson);
+                    String node = nodes.get(privateAddress);
+                    String nodePublicIp;
+                    if (cachedNodePublicIps.containsKey(node)) {
+                        nodePublicIp = cachedNodePublicIps.get(node);
+                    } else {
+                        String nodeUrl = String.format("%s/api/v1/nodes/%s", kubernetesMaster, node);
+                        nodePublicIp = extractNodePublicIp(callGet(nodeUrl));
+                        cachedNodePublicIps.put(node, nodePublicIp);
+                    }
+                    publicIps.put(privateAddress, nodePublicIp);
+                    publicPorts.put(privateAddress, nodePort);
+                }
+            }
+
+            return createEndpoints(endpoints, publicIps, publicPorts);
+        } catch (Exception e) {
+            LOGGER.finest(e);
+            // Log warning only once.
+            if (!isNoPublicIpAlreadyLogged) {
+                LOGGER.warning(
+                        "Cannot fetch public IPs of Hazelcast Member PODs, you won't be able to use Hazelcast Smart Client from "
+                                + "outside of the Kubernetes network");
+                isNoPublicIpAlreadyLogged = true;
+            }
+            return endpoints;
+        }
+    }
+
+    private static List<EndpointAddress> privateAddresses(List<Endpoint> endpoints) {
+        List<EndpointAddress> result = new ArrayList<EndpointAddress>();
+        for (Endpoint endpoint : endpoints) {
+            result.add(endpoint.getPrivateAddress());
+        }
+        return result;
+    }
+
+    private static Map<EndpointAddress, String> extractServices(JsonObject endpointsListJson,
+                                                                List<EndpointAddress> privateAddresses) {
+        Map<EndpointAddress, String> result = new HashMap<EndpointAddress, String>();
+        Set<EndpointAddress> left = new HashSet<EndpointAddress>(privateAddresses);
+        for (JsonValue item : toJsonArray(endpointsListJson.get("items"))) {
+            String service = toString(item.asObject().get("metadata").asObject().get("name"));
+            List<Endpoint> endpoints = parseEndpoints(item);
+            // Service must point to exactly one endpoint address, otherwise the public IP would be ambiguous.
+            if (endpoints.size() == 1) {
+                EndpointAddress address = endpoints.get(0).getPrivateAddress();
+                if (left.contains(address)) {
+                    result.put(address, service);
+                    left.remove(address);
+                }
+            }
+        }
+        if (!left.isEmpty()) {
+            // At least one Hazelcast Member POD does not have a corresponding service.
+            throw new KubernetesClientException(String.format("Cannot fetch services dedicated to the following PODs: %s", left));
+        }
+        return result;
+    }
+
+    private static Map<EndpointAddress, String> extractNodes(JsonObject endpointsListJson,
+                                                             List<EndpointAddress> privateAddresses) {
+        Map<EndpointAddress, String> result = new HashMap<EndpointAddress, String>();
+        Set<EndpointAddress> left = new HashSet<EndpointAddress>(privateAddresses);
+        for (JsonValue item : toJsonArray(endpointsListJson.get("items"))) {
+            for (JsonValue subset : toJsonArray(item.asObject().get("subsets"))) {
+                JsonObject subsetObject = subset.asObject();
+                List<Integer> ports = new ArrayList<Integer>();
+                for (JsonValue port : toJsonArray(subsetObject.get("ports"))) {
+                    ports.add(port.asObject().get("port").asInt());
+                }
+
+                Map<EndpointAddress, String> nodes = new HashMap<EndpointAddress, String>();
+                nodes.putAll(extractNodes(subsetObject.get("addresses"), ports));
+                nodes.putAll(extractNodes(subsetObject.get("notReadyAddresses"), ports));
+                for (Map.Entry<EndpointAddress, String> nodeEntry : nodes.entrySet()) {
+                    EndpointAddress address = nodeEntry.getKey();
+                    if (privateAddresses.contains(address)) {
+                        result.put(address, nodes.get(address));
+                        left.remove(address);
+                    }
+                }
+            }
+        }
+        if (!left.isEmpty()) {
+            // At least one Hazelcast Member POD does not have 'nodeName' assigned.
+            throw new KubernetesClientException(String.format("Cannot fetch nodeName from the following PODs: %s", left));
+        }
+        return result;
+    }
+
+    private static Map<EndpointAddress, String> extractNodes(JsonValue addressesJson, List<Integer> ports) {
+        Map<EndpointAddress, String> result = new HashMap<EndpointAddress, String>();
+        for (JsonValue address : toJsonArray(addressesJson)) {
+            String ip = address.asObject().get("ip").asString();
+            String nodeName = toString(address.asObject().get("nodeName"));
+            for (Integer port : ports) {
+                result.put(new EndpointAddress(ip, port), nodeName);
+            }
+        }
+        return result;
+    }
+
+    private static String extractLoadBalancerIp(JsonObject serviceResponse) {
+        return serviceResponse.get("status").asObject()
+                              .get("loadBalancer").asObject()
+                              .get("ingress").asArray().get(0).asObject()
+                              .get("ip").asString();
+    }
+
+    private static Integer extractServicePort(JsonObject serviceJson) {
+        JsonArray ports = toJsonArray(serviceJson.get("spec").asObject().get("ports"));
+        // Service must have one and only one Node Port assigned.
+        if (ports.size() != 1) {
+            throw new KubernetesClientException("Cannot fetch nodePort from the service");
+        }
+        return ports.get(0).asObject().get("port").asInt();
+    }
+
+    private static Integer extractNodePort(JsonObject serviceJson) {
+        JsonArray ports = toJsonArray(serviceJson.get("spec").asObject().get("ports"));
+        // Service must have one and only one Node Port assigned.
+        if (ports.size() != 1) {
+            throw new KubernetesClientException("Cannot fetch nodePort from the service");
+        }
+        return ports.get(0).asObject().get("nodePort").asInt();
+    }
+
+    private static String extractNodePublicIp(JsonObject nodeJson) {
+        for (JsonValue address : toJsonArray(nodeJson.get("status").asObject().get("addresses"))) {
+            if ("ExternalIP".equals(address.asObject().get("type").asString())) {
+                return address.asObject().get("address").asString();
+            }
+        }
+        throw new KubernetesClientException("Node does not have ExternalIP assigned");
+    }
+
+    private static List<Endpoint> createEndpoints(List<Endpoint> endpoints, Map<EndpointAddress, String> publicIps,
+                                                  Map<EndpointAddress, Integer> publicPorts) {
+        List<Endpoint> result = new ArrayList<Endpoint>();
+        for (Endpoint endpoint : endpoints) {
+            EndpointAddress privateAddress = endpoint.getPrivateAddress();
+            EndpointAddress publicAddress = new EndpointAddress(publicIps.get(privateAddress),
+                    publicPorts.get(privateAddress));
+            result.add(new Endpoint(privateAddress, publicAddress, endpoint.isReady(), endpoint.getAdditionalProperties()));
+        }
+        return result;
+    }
 
     /**
-     * Result which stores the information about all addresses.
+     * Makes a REST call to Kubernetes API and returns the result JSON.
+     *
+     * @param urlString Kubernetes API REST endpoint
+     * @return parsed JSON
+     * @throws KubernetesClientException if Kubernetes API didn't respond with 200 and a valid JSON content
      */
-    final class Endpoints {
-        private final List<EntrypointAddress> addresses;
-        private final List<EntrypointAddress> notReadyAddresses;
+    private JsonObject callGet(final String urlString) {
+        return RetryUtils.retry(new Callable<JsonObject>() {
+            @Override
+            public JsonObject call() {
+                return Json
+                        .parse(RestClient.create(urlString).withHeader("Authorization", String.format("Bearer %s", apiToken))
+                                         .withCaCertificate(caCertificate)
+                                         .get())
+                        .asObject();
+            }
+        }, RETRIES, NON_RETRYABLE_KEYWORDS);
+    }
 
-        Endpoints(List<EntrypointAddress> addresses, List<EntrypointAddress> notReadyAddresses) {
-            this.addresses = addresses;
-            this.notReadyAddresses = notReadyAddresses;
+    @SuppressWarnings("checkstyle:magicnumber")
+    private static List<Endpoint> handleKnownException(RestClientException e) {
+        if (e.getHttpErrorCode() == 401) {
+            LOGGER.severe("Kubernetes API authorization failure, please check your 'api-token' property");
+        } else if (e.getHttpErrorCode() == 403) {
+            LOGGER.severe(
+                    "Kubernetes API forbidden access, please check that your Service Account have the correct (Cluster) Role "
+                            + "rules");
+        } else {
+            throw e;
         }
+        LOGGER.finest(e);
+        return emptyList();
+    }
 
-        List<EntrypointAddress> getAddresses() {
-            return addresses;
+    private static JsonArray toJsonArray(JsonValue jsonValue) {
+        if (jsonValue == null || jsonValue.isNull()) {
+            return new JsonArray();
+        } else {
+            return jsonValue.asArray();
         }
+    }
 
-        List<EntrypointAddress> getNotReadyAddresses() {
-            return notReadyAddresses;
+    private static String toString(JsonValue jsonValue) {
+        if (jsonValue == null || jsonValue.isNull()) {
+            return null;
+        } else if (jsonValue.isString()) {
+            return jsonValue.asString();
+        } else {
+            return jsonValue.toString();
         }
     }
 
     /**
-     * Result which stores the information about a single address.
+     * Result which stores the information about a single endpoint.
      */
-    final class EntrypointAddress {
-        private final String ip;
-        private final Integer port;
+    static final class Endpoint {
+        private final EndpointAddress privateAddress;
+        private final EndpointAddress publicAddress;
+        private final boolean isReady;
         private final Map<String, Object> additionalProperties;
 
-        EntrypointAddress(String ip, Map<String, Object> additionalProperties) {
-            this.ip = ip;
-            this.port = null;
+        Endpoint(EndpointAddress privateAddress, boolean isReady) {
+            this.privateAddress = privateAddress;
+            this.publicAddress = null;
+            this.isReady = isReady;
+            this.additionalProperties = Collections.emptyMap();
+        }
+
+        Endpoint(EndpointAddress privateAddress, boolean isReady, Map<String, Object> additionalProperties) {
+            this.privateAddress = privateAddress;
+            this.publicAddress = null;
+            this.isReady = isReady;
+            this.additionalProperties = additionalProperties;
+        }
+
+        Endpoint(EndpointAddress privateAddress, EndpointAddress publicAddress, boolean isReady,
+                 Map<String, Object> additionalProperties) {
+            this.privateAddress = privateAddress;
+            this.publicAddress = publicAddress;
+            this.isReady = isReady;
             this.additionalProperties = additionalProperties;
         }
 
-        EntrypointAddress(String ip, Integer port, Map<String, Object> additionalProperties) {
+        EndpointAddress getPublicAddress() {
+            return publicAddress;
+        }
+
+        EndpointAddress getPrivateAddress() {
+            return privateAddress;
+        }
+
+        boolean isReady() {
+            return isReady;
+        }
+
+        Map<String, Object> getAdditionalProperties() {
+            return additionalProperties;
+        }
+    }
+
+    static final class EndpointAddress {
+        private final String ip;
+        private final Integer port;
+
+        EndpointAddress(String ip, Integer port) {
             this.ip = ip;
             this.port = port;
-            this.additionalProperties = additionalProperties;
         }
 
         String getIp() {
@@ -117,8 +560,33 @@ interface KubernetesClient {
             return port;
         }
 
-        Map<String, Object> getAdditionalProperties() {
-            return additionalProperties;
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true;
+            }
+            if (o == null || getClass() != o.getClass()) {
+                return false;
+            }
+
+            EndpointAddress address = (EndpointAddress) o;
+
+            if (ip != null ? !ip.equals(address.ip) : address.ip != null) {
+                return false;
+            }
+            return port != null ? port.equals(address.port) : address.port == null;
+        }
+
+        @Override
+        public int hashCode() {
+            int result = ip != null ? ip.hashCode() : 0;
+            result = 31 * result + (port != null ? port.hashCode() : 0);
+            return result;
+        }
+
+        @Override
+        public String toString() {
+            return String.format("%s:%s", ip, port);
         }
     }
 }
diff --git a/src/main/java/com/hazelcast/kubernetes/RestClient.java b/src/main/java/com/hazelcast/kubernetes/RestClient.java
new file mode 100644
index 0000000..608dc7c
--- /dev/null
+++ b/src/main/java/com/hazelcast/kubernetes/RestClient.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (c) 2008-2018, Hazelcast, Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hazelcast.kubernetes;
+
+import com.hazelcast.logging.ILogger;
+import com.hazelcast.logging.Logger;
+import com.hazelcast.nio.IOUtil;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManagerFactory;
+import java.io.ByteArrayInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.security.KeyStore;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+
+/**
+ * Utility class for making REST calls.
+ */
+final class RestClient {
+    private static final ILogger LOGGER = Logger.getLogger(RestClient.class);
+
+    private static final int HTTP_OK = 200;
+
+    private final String url;
+    private final List<Header> headers = new ArrayList<Header>();
+    private String body;
+    private String caCertificate;
+
+    private RestClient(String url) {
+        this.url = url;
+    }
+
+    static RestClient create(String url) {
+        return new RestClient(url);
+    }
+
+    RestClient withHeader(String key, String value) {
+        headers.add(new Header(key, value));
+        return this;
+    }
+
+    RestClient withBody(String body) {
+        this.body = body;
+        return this;
+    }
+
+    RestClient withCaCertificate(String caCertificate) {
+        this.caCertificate = caCertificate;
+        return this;
+    }
+
+    String get() {
+        return call("GET");
+    }
+
+    String post() {
+        return call("POST");
+    }
+
+    private String call(String method) {
+        HttpURLConnection connection = null;
+        DataOutputStream outputStream = null;
+        try {
+            URL urlToConnect = new URL(url);
+            connection = (HttpURLConnection) urlToConnect.openConnection();
+            if (connection instanceof HttpsURLConnection) {
+                ((HttpsURLConnection) connection).setSSLSocketFactory(buildSslSocketFactory());
+            }
+            connection.setRequestMethod(method);
+            for (Header header : headers) {
+                connection.setRequestProperty(header.getKey(), header.getValue());
+            }
+            if (body != null) {
+                byte[] bodyData = body.getBytes("UTF-8");
+
+                connection.setDoOutput(true);
+                connection.setRequestProperty("charset", "utf-8");
+                connection.setRequestProperty("Content-Length", Integer.toString(bodyData.length));
+
+                outputStream = new DataOutputStream(connection.getOutputStream());
+                outputStream.write(bodyData);
+                outputStream.flush();
+            }
+
+            checkHttpOk(method, connection);
+            return read(connection.getInputStream());
+        } catch (IOException e) {
+            throw new RestClientException("Failure in executing REST call", e);
+        } finally {
+            if (connection != null) {
+                connection.disconnect();
+            }
+            if (outputStream != null) {
+                try {
+                    outputStream.close();
+                } catch (IOException e) {
+                    LOGGER.finest("Error while closing HTTP output stream", e);
+                }
+            }
+        }
+    }
+
+    private void checkHttpOk(String method, HttpURLConnection connection)
+            throws IOException {
+        if (connection.getResponseCode() != HTTP_OK) {
+            String errorMessage;
+            try {
+                errorMessage = read(connection.getErrorStream());
+            } catch (Exception e) {
+                throw new RestClientException(
+                        String.format("Failure executing: %s at: %s", method, url), connection.getResponseCode());
+            }
+            throw new RestClientException(String.format("Failure executing: %s at: %s. Message: %s", method, url, errorMessage),
+                    connection.getResponseCode());
+
+        }
+    }
+
+    private static String read(InputStream stream) {
+        if (stream == null) {
+            return "";
+        }
+        Scanner scanner = new Scanner(stream, "UTF-8");
+        scanner.useDelimiter("\\Z");
+        return scanner.next();
+    }
+
+    private static final class Header {
+        private final String key;
+        private final String value;
+
+        private Header(String key, String value) {
+            this.key = key;
+            this.value = value;
+        }
+
+        private String getKey() {
+            return key;
+        }
+
+        private String getValue() {
+            return value;
+        }
+    }
+
+    /**
+     * Builds SSL Socket Factory with the public CA Certificate from Kubernetes Master.
+     */
+    private SSLSocketFactory buildSslSocketFactory() {
+        try {
+            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
+            keyStore.load(null, null);
+            keyStore.setCertificateEntry("ca", generateCertificate());
+
+            TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+            tmf.init(keyStore);
+
+            SSLContext context = SSLContext.getInstance("TLSv1.2");
+            context.init(null, tmf.getTrustManagers(), null);
+            return context.getSocketFactory();
+
+        } catch (Exception e) {
+            throw new KubernetesClientException("Failure in generating SSLSocketFactory", e);
+        }
+    }
+
+    /**
+     * Generates CA Certificate from the default CA Cert file or from the externally provided "ca-certificate" property.
+     */
+    private Certificate generateCertificate()
+            throws IOException, CertificateException {
+        InputStream caInput = null;
+        try {
+            CertificateFactory cf = CertificateFactory.getInstance("X.509");
+            caInput = new ByteArrayInputStream(caCertificate.getBytes("UTF-8"));
+            return cf.generateCertificate(caInput);
+        } finally {
+            IOUtil.closeResource(caInput);
+        }
+    }
+}
diff --git a/src/main/java/com/hazelcast/kubernetes/RestClientException.java b/src/main/java/com/hazelcast/kubernetes/RestClientException.java
new file mode 100644
index 0000000..cfe191f
--- /dev/null
+++ b/src/main/java/com/hazelcast/kubernetes/RestClientException.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2008-2018, Hazelcast, Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hazelcast.kubernetes;
+
+/**
+ * Exception to indicate any issues while executing a REST call.
+ */
+class RestClientException
+        extends RuntimeException {
+    private int httpErrorCode;
+
+    RestClientException(String message, int httpErrorCode) {
+        super(String.format("%s. HTTP Error Code: %s", message, httpErrorCode));
+        this.httpErrorCode = httpErrorCode;
+    }
+
+    RestClientException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    int getHttpErrorCode() {
+        return httpErrorCode;
+    }
+}
diff --git a/src/main/java/com/hazelcast/kubernetes/RetryKubernetesClient.java b/src/main/java/com/hazelcast/kubernetes/RetryKubernetesClient.java
deleted file mode 100644
index 3d9aa22..0000000
--- a/src/main/java/com/hazelcast/kubernetes/RetryKubernetesClient.java
+++ /dev/null
@@ -1,86 +0,0 @@
-/*
- * Copyright (c) 2008-2018, Hazelcast, Inc. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.hazelcast.kubernetes;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.concurrent.Callable;
-
-/**
- * Connects to Kubernetes API with retries.
- */
-class RetryKubernetesClient
-        implements KubernetesClient {
-    private static final int DEFAULT_RETRIES = 10;
-    private static final List<String> NON_RETRYABLE_KEYWORDS = Arrays.asList("\"reason\":\"Forbidden\"");
-
-    private final KubernetesClient kubernetesClient;
-    private final int retries;
-
-    RetryKubernetesClient(KubernetesClient kubernetesClient, int retries) {
-        this.kubernetesClient = kubernetesClient;
-        this.retries = retries;
-    }
-
-    RetryKubernetesClient(KubernetesClient kubernetesClient) {
-        this(kubernetesClient, DEFAULT_RETRIES);
-    }
-
-    @Override
-    public Endpoints endpoints(final String namespace) {
-        return RetryUtils.retry(new Callable<Endpoints>() {
-            @Override
-            public Endpoints call()
-                    throws Exception {
-                return kubernetesClient.endpoints(namespace);
-            }
-        }, retries, NON_RETRYABLE_KEYWORDS);
-    }
-
-    @Override
-    public Endpoints endpointsByLabel(final String namespace, final String serviceLabel, final String serviceLabelValue) {
-        return RetryUtils.retry(new Callable<Endpoints>() {
-            @Override
-            public Endpoints call()
-                    throws Exception {
-                return kubernetesClient.endpointsByLabel(namespace, serviceLabel, serviceLabelValue);
-            }
-        }, retries, NON_RETRYABLE_KEYWORDS);
-    }
-
-    @Override
-    public Endpoints endpointsByName(final String namespace, final String endpointName) {
-        return RetryUtils.retry(new Callable<Endpoints>() {
-            @Override
-            public Endpoints call()
-                    throws Exception {
-                return kubernetesClient.endpointsByName(namespace, endpointName);
-            }
-        }, retries, NON_RETRYABLE_KEYWORDS);
-    }
-
-    @Override
-    public String zone(final String namespace, final String podName) {
-        return RetryUtils.retry(new Callable<String>() {
-            @Override
-            public String call()
-                    throws Exception {
-                return kubernetesClient.zone(namespace, podName);
-            }
-        }, retries, NON_RETRYABLE_KEYWORDS);
-    }
-}


Test patch:
diff --git a/src/test/java/com/hazelcast/kubernetes/DefaultKubernetesClientTest.java b/src/test/java/com/hazelcast/kubernetes/DefaultKubernetesClientTest.java
deleted file mode 100644
index 5ca834b..0000000
--- a/src/test/java/com/hazelcast/kubernetes/DefaultKubernetesClientTest.java
+++ /dev/null
@@ -1,1034 +0,0 @@
-/*
- * Copyright (c) 2008-2018, Hazelcast, Inc. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.hazelcast.kubernetes;
-
-import com.github.tomakehurst.wiremock.junit.WireMockRule;
-import com.hazelcast.kubernetes.KubernetesClient.Endpoints;
-import com.hazelcast.kubernetes.KubernetesClient.EntrypointAddress;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
-import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
-import static com.github.tomakehurst.wiremock.client.WireMock.get;
-import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
-import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
-import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching;
-import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.containsInAnyOrder;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-public class DefaultKubernetesClientTest {
-    private static final String KUBERNETES_MASTER_IP = "localhost";
-
-    private static final String TOKEN = "sample-token";
-    private static final String CA_CERTIFICATE = "sample-ca-certificate";
-    private static final String NAMESPACE = "sample-namespace";
-
-    private static final String SAMPLE_ADDRESS_1 = "192.168.0.25";
-    private static final String SAMPLE_ADDRESS_2 = "172.17.0.5";
-    private static final String SAMPLE_NOT_READY_ADDRESS = "172.17.0.6";
-    private static final Integer SAMPLE_PORT_1 = 5701;
-    private static final Integer SAMPLE_PORT_2 = 5702;
-    private static final String SAMPLE_IP_PORT_1 = ipPort(SAMPLE_ADDRESS_1, SAMPLE_PORT_1);
-    private static final String SAMPLE_IP_PORT_2 = ipPort(SAMPLE_ADDRESS_2, SAMPLE_PORT_2);
-    private static final String SAMPLE_NOT_READY_IP = ipPort(SAMPLE_NOT_READY_ADDRESS, null);
-    private static final String ZONE = "us-central1-a";
-
-    @Rule
-    public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort());
-
-    private DefaultKubernetesClient kubernetesClient;
-
-    @Before
-    public void setUp() {
-        String kubernetesMasterUrl = String.format("http://%s:%d", KUBERNETES_MASTER_IP, wireMockRule.port());
-        kubernetesClient = new DefaultKubernetesClient(kubernetesMasterUrl, TOKEN, CA_CERTIFICATE);
-    }
-
-    @Test
-    public void endpointsByNamespace() {
-        // given
-        stubFor(get(urlEqualTo(String.format("/api/v1/namespaces/%s/pods", NAMESPACE)))
-                .withHeader("Authorization", equalTo(String.format("Bearer %s", TOKEN)))
-                .willReturn(aResponse().withStatus(200).withBody(podsListBody())));
-
-        // when
-        Endpoints result = kubernetesClient.endpoints(NAMESPACE);
-
-        // then
-        assertThat(extractIpPort(result.getAddresses()), containsInAnyOrder(SAMPLE_IP_PORT_1, SAMPLE_IP_PORT_2));
-        assertThat(extractIpPort(result.getNotReadyAddresses()), containsInAnyOrder(SAMPLE_NOT_READY_IP));
-    }
-
-    @Test
-    public void endpointsByNamespaceAndLabel() {
-        // given
-        String serviceLabel = "sample-service-label";
-        String serviceLabelValue = "sample-service-label-value";
-        stubFor(get(urlPathMatching(String.format("/api/v1/namespaces/%s/endpoints", NAMESPACE)))
-                .withQueryParam("labelSelector", equalTo(String.format("%s=%s", serviceLabel, serviceLabelValue)))
-                .withHeader("Authorization", equalTo(String.format("Bearer %s", TOKEN)))
-                .willReturn(aResponse().withStatus(200).withBody(endpointsListBody())));
-
-        // when
-        Endpoints result = kubernetesClient.endpointsByLabel(NAMESPACE, serviceLabel, serviceLabelValue);
-
-        // then
-        assertThat(extractIpPort(result.getAddresses()), containsInAnyOrder(SAMPLE_IP_PORT_1, SAMPLE_IP_PORT_2));
-        assertThat(extractIpPort(result.getNotReadyAddresses()), containsInAnyOrder(SAMPLE_NOT_READY_IP));
-
-    }
-
-    @Test
-    public void endpointsByNamespaceAndServiceName() {
-        // given
-        String serviceName = "service-name";
-        stubFor(get(urlPathMatching(String.format("/api/v1/namespaces/%s/endpoints/%s", NAMESPACE, serviceName)))
-                .withHeader("Authorization", equalTo(String.format("Bearer %s", TOKEN)))
-                .willReturn(aResponse().withStatus(200).withBody(endpointsBody())));
-
-        // when
-        Endpoints result = kubernetesClient.endpointsByName(NAMESPACE, serviceName);
-
-        // then
-        assertThat(extractIpPort(result.getAddresses()), containsInAnyOrder(SAMPLE_IP_PORT_1, SAMPLE_IP_PORT_2));
-        assertTrue(result.getNotReadyAddresses().isEmpty());
-    }
-
-    @Test
-    public void zoneBeta() {
-        // given
-        String podName = "my-release-hazelcast-0";
-        String nodeName = "gke-rafal-test-cluster-default-pool-9238654c-12tz";
-        stubFor(get(urlPathMatching(String.format("/api/v1/namespaces/%s/pods/%s", NAMESPACE, podName)))
-                .withHeader("Authorization", equalTo(String.format("Bearer %s", TOKEN)))
-                .willReturn(aResponse().withStatus(200).withBody(podBody(nodeName))));
-        stubFor(get(urlPathMatching(String.format("/api/v1/nodes/%s", nodeName)))
-                .withHeader("Authorization", equalTo(String.format("Bearer %s", TOKEN)))
-                .willReturn(aResponse().withStatus(200).withBody(nodeBetaBody())));
-
-        // when
-        String zone = kubernetesClient.zone(NAMESPACE, podName);
-
-        // then
-        assertEquals(ZONE, zone);
-    }
-
-    @Test
-    public void zone() {
-        // given
-        String podName = "my-release-hazelcast-0";
-        String nodeName = "gke-rafal-test-cluster-default-pool-9238654c-12tz";
-        stubFor(get(urlPathMatching(String.format("/api/v1/namespaces/%s/pods/%s", NAMESPACE, podName)))
-                .withHeader("Authorization", equalTo(String.format("Bearer %s", TOKEN)))
-                .willReturn(aResponse().withStatus(200).withBody(podBody(nodeName))));
-        stubFor(get(urlPathMatching(String.format("/api/v1/nodes/%s", nodeName)))
-                .withHeader("Authorization", equalTo(String.format("Bearer %s", TOKEN)))
-                .willReturn(aResponse().withStatus(200).withBody(nodeBody())));
-
-        // when
-        String zone = kubernetesClient.zone(NAMESPACE, podName);
-
-        // then
-        assertEquals(ZONE, zone);
-    }
-
-    @Test(expected = KubernetesClientException.class)
-    public void forbidden() {
-        // given
-        stubFor(get(urlEqualTo(String.format("/api/v1/namespaces/%s/pods", NAMESPACE)))
-                .withHeader("Authorization", equalTo(String.format("Bearer %s", TOKEN)))
-                .willReturn(aResponse().withStatus(501).withBody(forbiddenBody())));
-
-        // when
-        kubernetesClient.endpoints(NAMESPACE);
-    }
-
-    @Test(expected = KubernetesClientException.class)
-    public void malformedResponse() {
-        // given
-        stubFor(get(urlEqualTo(String.format("/api/v1/namespaces/%s/pods", NAMESPACE)))
-                .withHeader("Authorization", equalTo(String.format("Bearer %s", TOKEN)))
-                .willReturn(aResponse().withStatus(200).withBody(malformedBody())));
-
-        // when
-        kubernetesClient.endpoints(NAMESPACE);
-    }
-
-    @Test(expected = KubernetesClientException.class)
-    public void nullToken() {
-        // given
-        String kubernetesMasterUrl = String.format("http://%s:%d", KUBERNETES_MASTER_IP, wireMockRule.port());
-        KubernetesClient kubernetesClient = new DefaultKubernetesClient(kubernetesMasterUrl, TOKEN, null);
-
-        stubFor(get(urlEqualTo(String.format("/api/v1/namespaces/%s/pods", NAMESPACE)))
-                .withHeader("Authorization", equalTo(String.format("Bearer %s", TOKEN)))
-                .willReturn(aResponse().withStatus(200).withBody(malformedBody())));
-
-        // when
-        kubernetesClient.endpoints(NAMESPACE);
-    }
-
-    /**
-     * Real response recorded from the Kubernetes API call "/api/v1/namespaces/{namespace}/pods".
-     */
-    private static String podsListBody() {
-        return String.format("{\n"
-                + "  \"kind\": \"PodList\",\n"
-                + "  \"apiVersion\": \"v1\",\n"
-                + "  \"metadata\": {\n"
-                + "    \"selfLink\": \"/api/v1/namespaces/default/pods\",\n"
-                + "    \"resourceVersion\": \"4400\"\n"
-                + "  },\n"
-                + "  \"items\": [\n"
-                + "    {\n"
-                + "      \"metadata\": {\n"
-                + "        \"name\": \"my-release-hazelcast-0\",\n"
-                + "        \"generateName\": \"my-release-hazelcast-\",\n"
-                + "        \"namespace\": \"default\",\n"
-                + "        \"selfLink\": \"/api/v1/namespaces/default/pods/my-release-hazelcast-0\",\n"
-                + "        \"uid\": \"21b91e5b-eefd-11e8-ab27-42010a8001ce\",\n"
-                + "        \"resourceVersion\": \"1967\",\n"
-                + "        \"creationTimestamp\": \"2018-11-23T08:52:39Z\",\n"
-                + "        \"labels\": {\n"
-                + "          \"app\": \"hazelcast\",\n"
-                + "          \"controller-revision-hash\": \"my-release-hazelcast-7bcf66dc79\",\n"
-                + "          \"release\": \"my-release\",\n"
-                + "          \"role\": \"hazelcast\",\n"
-                + "          \"statefulset.kubernetes.io/pod-name\": \"my-release-hazelcast-0\"\n"
-                + "        },\n"
-                + "        \"annotations\": {\n"
-                + "          \"kubernetes.io/limit-ranger\": \"LimitRanger plugin set: cpu request for container my-release-hazelcast\"\n"
-                + "        },\n"
-                + "        \"ownerReferences\": [\n"
-                + "          {\n"
-                + "            \"apiVersion\": \"apps/v1beta1\",\n"
-                + "            \"kind\": \"StatefulSet\",\n"
-                + "            \"name\": \"my-release-hazelcast\",\n"
-                + "            \"uid\": \"21b3fb7f-eefd-11e8-ab27-42010a8001ce\",\n"
-                + "            \"controller\": true,\n"
-                + "            \"blockOwnerDeletion\": true\n"
-                + "          }\n"
-                + "        ]\n"
-                + "      },\n"
-                + "      \"spec\": {\n"
-                + "        \"volumes\": [\n"
-                + "          {\n"
-                + "            \"name\": \"hazelcast-storage\",\n"
-                + "            \"configMap\": {\n"
-                + "              \"name\": \"my-release-hazelcast-configuration\",\n"
-                + "              \"defaultMode\": 420\n"
-                + "            }\n"
-                + "          },\n"
-                + "          {\n"
-                + "            \"name\": \"my-release-hazelcast-token-j9db4\",\n"
-                + "            \"secret\": {\n"
-                + "              \"secretName\": \"my-release-hazelcast-token-j9db4\",\n"
-                + "              \"defaultMode\": 420\n"
-                + "            }\n"
-                + "          }\n"
-                + "        ],\n"
-                + "        \"containers\": [\n"
-                + "          {\n"
-                + "            \"name\": \"my-release-hazelcast\",\n"
-                + "            \"image\": \"hazelcast/hazelcast:latest\",\n"
-                + "            \"ports\": [\n"
-                + "              {\n"
-                + "                \"name\": \"hazelcast\",\n"
-                + "                \"containerPort\": %s,\n"
-                + "                \"protocol\": \"TCP\"\n"
-                + "              }\n"
-                + "            ],\n"
-                + "            \"resources\": {\n"
-                + "              \"requests\": {\n"
-                + "                \"cpu\": \"100m\"\n"
-                + "              }\n"
-                + "            },\n"
-                + "            \"volumeMounts\": [\n"
-                + "              {\n"
-                + "                \"name\": \"hazelcast-storage\",\n"
-                + "                \"mountPath\": \"/data/hazelcast\"\n"
-                + "              },\n"
-                + "              {\n"
-                + "                \"name\": \"my-release-hazelcast-token-j9db4\",\n"
-                + "                \"readOnly\": true,\n"
-                + "                \"mountPath\": \"/var/run/secrets/kubernetes.io/serviceaccount\"\n"
-                + "              }\n"
-                + "            ],\n"
-                + "            \"terminationMessagePath\": \"/dev/termination-log\",\n"
-                + "            \"terminationMessagePolicy\": \"File\",\n"
-                + "            \"imagePullPolicy\": \"Always\"\n"
-                + "          }\n"
-                + "        ],\n"
-                + "        \"restartPolicy\": \"Always\",\n"
-                + "        \"terminationGracePeriodSeconds\": 30,\n"
-                + "        \"dnsPolicy\": \"ClusterFirst\",\n"
-                + "        \"serviceAccountName\": \"my-release-hazelcast\",\n"
-                + "        \"serviceAccount\": \"my-release-hazelcast\",\n"
-                + "        \"nodeName\": \"gke-rafal-test-cluster-default-pool-e5fb2ea5-c7g8\",\n"
-                + "        \"securityContext\": {\n"
-                + "\n"
-                + "        },\n"
-                + "        \"hostname\": \"my-release-hazelcast-0\",\n"
-                + "        \"schedulerName\": \"default-scheduler\",\n"
-                + "        \"tolerations\": [\n"
-                + "          {\n"
-                + "            \"key\": \"node.kubernetes.io/not-ready\",\n"
-                + "            \"operator\": \"Exists\",\n"
-                + "            \"effect\": \"NoExecute\",\n"
-                + "            \"tolerationSeconds\": 300\n"
-                + "          },\n"
-                + "          {\n"
-                + "            \"key\": \"node.kubernetes.io/unreachable\",\n"
-                + "            \"operator\": \"Exists\",\n"
-                + "            \"effect\": \"NoExecute\",\n"
-                + "            \"tolerationSeconds\": 300\n"
-                + "          }\n"
-                + "        ]\n"
-                + "      },\n"
-                + "      \"status\": {\n"
-                + "        \"phase\": \"Running\",\n"
-                + "        \"conditions\": [\n"
-                + "          {\n"
-                + "            \"type\": \"Initialized\",\n"
-                + "            \"status\": \"True\",\n"
-                + "            \"lastProbeTime\": null,\n"
-                + "            \"lastTransitionTime\": \"2018-11-23T08:52:39Z\"\n"
-                + "          },\n"
-                + "          {\n"
-                + "            \"type\": \"Ready\",\n"
-                + "            \"status\": \"True\",\n"
-                + "            \"lastProbeTime\": null,\n"
-                + "            \"lastTransitionTime\": \"2018-11-23T08:53:21Z\"\n"
-                + "          },\n"
-                + "          {\n"
-                + "            \"type\": \"PodScheduled\",\n"
-                + "            \"status\": \"True\",\n"
-                + "            \"lastProbeTime\": null,\n"
-                + "            \"lastTransitionTime\": \"2018-11-23T08:52:39Z\"\n"
-                + "          }\n"
-                + "        ],\n"
-                + "        \"hostIP\": \"10.240.0.18\",\n"
-                + "        \"podIP\": \"%s\",\n"
-                + "        \"startTime\": \"2018-11-23T08:52:39Z\",\n"
-                + "        \"containerStatuses\": [\n"
-                + "          {\n"
-                + "            \"name\": \"my-release-hazelcast\",\n"
-                + "            \"state\": {\n"
-                + "              \"running\": {\n"
-                + "                \"startedAt\": \"2018-11-23T08:52:47Z\"\n"
-                + "              }\n"
-                + "            },\n"
-                + "            \"lastState\": {\n"
-                + "\n"
-                + "            },\n"
-                + "            \"ready\": true,\n"
-                + "            \"restartCount\": 0,\n"
-                + "            \"image\": \"hazelcast/hazelcast:latest\",\n"
-                + "            \"imageID\": \"docker-pullable://hazelcast/hazelcast@sha256:a4dd478dc792ba3fa560aa41b107fed676b37c283be0306303544a0a8ebcc4c8\",\n"
-                + "            \"containerID\": \"docker://d2c59dd02561ae2d274dfa0413277422383241425ce5701ca36c30a862d1520a\"\n"
-                + "          }\n"
-                + "        ],\n"
-                + "        \"qosClass\": \"Burstable\"\n"
-                + "      }\n"
-                + "    },\n"
-                + "    {\n"
-                + "      \"metadata\": {\n"
-                + "        \"name\": \"my-release-hazelcast-1\",\n"
-                + "        \"generateName\": \"my-release-hazelcast-\",\n"
-                + "        \"namespace\": \"default\",\n"
-                + "        \"selfLink\": \"/api/v1/namespaces/default/pods/my-release-hazelcast-1\",\n"
-                + "        \"uid\": \"3a7fd73f-eefd-11e8-ab27-42010a8001ce\",\n"
-                + "        \"resourceVersion\": \"2022\",\n"
-                + "        \"creationTimestamp\": \"2018-11-23T08:53:21Z\",\n"
-                + "        \"labels\": {\n"
-                + "          \"role\": \"hazelcast\",\n"
-                + "          \"statefulset.kubernetes.io/pod-name\": \"my-release-hazelcast-1\",\n"
-                + "          \"app\": \"hazelcast\",\n"
-                + "          \"controller-revision-hash\": \"my-release-hazelcast-7bcf66dc79\",\n"
-                + "          \"release\": \"my-release\"\n"
-                + "        },\n"
-                + "        \"annotations\": {\n"
-                + "          \"kubernetes.io/limit-ranger\": \"LimitRanger plugin set: cpu request for container my-release-hazelcast\"\n"
-                + "        },\n"
-                + "        \"ownerReferences\": [\n"
-                + "          {\n"
-                + "            \"apiVersion\": \"apps/v1beta1\",\n"
-                + "            \"kind\": \"StatefulSet\",\n"
-                + "            \"name\": \"my-release-hazelcast\",\n"
-                + "            \"uid\": \"21b3fb7f-eefd-11e8-ab27-42010a8001ce\",\n"
-                + "            \"controller\": true,\n"
-                + "            \"blockOwnerDeletion\": true\n"
-                + "          }\n"
-                + "        ]\n"
-                + "      },\n"
-                + "      \"spec\": {\n"
-                + "        \"volumes\": [\n"
-                + "          {\n"
-                + "            \"name\": \"hazelcast-storage\",\n"
-                + "            \"configMap\": {\n"
-                + "              \"name\": \"my-release-hazelcast-configuration\",\n"
-                + "              \"defaultMode\": 420\n"
-                + "            }\n"
-                + "          },\n"
-                + "          {\n"
-                + "            \"name\": \"my-release-hazelcast-token-j9db4\",\n"
-                + "            \"secret\": {\n"
-                + "              \"secretName\": \"my-release-hazelcast-token-j9db4\",\n"
-                + "              \"defaultMode\": 420\n"
-                + "            }\n"
-                + "          }\n"
-                + "        ],\n"
-                + "        \"containers\": [\n"
-                + "          {\n"
-                + "            \"name\": \"my-release-hazelcast\",\n"
-                + "            \"image\": \"hazelcast/hazelcast:latest\",\n"
-                + "            \"ports\": [\n"
-                + "              {\n"
-                + "                \"name\": \"hazelcast\",\n"
-                + "                \"containerPort\": %s,\n"
-                + "                \"protocol\": \"TCP\"\n"
-                + "              }\n"
-                + "            ],\n"
-                + "            \"resources\": {\n"
-                + "              \"requests\": {\n"
-                + "                \"cpu\": \"100m\"\n"
-                + "              }\n"
-                + "            },\n"
-                + "            \"volumeMounts\": [\n"
-                + "              {\n"
-                + "                \"name\": \"hazelcast-storage\",\n"
-                + "                \"mountPath\": \"/data/hazelcast\"\n"
-                + "              },\n"
-                + "              {\n"
-                + "                \"name\": \"my-release-hazelcast-token-j9db4\",\n"
-                + "                \"readOnly\": true,\n"
-                + "                \"mountPath\": \"/var/run/secrets/kubernetes.io/serviceaccount\"\n"
-                + "              }\n"
-                + "            ],\n"
-                + "            \"terminationMessagePath\": \"/dev/termination-log\",\n"
-                + "            \"terminationMessagePolicy\": \"File\",\n"
-                + "            \"imagePullPolicy\": \"Always\"\n"
-                + "          }\n"
-                + "        ],\n"
-                + "        \"restartPolicy\": \"Always\",\n"
-                + "        \"terminationGracePeriodSeconds\": 30,\n"
-                + "        \"dnsPolicy\": \"ClusterFirst\",\n"
-                + "        \"serviceAccountName\": \"my-release-hazelcast\",\n"
-                + "        \"serviceAccount\": \"my-release-hazelcast\",\n"
-                + "        \"nodeName\": \"gke-rafal-test-cluster-default-pool-e5fb2ea5-c7g8\",\n"
-                + "        \"securityContext\": {\n"
-                + "\n"
-                + "        },\n"
-                + "        \"hostname\": \"my-release-hazelcast-1\",\n"
-                + "        \"schedulerName\": \"default-scheduler\",\n"
-                + "        \"tolerations\": [\n"
-                + "          {\n"
-                + "            \"key\": \"node.kubernetes.io/not-ready\",\n"
-                + "            \"operator\": \"Exists\",\n"
-                + "            \"effect\": \"NoExecute\",\n"
-                + "            \"tolerationSeconds\": 300\n"
-                + "          },\n"
-                + "          {\n"
-                + "            \"key\": \"node.kubernetes.io/unreachable\",\n"
-                + "            \"operator\": \"Exists\",\n"
-                + "            \"effect\": \"NoExecute\",\n"
-                + "            \"tolerationSeconds\": 300\n"
-                + "          }\n"
-                + "        ]\n"
-                + "      },\n"
-                + "      \"status\": {\n"
-                + "        \"phase\": \"Running\",\n"
-                + "        \"conditions\": [\n"
-                + "          {\n"
-                + "            \"type\": \"Initialized\",\n"
-                + "            \"status\": \"True\",\n"
-                + "            \"lastProbeTime\": null,\n"
-                + "            \"lastTransitionTime\": \"2018-11-23T08:53:21Z\"\n"
-                + "          },\n"
-                + "          {\n"
-                + "            \"type\": \"Ready\",\n"
-                + "            \"status\": \"True\",\n"
-                + "            \"lastProbeTime\": null,\n"
-                + "            \"lastTransitionTime\": \"2018-11-23T08:53:55Z\"\n"
-                + "          },\n"
-                + "          {\n"
-                + "            \"type\": \"PodScheduled\",\n"
-                + "            \"status\": \"True\",\n"
-                + "            \"lastProbeTime\": null,\n"
-                + "            \"lastTransitionTime\": \"2018-11-23T08:53:21Z\"\n"
-                + "          }\n"
-                + "        ],\n"
-                + "        \"hostIP\": \"10.240.0.18\",\n"
-                + "        \"podIP\": \"%s\",\n"
-                + "        \"startTime\": \"2018-11-23T08:53:21Z\",\n"
-                + "        \"containerStatuses\": [\n"
-                + "          {\n"
-                + "            \"name\": \"my-release-hazelcast\",\n"
-                + "            \"state\": {\n"
-                + "              \"running\": {\n"
-                + "                \"startedAt\": \"2018-11-23T08:53:23Z\"\n"
-                + "              }\n"
-                + "            },\n"
-                + "            \"lastState\": {\n"
-                + "\n"
-                + "            },\n"
-                + "            \"ready\": true,\n"
-                + "            \"restartCount\": 0,\n"
-                + "            \"image\": \"hazelcast/hazelcast:latest\",\n"
-                + "            \"imageID\": \"docker-pullable://hazelcast/hazelcast@sha256:a4dd478dc792ba3fa560aa41b107fed676b37c283be0306303544a0a8ebcc4c8\",\n"
-                + "            \"containerID\": \"docker://705df57d5bfb1417683800aad2b8ac38ba68abcfaf03550d797e36e79313c903\"\n"
-                + "          }\n"
-                + "        ],\n"
-                + "        \"qosClass\": \"Burstable\"\n"
-                + "      }\n"
-                + "    },\n"
-                + "    {\n"
-                + "      \"metadata\": {\n"
-                + "        \"name\": \"my-release-hazelcast-mancenter-f54949c7f-k8vx8\",\n"
-                + "        \"generateName\": \"my-release-hazelcast-mancenter-f54949c7f-\",\n"
-                + "        \"namespace\": \"default\",\n"
-                + "        \"selfLink\": \"/api/v1/namespaces/default/pods/my-release-hazelcast-mancenter-f54949c7f-k8vx8\",\n"
-                + "        \"uid\": \"21b7e41c-eefd-11e8-ab27-42010a8001ce\",\n"
-                + "        \"resourceVersion\": \"2025\",\n"
-                + "        \"creationTimestamp\": \"2018-11-23T08:52:39Z\",\n"
-                + "        \"labels\": {\n"
-                + "          \"app\": \"hazelcast\",\n"
-                + "          \"pod-template-hash\": \"910505739\",\n"
-                + "          \"release\": \"my-release\",\n"
-                + "          \"role\": \"mancenter\"\n"
-                + "        },\n"
-                + "        \"annotations\": {\n"
-                + "          \"kubernetes.io/limit-ranger\": \"LimitRanger plugin set: cpu request for container my-release-hazelcast-mancenter\"\n"
-                + "        },\n"
-                + "        \"ownerReferences\": [\n"
-                + "          {\n"
-                + "            \"apiVersion\": \"extensions/v1beta1\",\n"
-                + "            \"kind\": \"ReplicaSet\",\n"
-                + "            \"name\": \"my-release-hazelcast-mancenter-f54949c7f\",\n"
-                + "            \"uid\": \"21b450e8-eefd-11e8-ab27-42010a8001ce\",\n"
-                + "            \"controller\": true,\n"
-                + "            \"blockOwnerDeletion\": true\n"
-                + "          }\n"
-                + "        ]\n"
-                + "      },\n"
-                + "      \"spec\": {\n"
-                + "        \"volumes\": [\n"
-                + "          {\n"
-                + "            \"name\": \"mancenter-storage\",\n"
-                + "            \"persistentVolumeClaim\": {\n"
-                + "              \"claimName\": \"my-release-hazelcast-mancenter\"\n"
-                + "            }\n"
-                + "          },\n"
-                + "          {\n"
-                + "            \"name\": \"default-token-cvdgc\",\n"
-                + "            \"secret\": {\n"
-                + "              \"secretName\": \"default-token-cvdgc\",\n"
-                + "              \"defaultMode\": 420\n"
-                + "            }\n"
-                + "          }\n"
-                + "        ],\n"
-                + "        \"containers\": [\n"
-                + "          {\n"
-                + "            \"name\": \"my-release-hazelcast-mancenter\",\n"
-                + "            \"image\": \"hazelcast/management-center:latest\",\n"
-                + "            \"ports\": [\n"
-                + "              {\n"
-                + "                \"name\": \"mancenter\",\n"
-                + "                \"protocol\": \"TCP\"\n"
-                + "              }\n"
-                + "            ],\n"
-                + "            \"resources\": {\n"
-                + "              \"requests\": {\n"
-                + "                \"cpu\": \"100m\"\n"
-                + "              }\n"
-                + "            },\n"
-                + "            \"terminationMessagePath\": \"/dev/termination-log\",\n"
-                + "            \"terminationMessagePolicy\": \"File\",\n"
-                + "            \"imagePullPolicy\": \"Always\"\n"
-                + "          }\n"
-                + "        ],\n"
-                + "        \"restartPolicy\": \"Always\",\n"
-                + "        \"terminationGracePeriodSeconds\": 30,\n"
-                + "        \"dnsPolicy\": \"ClusterFirst\",\n"
-                + "        \"serviceAccountName\": \"default\",\n"
-                + "        \"serviceAccount\": \"default\",\n"
-                + "        \"nodeName\": \"gke-rafal-test-cluster-default-pool-e5fb2ea5-c7g8\",\n"
-                + "        \"securityContext\": {\n"
-                + "          \"runAsUser\": 0\n"
-                + "        },\n"
-                + "        \"schedulerName\": \"default-scheduler\",\n"
-                + "        \"tolerations\": [\n"
-                + "          {\n"
-                + "            \"key\": \"node.kubernetes.io/not-ready\",\n"
-                + "            \"operator\": \"Exists\",\n"
-                + "            \"effect\": \"NoExecute\",\n"
-                + "            \"tolerationSeconds\": 300\n"
-                + "          },\n"
-                + "          {\n"
-                + "            \"key\": \"node.kubernetes.io/unreachable\",\n"
-                + "            \"operator\": \"Exists\",\n"
-                + "            \"effect\": \"NoExecute\",\n"
-                + "            \"tolerationSeconds\": 300\n"
-                + "          }\n"
-                + "        ]\n"
-                + "      },\n"
-                + "      \"status\": {\n"
-                + "        \"phase\": \"Running\",\n"
-                + "        \"conditions\": [\n"
-                + "          {\n"
-                + "            \"type\": \"Initialized\",\n"
-                + "            \"status\": \"True\",\n"
-                + "            \"lastProbeTime\": null,\n"
-                + "            \"lastTransitionTime\": \"2018-11-23T08:52:46Z\"\n"
-                + "          },\n"
-                + "          {\n"
-                + "            \"type\": \"Ready\",\n"
-                + "            \"status\": \"True\",\n"
-                + "            \"lastProbeTime\": null,\n"
-                + "            \"lastTransitionTime\": \"2018-11-23T08:53:55Z\"\n"
-                + "          },\n"
-                + "          {\n"
-                + "            \"type\": \"PodScheduled\",\n"
-                + "            \"status\": \"True\",\n"
-                + "            \"lastProbeTime\": null,\n"
-                + "            \"lastTransitionTime\": \"2018-11-23T08:52:46Z\"\n"
-                + "          }\n"
-                + "        ],\n"
-                + "        \"hostIP\": \"10.240.0.18\",\n"
-                + "        \"podIP\": \"%s\",\n"
-                + "        \"startTime\": \"2018-11-23T08:52:46Z\",\n"
-                + "        \"containerStatuses\": [\n"
-                + "          {\n"
-                + "            \"name\": \"my-release-hazelcast-mancenter\",\n"
-                + "            \"state\": {\n"
-                + "              \"running\": {\n"
-                + "                \"startedAt\": \"2018-11-23T08:53:08Z\"\n"
-                + "              }\n"
-                + "            },\n"
-                + "            \"lastState\": {\n"
-                + "\n"
-                + "            },\n"
-                + "            \"ready\": false,\n"
-                + "            \"restartCount\": 0,\n"
-                + "            \"image\": \"hazelcast/management-center:latest\",\n"
-                + "            \"imageID\": \"docker-pullable://hazelcast/management-center@sha256:0427778a84476a7b11b248b1720d22e43ee689f5148506ea532491aad1a91afa\",\n"
-                + "            \"containerID\": \"docker://99ac6e5204876a3106b01d6b3ab2466b1d63c9bd1621d86d7e984840b2c44a3f\"\n"
-                + "          }\n"
-                + "        ],\n"
-                + "        \"qosClass\": \"Burstable\"\n"
-                + "      }\n"
-                + "    }\n"
-                + "  ]\n"
-                + "}", SAMPLE_PORT_1, SAMPLE_ADDRESS_1, SAMPLE_PORT_2, SAMPLE_ADDRESS_2, SAMPLE_NOT_READY_ADDRESS);
-    }
-
-    /**
-     * Real response recorded from the Kubernetes API call "/api/v1/namespaces/{namespace}/endpoints".
-     */
-    private static String endpointsListBody() {
-        return String.format(
-                "{" +
-                        "  \"kind\": \"EndpointsList\"," +
-                        "  \"apiVersion\": \"v1\"," +
-                        "  \"metadata\": {" +
-                        "    \"selfLink\": \"/api/v1/namespaces/default/endpoints\"," +
-                        "    \"resourceVersion\": \"8792\"" +
-                        "  }," +
-                        "  \"items\": [" +
-                        "    {" +
-                        "      \"metadata\": {" +
-                        "        \"name\": \"kubernetes\"," +
-                        "        \"namespace\": \"default\"," +
-                        "        \"selfLink\": \"/api/v1/namespaces/default/endpoints/kubernetes\"," +
-                        "        \"uid\": \"01c5aaa4-8411-11e8-abd2-00155d395157\"," +
-                        "        \"resourceVersion\": \"38\"," +
-                        "        \"creationTimestamp\": \"2018-07-10T07:15:21Z\"" +
-                        "      }," +
-                        "      \"subsets\": [" +
-                        "        {" +
-                        "          \"addresses\": [" +
-                        "            {" +
-                        "              \"ip\": \"%s\"," +
-                        "              \"hazelcast-service-port\" :%s" +
-                        "            }" +
-                        "          ]," +
-                        "          \"ports\": [" +
-                        "            {" +
-                        "              \"name\": \"https\"," +
-                        "              \"port\": 8443," +
-                        "              \"protocol\": \"TCP\"" +
-                        "            }" +
-                        "          ]" +
-                        "        }" +
-                        "      ]" +
-                        "    }," +
-                        "    {" +
-                        "      \"metadata\": {" +
-                        "        \"name\": \"my-hazelcast\"," +
-                        "        \"namespace\": \"default\"," +
-                        "        \"selfLink\": \"/api/v1/namespaces/default/endpoints/my-hazelcast\"," +
-                        "        \"uid\": \"80f7f03d-8425-11e8-abd2-00155d395157\"," +
-                        "        \"resourceVersion\": \"8788\"," +
-                        "        \"creationTimestamp\": \"2018-07-10T09:42:04Z\"," +
-                        "        \"labels\": {" +
-                        "          \"app\": \"hazelcast\"," +
-                        "          \"chart\": \"hazelcast-1.0.0\"," +
-                        "          \"heritage\": \"Tiller\"," +
-                        "          \"release\": \"my-hazelcast\"" +
-                        "        }" +
-                        "      }," +
-                        "      \"subsets\": [" +
-                        "        {" +
-                        "          \"addresses\": [" +
-                        "            {" +
-                        "              \"ip\": \"%s\"," +
-                        "              \"nodeName\": \"minikube\"," +
-                        "              \"hazelcast-service-port\" : %s," +
-                        "              \"targetRef\": {" +
-                        "                \"kind\": \"Pod\"," +
-                        "                \"namespace\": \"default\"," +
-                        "                \"name\": \"my-hazelcast-0\"," +
-                        "                \"uid\": \"80f20bcb-8425-11e8-abd2-00155d395157\"," +
-                        "                \"resourceVersion\": \"8771\"" +
-                        "              }" +
-                        "            }" +
-                        "          ]," +
-                        "          \"notReadyAddresses\": [" +
-                        "            {" +
-                        "              \"ip\": \"%s\"," +
-                        "              \"nodeName\": \"minikube\"," +
-                        "              \"targetRef\": {" +
-                        "                \"kind\": \"Pod\"," +
-                        "                \"namespace\": \"default\"," +
-                        "                \"name\": \"my-hazelcast-1\"," +
-                        "                \"uid\": \"95bd3636-8425-11e8-abd2-00155d395157\"," +
-                        "                \"resourceVersion\": \"8787\"" +
-                        "              }" +
-                        "            }" +
-                        "          ]," +
-                        "          \"ports\": [" +
-                        "            {" +
-                        "              \"name\": \"hzport\"," +
-                        "              \"port\": 5701," +
-                        "              \"protocol\": \"TCP\"" +
-                        "            }" +
-                        "          ]" +
-                        "        }" +
-                        "      ]" +
-                        "    }" +
-                        "  ]" +
-                        "}"
-                , SAMPLE_ADDRESS_1, SAMPLE_PORT_1, SAMPLE_ADDRESS_2, SAMPLE_PORT_2, SAMPLE_NOT_READY_ADDRESS);
-    }
-
-    /**
-     * Real response recorded from the Kubernetes API call "/api/v1/namespaces/{namespace}/endpoints/{endpoint}".
-     */
-    private static String endpointsBody() {
-        return String.format(
-                "{" +
-                        "  \"kind\": \"Endpoints\"," +
-                        "  \"apiVersion\": \"v1\"," +
-                        "  \"metadata\": {" +
-                        "    \"name\": \"my-hazelcast\"," +
-                        "    \"namespace\": \"default\"," +
-                        "    \"selfLink\": \"/api/v1/namespaces/default/endpoints/my-hazelcast\"," +
-                        "    \"uid\": \"deefb354-8443-11e8-abd2-00155d395157\"," +
-                        "    \"resourceVersion\": \"18816\"," +
-                        "    \"creationTimestamp\": \"2018-07-10T13:19:27Z\"," +
-                        "    \"labels\": {" +
-                        "      \"app\": \"hazelcast\"," +
-                        "      \"chart\": \"hazelcast-1.0.0\"," +
-                        "      \"heritage\": \"Tiller\"," +
-                        "      \"release\": \"my-hazelcast\"" +
-                        "    }" +
-                        "  }," +
-                        "  \"subsets\": [" +
-                        "    {" +
-                        "      \"addresses\": [" +
-                        "        {" +
-                        "          \"ip\": \"%s\"," +
-                        "          \"nodeName\": \"minikube\"," +
-                        "          \"hazelcast-service-port\" : %s," +
-                        "          \"targetRef\": {" +
-                        "            \"kind\": \"Pod\"," +
-                        "            \"namespace\": \"default\"," +
-                        "            \"name\": \"my-hazelcast-0\"," +
-                        "            \"uid\": \"def2f426-8443-11e8-abd2-00155d395157\"," +
-                        "            \"resourceVersion\": \"18757\"" +
-                        "          }" +
-                        "        }," +
-                        "        {" +
-                        "          \"ip\": \"%s\"," +
-                        "          \"nodeName\": \"minikube\"," +
-                        "          \"hazelcast-service-port\" : %s," +
-                        "          \"targetRef\": {" +
-                        "            \"kind\": \"Pod\"," +
-                        "            \"namespace\": \"default\"," +
-                        "            \"name\": \"my-hazelcast-1\"," +
-                        "            \"uid\": \"f3b96106-8443-11e8-abd2-00155d395157\"," +
-                        "            \"resourceVersion\": \"18815\"" +
-                        "          }" +
-                        "        }" +
-                        "      ]," +
-                        "      \"ports\": [" +
-                        "        {" +
-                        "          \"name\": \"hzport\"," +
-                        "          \"port\": 5701," +
-                        "          \"protocol\": \"TCP\"" +
-                        "        }" +
-                        "      ]" +
-                        "    }" +
-                        "  ]" +
-                        "}"
-                , SAMPLE_ADDRESS_1, SAMPLE_PORT_1, SAMPLE_ADDRESS_2, SAMPLE_PORT_2);
-    }
-
-    /**
-     * Real response recorded from the Kubernetes API call "/api/v1/namespaces/{namespace}/pods/{pod-name}".
-     */
-    private static String podBody(String nodeName) {
-        return String.format(
-                "{  \"kind\": \"Pod\",\n"
-                        + "  \"apiVersion\": \"v1\",\n"
-                        + "  \"metadata\": {\n"
-                        + "    \"name\": \"my-release-hazelcast-0\",\n"
-                        + "    \"generateName\": \"my-release-hazelcast-\",\n"
-                        + "    \"namespace\": \"default\",\n"
-                        + "    \"selfLink\": \"/api/v1/namespaces/default/pods/my-release-hazelcast-0\",\n"
-                        + "    \"uid\": \"53112ead-0511-11e9-9c53-42010a800013\",\n"
-                        + "    \"resourceVersion\": \"7724\",\n"
-                        + "    \"creationTimestamp\": \"2018-12-21T11:12:37Z\",\n"
-                        + "    \"labels\": {\n"
-                        + "      \"app\": \"hazelcast\",\n"
-                        + "      \"controller-revision-hash\": \"my-release-hazelcast-695d9d97dd\",\n"
-                        + "      \"release\": \"my-release\",\n"
-                        + "      \"role\": \"hazelcast\",\n"
-                        + "      \"statefulset.kubernetes.io/pod-name\": \"my-release-hazelcast-0\"\n"
-                        + "    },\n"
-                        + "    \"annotations\": {\n"
-                        + "      \"kubernetes.io/limit-ranger\": \"LimitRanger plugin set: cpu request for container my-release-hazelcast\"\n"
-                        + "    },\n"
-                        + "    \"ownerReferences\": [\n"
-                        + "      {\n"
-                        + "        \"apiVersion\": \"apps/v1\",\n"
-                        + "        \"kind\": \"StatefulSet\",\n"
-                        + "        \"name\": \"my-release-hazelcast\",\n"
-                        + "        \"uid\": \"53096f3b-0511-11e9-9c53-42010a800013\",\n"
-                        + "        \"controller\": true,\n"
-                        + "        \"blockOwnerDeletion\": true\n"
-                        + "      }\n"
-                        + "    ]\n"
-                        + "  },\n"
-                        + "  \"spec\": {\n"
-                        + "    \"volumes\": [\n"
-                        + "      {\n"
-                        + "        \"name\": \"hazelcast-storage\",\n"
-                        + "        \"configMap\": {\n"
-                        + "          \"name\": \"my-release-hazelcast-configuration\",\n"
-                        + "          \"defaultMode\": 420\n"
-                        + "        }\n"
-                        + "      },\n"
-                        + "      {\n"
-                        + "        \"name\": \"my-release-hazelcast-token-j89v4\",\n"
-                        + "        \"secret\": {\n"
-                        + "          \"secretName\": \"my-release-hazelcast-token-j89v4\",\n"
-                        + "          \"defaultMode\": 420\n"
-                        + "        }\n"
-                        + "      }\n"
-                        + "    ],\n"
-                        + "    \"containers\": [\n"
-                        + "      {\n"
-                        + "        \"name\": \"my-release-hazelcast\",\n"
-                        + "        \"image\": \"hazelcast/hazelcast:latest\",\n"
-                        + "        \"ports\": [\n"
-                        + "          {\n"
-                        + "            \"name\": \"hazelcast\",\n"
-                        + "            \"containerPort\": 5701,\n"
-                        + "            \"protocol\": \"TCP\"\n"
-                        + "          }\n"
-                        + "        ],\n"
-                        + "        \"env\": [\n"
-                        + "          {\n"
-                        + "            \"name\": \"JAVA_OPTS\",\n"
-                        + "            \"value\": \"-Dhazelcast.rest.enabled=true -Dhazelcast.config=/data/hazelcast/hazelcast.xml -DserviceName=my-release-hazelcast -Dnamespace=default -Dhazelcast.mancenter.enabled=true -Dhazelcast.mancenter.url=http://my-release-hazelcast-mancenter:8080/hazelcast-mancenter -Dhazelcast.shutdownhook.policy=GRACEFUL -Dhazelcast.shutdownhook.enabled=true -Dhazelcast.graceful.shutdown.max.wait=600 \"\n"
-                        + "          }\n"
-                        + "        ],\n"
-                        + "        \"resources\": {\n"
-                        + "          \"requests\": {\n"
-                        + "            \"cpu\": \"100m\"\n"
-                        + "          }\n"
-                        + "        },\n"
-                        + "        \"volumeMounts\": [\n"
-                        + "          {\n"
-                        + "            \"name\": \"hazelcast-storage\",\n"
-                        + "            \"mountPath\": \"/data/hazelcast\"\n"
-                        + "          },\n"
-                        + "          {\n"
-                        + "            \"name\": \"my-release-hazelcast-token-j89v4\",\n"
-                        + "            \"readOnly\": true,\n"
-                        + "            \"mountPath\": \"/var/run/secrets/kubernetes.io/serviceaccount\"\n"
-                        + "          }\n"
-                        + "        ],\n"
-                        + "        \"terminationMessagePath\": \"/dev/termination-log\",\n"
-                        + "        \"terminationMessagePolicy\": \"File\",\n"
-                        + "        \"imagePullPolicy\": \"Always\"\n"
-                        + "      }\n"
-                        + "    ],\n"
-                        + "    \"restartPolicy\": \"Always\",\n"
-                        + "    \"terminationGracePeriodSeconds\": 600,\n"
-                        + "    \"dnsPolicy\": \"ClusterFirst\",\n"
-                        + "    \"serviceAccountName\": \"my-release-hazelcast\",\n"
-                        + "    \"serviceAccount\": \"my-release-hazelcast\",\n"
-                        + "    \"nodeName\": \"%s\",\n"
-                        + "    \"securityContext\": {\n"
-                        + "      \"runAsUser\": 1001,\n"
-                        + "      \"fsGroup\": 1001\n"
-                        + "    },\n"
-                        + "    \"hostname\": \"my-release-hazelcast-0\",\n"
-                        + "    \"schedulerName\": \"default-scheduler\",\n"
-                        + "    \"tolerations\": [\n"
-                        + "      {\n"
-                        + "        \"key\": \"node.kubernetes.io/not-ready\",\n"
-                        + "        \"operator\": \"Exists\",\n"
-                        + "        \"effect\": \"NoExecute\",\n"
-                        + "        \"tolerationSeconds\": 300\n"
-                        + "      },\n"
-                        + "      {\n"
-                        + "        \"key\": \"node.kubernetes.io/unreachable\",\n"
-                        + "        \"operator\": \"Exists\",\n"
-                        + "        \"effect\": \"NoExecute\",\n"
-                        + "        \"tolerationSeconds\": 300\n"
-                        + "      }\n"
-                        + "    ]\n"
-                        + "  },\n"
-                        + "  \"status\": {\n"
-                        + "  }\n"
-                        + "}", nodeName);
-    }
-
-    /**
-     * Real response recorded from the Kubernetes (version < 1.13) API call "/api/v1/nodes/{node-name}".
-     */
-    private static String nodeBetaBody() {
-        return String.format(
-                "{"
-                        + "  \"kind\": \"Node\",\n"
-                        + "  \"apiVersion\": \"v1\",\n"
-                        + "  \"metadata\": {\n"
-                        + "    \"name\": \"gke-rafal-test-cluster-default-pool-9238654c-12tz\",\n"
-                        + "    \"selfLink\": \"/api/v1/nodes/gke-rafal-test-cluster-default-pool-9238654c-12tz\",\n"
-                        + "    \"uid\": \"ceab9c17-0508-11e9-9c53-42010a800013\",\n"
-                        + "    \"resourceVersion\": \"7954\",\n"
-                        + "    \"creationTimestamp\": \"2018-12-21T10:11:39Z\",\n"
-                        + "    \"labels\": {\n"
-                        + "      \"beta.kubernetes.io/arch\": \"amd64\",\n"
-                        + "      \"beta.kubernetes.io/fluentd-ds-ready\": \"true\",\n"
-                        + "      \"beta.kubernetes.io/instance-type\": \"n1-standard-1\",\n"
-                        + "      \"beta.kubernetes.io/os\": \"linux\",\n"
-                        + "      \"cloud.google.com/gke-nodepool\": \"default-pool\",\n"
-                        + "      \"cloud.google.com/gke-os-distribution\": \"cos\",\n"
-                        + "      \"failure-domain.beta.kubernetes.io/region\": \"us-central1\",\n"
-                        + "      \"failure-domain.beta.kubernetes.io/zone\": \"%s\",\n"
-                        + "      \"kubernetes.io/hostname\": \"gke-rafal-test-cluster-default-pool-9238654c-12tz\"\n"
-                        + "    },\n"
-                        + "    \"annotations\": {\n"
-                        + "      \"node.alpha.kubernetes.io/ttl\": \"0\",\n"
-                        + "      \"volumes.kubernetes.io/controller-managed-attach-detach\": \"true\"\n"
-                        + "    }\n"
-                        + "  },\n"
-                        + "  \"spec\": {\n"
-                        + "  },\n"
-                        + "  \"status\": {\n"
-                        + "  }\n"
-                        + "}", ZONE);
-    }
-
-    /**
-     * Real response recorded from the Kubernetes (version >= 1.13) API call "/api/v1/nodes/{node-name}".
-     */
-    private static String nodeBody() {
-        return String.format(
-                "{"
-                        + "  \"kind\": \"Node\",\n"
-                        + "  \"apiVersion\": \"v1\",\n"
-                        + "  \"metadata\": {\n"
-                        + "    \"name\": \"gke-rafal-test-cluster-default-pool-9238654c-12tz\",\n"
-                        + "    \"selfLink\": \"/api/v1/nodes/gke-rafal-test-cluster-default-pool-9238654c-12tz\",\n"
-                        + "    \"uid\": \"ceab9c17-0508-11e9-9c53-42010a800013\",\n"
-                        + "    \"resourceVersion\": \"7954\",\n"
-                        + "    \"creationTimestamp\": \"2018-12-21T10:11:39Z\",\n"
-                        + "    \"labels\": {\n"
-                        + "      \"beta.kubernetes.io/arch\": \"amd64\",\n"
-                        + "      \"beta.kubernetes.io/fluentd-ds-ready\": \"true\",\n"
-                        + "      \"beta.kubernetes.io/instance-type\": \"n1-standard-1\",\n"
-                        + "      \"beta.kubernetes.io/os\": \"linux\",\n"
-                        + "      \"cloud.google.com/gke-nodepool\": \"default-pool\",\n"
-                        + "      \"cloud.google.com/gke-os-distribution\": \"cos\",\n"
-                        + "      \"failure-domain.beta.kubernetes.io/region\": \"deprecated-region\",\n"
-                        + "      \"failure-domain.beta.kubernetes.io/zone\": \"deprecated-zone\",\n"
-                        + "      \"failure-domain.kubernetes.io/region\": \"us-central1\",\n"
-                        + "      \"failure-domain.kubernetes.io/zone\": \"%s\",\n"
-                        + "      \"kubernetes.io/hostname\": \"gke-rafal-test-cluster-default-pool-9238654c-12tz\"\n"
-                        + "    },\n"
-                        + "    \"annotations\": {\n"
-                        + "      \"node.alpha.kubernetes.io/ttl\": \"0\",\n"
-                        + "      \"volumes.kubernetes.io/controller-managed-attach-detach\": \"true\"\n"
-                        + "    }\n"
-                        + "  },\n"
-                        + "  \"spec\": {\n"
-                        + "  },\n"
-                        + "  \"status\": {\n"
-                        + "  }\n"
-                        + "}", ZONE);
-    }
-
-    /**
-     * Real response recorded from the Kubernetes API.
-     */
-    private static String forbiddenBody() {
-        return "Forbidden!Configured service account doesn't have access. Service account may have been revoked. "
-                + "endpoints is forbidden: User \"system:serviceaccount:default:default\" cannot list endpoints "
-                + "in the namespace \"default\"";
-    }
-
-    private static String malformedBody() {
-        return "malformed response";
-    }
-
-    private static List<String> extractIpPort(List<EntrypointAddress> addresses) {
-        List<String> result = new ArrayList<String>();
-        for (EntrypointAddress address : addresses) {
-            String ip = address.getIp();
-            Integer port = address.getPort();
-            result.add(ipPort(ip, port));
-        }
-        return result;
-    }
-
-    private static String ipPort(String ip, Integer port) {
-        return String.format("%s:%s", ip, port);
-    }
-}
diff --git a/src/test/java/com/hazelcast/kubernetes/HazelcastKubernetesDiscoveryStrategyFactoryTest.java b/src/test/java/com/hazelcast/kubernetes/HazelcastKubernetesDiscoveryStrategyFactoryTest.java
index b9257b7..43f43fb 100644
--- a/src/test/java/com/hazelcast/kubernetes/HazelcastKubernetesDiscoveryStrategyFactoryTest.java
+++ b/src/test/java/com/hazelcast/kubernetes/HazelcastKubernetesDiscoveryStrategyFactoryTest.java
@@ -47,12 +47,12 @@ public class HazelcastKubernetesDiscoveryStrategyFactoryTest {
     DiscoveryNode discoveryNode;
 
     @Mock
-    private DefaultKubernetesClient client;
+    private KubernetesClient client;
 
     @Before
     public void setup()
             throws Exception {
-        PowerMockito.whenNew(DefaultKubernetesClient.class).withAnyArguments().thenReturn(client);
+        PowerMockito.whenNew(KubernetesClient.class).withAnyArguments().thenReturn(client);
     }
 
     @Test
diff --git a/src/test/java/com/hazelcast/kubernetes/KubernetesApiEndpointResolverTest.java b/src/test/java/com/hazelcast/kubernetes/KubernetesApiEndpointResolverTest.java
index 26402ca..683cb4d 100644
--- a/src/test/java/com/hazelcast/kubernetes/KubernetesApiEndpointResolverTest.java
+++ b/src/test/java/com/hazelcast/kubernetes/KubernetesApiEndpointResolverTest.java
@@ -16,8 +16,7 @@
 
 package com.hazelcast.kubernetes;
 
-import com.hazelcast.kubernetes.KubernetesClient.Endpoints;
-import com.hazelcast.kubernetes.KubernetesClient.EntrypointAddress;
+import com.hazelcast.kubernetes.KubernetesClient.Endpoint;
 import com.hazelcast.logging.ILogger;
 import com.hazelcast.logging.NoLogFactory;
 import com.hazelcast.spi.discovery.DiscoveryNode;
@@ -30,7 +29,6 @@ import org.powermock.core.classloader.annotations.PrepareForTest;
 import org.powermock.modules.junit4.PowerMockRunner;
 
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 
 import static java.util.Arrays.asList;
@@ -45,26 +43,23 @@ public class KubernetesApiEndpointResolverTest {
     private static final String SERVICE_LABEL = "theLabel";
     private static final String SERVICE_LABEL_VALUE = "serviceLabelValue";
     private static final Boolean RESOLVE_NOT_READY_ADDRESSES = true;
-    private static final String NAMESPACE = "theNamespace";
 
     @Mock
-    private RetryKubernetesClient client;
+    private KubernetesClient client;
 
     @Before
     public void setup()
             throws Exception {
-        PowerMockito.whenNew(RetryKubernetesClient.class).withAnyArguments().thenReturn(client);
+        PowerMockito.whenNew(KubernetesClient.class).withAnyArguments().thenReturn(client);
     }
 
     @Test
     public void resolveWhenNodeInFound() {
         // given
-        Endpoints endpoints = new Endpoints(Collections.<EntrypointAddress>emptyList(),
-                Collections.<EntrypointAddress>emptyList());
-        given(client.endpoints(NAMESPACE)).willReturn(endpoints);
+        List<Endpoint> endpoints = Collections.<Endpoint>emptyList();
+        given(client.endpoints()).willReturn(endpoints);
 
-        KubernetesApiEndpointResolver sut = new KubernetesApiEndpointResolver(LOGGER, null, 0, null, null, NAMESPACE, null,
-                client);
+        KubernetesApiEndpointResolver sut = new KubernetesApiEndpointResolver(LOGGER, null, 0, null, null, null, client);
 
         // when
         List<DiscoveryNode> nodes = sut.resolve();
@@ -85,11 +80,11 @@ public class KubernetesApiEndpointResolverTest {
 
     private void resolveWithServiceNameWhenNodeInNamespace(final int port, final int expectedPort) {
         // given
-        Endpoints endpoints = createEndpoints(1);
-        given(client.endpointsByName(NAMESPACE, SERVICE_NAME)).willReturn(endpoints);
+        List<Endpoint> endpoints = createEndpoints(1);
+        given(client.endpointsByName(SERVICE_NAME)).willReturn(endpoints);
 
-        KubernetesApiEndpointResolver sut = new KubernetesApiEndpointResolver(LOGGER, SERVICE_NAME, port, null, null, NAMESPACE,
-                null, client);
+        KubernetesApiEndpointResolver sut = new KubernetesApiEndpointResolver(LOGGER, SERVICE_NAME, port, null, null, null,
+                client);
 
         // when
         List<DiscoveryNode> nodes = sut.resolve();
@@ -102,11 +97,11 @@ public class KubernetesApiEndpointResolverTest {
     @Test
     public void resolveWithServiceLabelWhenNodeWithServiceLabel() {
         // given
-        Endpoints endpoints = createEndpoints(2);
-        given(client.endpointsByLabel(NAMESPACE, SERVICE_LABEL, SERVICE_LABEL_VALUE)).willReturn(endpoints);
+        List<Endpoint> endpoints = createEndpoints(2);
+        given(client.endpointsByLabel(SERVICE_LABEL, SERVICE_LABEL_VALUE)).willReturn(endpoints);
 
         KubernetesApiEndpointResolver sut = new KubernetesApiEndpointResolver(LOGGER, null, 0, SERVICE_LABEL, SERVICE_LABEL_VALUE,
-                NAMESPACE, null, client);
+                null, client);
 
         // when
         List<DiscoveryNode> nodes = sut.resolve();
@@ -119,10 +114,10 @@ public class KubernetesApiEndpointResolverTest {
     @Test
     public void resolveWithServiceNameWhenNotReadyAddressesAndNotReadyEnabled() {
         // given
-        Endpoints endpoints = createNotReadyEndpoints(2);
-        given(client.endpointsByName(NAMESPACE, SERVICE_NAME)).willReturn(endpoints);
+        List<Endpoint> endpoints = createNotReadyEndpoints(2);
+        given(client.endpointsByName(SERVICE_NAME)).willReturn(endpoints);
 
-        KubernetesApiEndpointResolver sut = new KubernetesApiEndpointResolver(LOGGER, SERVICE_NAME, 0, null, null, NAMESPACE,
+        KubernetesApiEndpointResolver sut = new KubernetesApiEndpointResolver(LOGGER, SERVICE_NAME, 0, null, null,
                 RESOLVE_NOT_READY_ADDRESSES, client);
 
         // when
@@ -135,11 +130,10 @@ public class KubernetesApiEndpointResolverTest {
     @Test
     public void resolveWithServiceNameWhenNotReadyAddressesAndNotReadyDisabled() {
         // given
-        Endpoints endpoints = createNotReadyEndpoints(2);
-        given(client.endpointsByName(NAMESPACE, SERVICE_NAME)).willReturn(endpoints);
+        List<Endpoint> endpoints = createNotReadyEndpoints(2);
+        given(client.endpointsByName(SERVICE_NAME)).willReturn(endpoints);
 
-        KubernetesApiEndpointResolver sut = new KubernetesApiEndpointResolver(LOGGER, SERVICE_NAME, 0, null, null, NAMESPACE,
-                null,
+        KubernetesApiEndpointResolver sut = new KubernetesApiEndpointResolver(LOGGER, SERVICE_NAME, 0, null, null, null,
                 client);
 
         // when
@@ -149,16 +143,16 @@ public class KubernetesApiEndpointResolverTest {
         assertEquals(0, nodes.size());
     }
 
-    private static Endpoints createEndpoints(int customPort) {
-        return new Endpoints(asList(createEntrypointAddress(customPort)), Collections.<EntrypointAddress>emptyList());
+    private static List<Endpoint> createEndpoints(int customPort) {
+        return asList(createEntrypointAddress(customPort, true));
     }
 
-    private static Endpoints createNotReadyEndpoints(int customPort) {
-        return new Endpoints(Collections.<EntrypointAddress>emptyList(), asList(createEntrypointAddress(customPort)));
+    private static List<Endpoint> createNotReadyEndpoints(int customPort) {
+        return asList(createEntrypointAddress(customPort, false));
     }
 
-    private static EntrypointAddress createEntrypointAddress(int customPort) {
+    private static Endpoint createEntrypointAddress(int customPort, boolean isReady) {
         String ip = "1.1.1.1";
-        return new EntrypointAddress(ip, customPort, new HashMap<String, Object>());
+        return new Endpoint(new KubernetesClient.EndpointAddress(ip, customPort), isReady);
     }
 }
\ No newline at end of file
diff --git a/src/test/java/com/hazelcast/kubernetes/KubernetesClientTest.java b/src/test/java/com/hazelcast/kubernetes/KubernetesClientTest.java
new file mode 100644
index 0000000..6ac7fb8
--- /dev/null
+++ b/src/test/java/com/hazelcast/kubernetes/KubernetesClientTest.java
@@ -0,0 +1,654 @@
+/*
+ * Copyright (c) 2008-2018, Hazelcast, Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hazelcast.kubernetes;
+
+import com.github.tomakehurst.wiremock.client.MappingBuilder;
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import com.hazelcast.kubernetes.KubernetesClient.Endpoint;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlMatching;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlPathMatching;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.singletonMap;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.junit.Assert.assertEquals;
+
+public class KubernetesClientTest {
+    private static final String KUBERNETES_MASTER_IP = "localhost";
+
+    private static final String TOKEN = "sample-token";
+    private static final String CA_CERTIFICATE = "sample-ca-certificate";
+    private static final String NAMESPACE = "sample-namespace";
+
+    @Rule
+    public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort());
+
+    private KubernetesClient kubernetesClient;
+
+    @Before
+    public void setUp() {
+        String kubernetesMasterUrl = String.format("http://%s:%d", KUBERNETES_MASTER_IP, wireMockRule.port());
+        kubernetesClient = new KubernetesClient(NAMESPACE, kubernetesMasterUrl, TOKEN, CA_CERTIFICATE);
+        stubFor(get(urlMatching("/api/.*")).atPriority(5)
+                                           .willReturn(aResponse().withStatus(401).withBody("\"reason\":\"Forbidden\"")));
+    }
+
+    @Test
+    public void endpointsByNamespace() {
+        // given
+        //language=JSON
+        String podsListResponse = "{\n"
+                + "  \"items\": [\n"
+                + "    {\n"
+                + "      \"spec\": {\n"
+                + "        \"containers\": [\n"
+                + "          {\n"
+                + "            \"ports\": [\n"
+                + "              {\n"
+                + "                \"containerPort\": 5701\n"
+                + "              }\n"
+                + "            ]\n"
+                + "          }\n"
+                + "        ]\n"
+                + "      },\n"
+                + "      \"status\": {\n"
+                + "        \"podIP\": \"192.168.0.25\",\n"
+                + "        \"containerStatuses\": [\n"
+                + "          {\n"
+                + "            \"ready\": true\n"
+                + "          }\n"
+                + "        ]\n"
+                + "      }\n"
+                + "    },\n"
+                + "    {\n"
+                + "      \"spec\": {\n"
+                + "        \"containers\": [\n"
+                + "          {\n"
+                + "            \"ports\": [\n"
+                + "              {\n"
+                + "                \"containerPort\": 5702\n"
+                + "              }\n"
+                + "            ]\n"
+                + "          }\n"
+                + "        ]\n"
+                + "      },\n"
+                + "      \"status\": {\n"
+                + "        \"podIP\": \"172.17.0.5\",\n"
+                + "        \"containerStatuses\": [\n"
+                + "          {\n"
+                + "            \"ready\": true\n"
+                + "          }\n"
+                + "        ]\n"
+                + "      }\n"
+                + "    },\n"
+                + "    {\n"
+                + "      \"spec\": {\n"
+                + "        \"containers\": [\n"
+                + "          {\n"
+                + "            \"ports\": [\n"
+                + "              {\n"
+                + "              }\n"
+                + "            ]\n"
+                + "          }\n"
+                + "        ]\n"
+                + "      },\n"
+                + "      \"status\": {\n"
+                + "        \"podIP\": \"172.17.0.6\",\n"
+                + "        \"containerStatuses\": [\n"
+                + "          {\n"
+                + "            \"ready\": false\n"
+                + "          }\n"
+                + "        ]\n"
+                + "      }\n"
+                + "    }\n"
+                + "  ]\n"
+                + "}";
+        stub(String.format("/api/v1/namespaces/%s/pods", NAMESPACE), podsListResponse);
+
+        // when
+        List<Endpoint> result = kubernetesClient.endpoints();
+
+        // then
+        assertThat(format(result),
+                containsInAnyOrder(ready("192.168.0.25", 5701), ready("172.17.0.5", 5702), notReady("172.17.0.6", null)));
+    }
+
+    @Test
+    public void endpointsByNamespaceAndLabel() {
+        // given
+        //language=JSON
+        String endpointsListResponse = "{\n"
+                + "  \"kind\": \"EndpointsList\",\n"
+                + "  \"items\": [\n"
+                + "    {\n"
+                + "      \"subsets\": [\n"
+                + "        {\n"
+                + "          \"addresses\": [\n"
+                + "            {\n"
+                + "              \"ip\": \"192.168.0.25\",\n"
+                + "              \"hazelcast-service-port\": 5701\n"
+                + "            }\n"
+                + "          ]\n"
+                + "        }\n"
+                + "      ]\n"
+                + "    },\n"
+                + "    {\n"
+                + "      \"subsets\": [\n"
+                + "        {\n"
+                + "          \"addresses\": [\n"
+                + "            {\n"
+                + "              \"ip\": \"172.17.0.5\",\n"
+                + "              \"hazelcast-service-port\": 5702\n"
+                + "            }\n"
+                + "          ],\n"
+                + "          \"notReadyAddresses\": [\n"
+                + "            {\n"
+                + "              \"ip\": \"172.17.0.6\"\n"
+                + "            }\n"
+                + "          ],\n"
+                + "          \"ports\": [\n"
+                + "            {\n"
+                + "              \"port\": 5701\n"
+                + "            }\n"
+                + "          ]\n"
+                + "        }\n"
+                + "      ]\n"
+                + "    }\n"
+                + "  ]\n"
+                + "}";
+        String serviceLabel = "sample-service-label";
+        String serviceLabelValue = "sample-service-label-value";
+        Map<String, String> queryParams = singletonMap("labelSelector", String.format("%s=%s", serviceLabel, serviceLabelValue));
+        stub(String.format("/api/v1/namespaces/%s/endpoints", NAMESPACE), queryParams, endpointsListResponse);
+
+        // when
+        List<Endpoint> result = kubernetesClient.endpointsByLabel(serviceLabel, serviceLabelValue);
+
+        // then
+        assertThat(format(result),
+                containsInAnyOrder(ready("192.168.0.25", 5701), ready("172.17.0.5", 5702), notReady("172.17.0.6", 5701)));
+    }
+
+    @Test
+    public void endpointsByNamespaceAndServiceName() {
+        // given
+        //language=JSON
+        String endpointResponse = "{\n"
+                + "  \"kind\": \"Endpoints\",\n"
+                + "  \"subsets\": [\n"
+                + "    {\n"
+                + "      \"addresses\": [\n"
+                + "        {\n"
+                + "          \"ip\": \"192.168.0.25\",\n"
+                + "          \"hazelcast-service-port\": 5701\n"
+                + "        },\n"
+                + "        {\n"
+                + "          \"ip\": \"172.17.0.5\",\n"
+                + "          \"hazelcast-service-port\": 5702\n"
+                + "        }\n"
+                + "      ]\n"
+                + "    }\n"
+                + "  ]\n"
+                + "}";
+        String serviceName = "service-name";
+        stub(String.format("/api/v1/namespaces/%s/endpoints/%s", NAMESPACE, serviceName), endpointResponse);
+
+        // when
+        List<Endpoint> result = kubernetesClient.endpointsByName(serviceName);
+
+        // then
+        assertThat(format(result), containsInAnyOrder(ready("192.168.0.25", 5701), ready("172.17.0.5", 5702)));
+    }
+
+    @Test
+    public void zoneBeta() {
+        // given
+        String podName = "pod-name";
+
+        //language=JSON
+        String podResponse = "{\n"
+                + "  \"kind\": \"Pod\",\n"
+                + "  \"spec\": {\n"
+                + "    \"nodeName\": \"node-name\"\n"
+                + "  }\n"
+                + "}";
+        stub(String.format("/api/v1/namespaces/%s/pods/%s", NAMESPACE, podName), podResponse);
+
+        //language=JSON
+        String nodeResponse = "{\n"
+                + "  \"kind\": \"Node\",\n"
+                + "  \"metadata\": {\n"
+                + "    \"labels\": {\n"
+                + "      \"failure-domain.beta.kubernetes.io/region\": \"us-central1\",\n"
+                + "      \"failure-domain.beta.kubernetes.io/zone\": \"us-central1-a\"\n"
+                + "    }\n"
+                + "  }\n"
+                + "}";
+        stub("/api/v1/nodes/node-name", nodeResponse);
+
+        // when
+        String zone = kubernetesClient.zone(podName);
+
+        // then
+        assertEquals("us-central1-a", zone);
+    }
+
+    @Test
+    public void zone() {
+        // given
+        String podName = "pod-name";
+
+        //language=JSON
+        String podResponse = "{\n"
+                + "  \"kind\": \"Pod\",\n"
+                + "  \"spec\": {\n"
+                + "    \"nodeName\": \"node-name\"\n"
+                + "  }\n"
+                + "}";
+        stub(String.format("/api/v1/namespaces/%s/pods/%s", NAMESPACE, podName), podResponse);
+
+        //language=JSON
+        String nodeResponse = "{\n"
+                + "  \"kind\": \"Node\",\n"
+                + "  \"metadata\": {\n"
+                + "    \"labels\": {\n"
+                + "      \"failure-domain.beta.kubernetes.io/region\": \"deprecated-region\",\n"
+                + "      \"failure-domain.beta.kubernetes.io/zone\": \"deprecated-zone\",\n"
+                + "      \"failure-domain.kubernetes.io/region\": \"us-central1\",\n"
+                + "      \"failure-domain.kubernetes.io/zone\": \"us-central1-a\"\n"
+                + "    }\n"
+                + "  }\n"
+                + "}";
+        stub("/api/v1/nodes/node-name", nodeResponse);
+
+        // when
+        String zone = kubernetesClient.zone(podName);
+
+        // then
+        assertEquals("us-central1-a", zone);
+    }
+
+    @Test
+    public void endpointsByNamespaceWithLoadBalancerPublicIp() {
+        // given
+        stub(String.format("/api/v1/namespaces/%s/pods", NAMESPACE), podsListResponse());
+        stub(String.format("/api/v1/namespaces/%s/endpoints", NAMESPACE), endpointsListResponse());
+
+        //language=JSON
+        String serviceResponse1 = "{\n"
+                + "  \"kind\": \"Service\",\n"
+                + "  \"spec\": {\n"
+                + "    \"ports\": [\n"
+                + "      {\n"
+                + "        \"port\": 32123,\n"
+                + "        \"targetPort\": 5701,\n"
+                + "        \"nodePort\": 31916\n"
+                + "      }\n"
+                + "    ]\n"
+                + "  },\n"
+                + "  \"status\": {\n"
+                + "    \"loadBalancer\": {\n"
+                + "      \"ingress\": [\n"
+                + "        {\n"
+                + "          \"ip\": \"35.232.226.200\"\n"
+                + "        }\n"
+                + "      ]\n"
+                + "    }\n"
+                + "  }\n"
+                + "}\n";
+        stub(String.format("/api/v1/namespaces/%s/services/service-0", NAMESPACE), serviceResponse1);
+
+        //language=JSON
+        String serviceResponse2 = "{\n"
+                + "  \"kind\": \"Service\",\n"
+                + "  \"spec\": {\n"
+                + "    \"ports\": [\n"
+                + "      {\n"
+                + "        \"port\": 32124,\n"
+                + "        \"targetPort\": 5701,\n"
+                + "        \"nodePort\": 31916\n"
+                + "      }\n"
+                + "    ]\n"
+                + "  },\n"
+                + "  \"status\": {\n"
+                + "    \"loadBalancer\": {\n"
+                + "      \"ingress\": [\n"
+                + "        {\n"
+                + "          \"ip\": \"35.232.226.201\"\n"
+                + "        }\n"
+                + "      ]\n"
+                + "    }\n"
+                + "  }\n"
+                + "}";
+        stub(String.format("/api/v1/namespaces/%s/services/service-1", NAMESPACE), serviceResponse2);
+
+        // when
+        List<Endpoint> result = kubernetesClient.endpoints();
+
+        // then
+        assertThat(format(result), containsInAnyOrder(ready("192.168.0.25", 5701), ready("172.17.0.5", 5702)));
+        assertThat(formatPublic(result), containsInAnyOrder(ready("35.232.226.200", 32123), ready("35.232.226.201", 32124)));
+    }
+
+    @Test
+    public void endpointsByNamespaceWithNodePublicIp() {
+        // given
+        stub(String.format("/api/v1/namespaces/%s/pods", NAMESPACE), podsListResponse());
+        stub(String.format("/api/v1/namespaces/%s/endpoints", NAMESPACE), endpointsListResponse());
+        //language=JSON
+        //language=JSON
+        String serviceResponse1 = "{\n"
+                + "  \"kind\": \"Service\",\n"
+                + "  \"spec\": {\n"
+                + "    \"ports\": [\n"
+                + "      {\n"
+                + "        \"port\": 32123,\n"
+                + "        \"targetPort\": 5701,\n"
+                + "        \"nodePort\": 31916\n"
+                + "      }\n"
+                + "    ]\n"
+                + "  }\n"
+                + "}\n";
+        stub(String.format("/api/v1/namespaces/%s/services/service-0", NAMESPACE), serviceResponse1);
+
+        //language=JSON
+        String serviceResponse2 = "{\n"
+                + "  \"kind\": \"Service\",\n"
+                + "  \"spec\": {\n"
+                + "    \"ports\": [\n"
+                + "      {\n"
+                + "        \"port\": 32124,\n"
+                + "        \"targetPort\": 5701,\n"
+                + "        \"nodePort\": 31917\n"
+                + "      }\n"
+                + "    ]\n"
+                + "  }\n"
+                + "}";
+        stub(String.format("/api/v1/namespaces/%s/services/service-1", NAMESPACE), serviceResponse2);
+
+        String nodeResponse1 = "{\n"
+                + "  \"kind\": \"Node\",\n"
+                + "  \"status\": {\n"
+                + "    \"addresses\": [\n"
+                + "      {\n"
+                + "        \"type\": \"InternalIP\",\n"
+                + "        \"address\": \"10.240.0.21\"\n"
+                + "      },\n"
+                + "      {\n"
+                + "        \"type\": \"ExternalIP\",\n"
+                + "        \"address\": \"35.232.226.200\"\n"
+                + "      }\n"
+                + "    ]\n"
+                + "  }\n"
+                + "}\n";
+        stub("/api/v1/nodes/node-name-1", nodeResponse1);
+
+        String nodeResponse2 = "{\n"
+                + "  \"kind\": \"Node\",\n"
+                + "  \"status\": {\n"
+                + "    \"addresses\": [\n"
+                + "      {\n"
+                + "        \"type\": \"InternalIP\",\n"
+                + "        \"address\": \"10.240.0.22\"\n"
+                + "      },\n"
+                + "      {\n"
+                + "        \"type\": \"ExternalIP\",\n"
+                + "        \"address\": \"35.232.226.201\"\n"
+                + "      }\n"
+                + "    ]\n"
+                + "  }\n"
+                + "}\n";
+        stub("/api/v1/nodes/node-name-2", nodeResponse2);
+
+        // when
+        List<Endpoint> result = kubernetesClient.endpoints();
+
+        // then
+        assertThat(format(result), containsInAnyOrder(ready("192.168.0.25", 5701), ready("172.17.0.5", 5702)));
+        assertThat(formatPublic(result), containsInAnyOrder(ready("35.232.226.200", 31916), ready("35.232.226.201", 31917)));
+    }
+
+    private static String podsListResponse() {
+        //language=JSON
+        return "{\n"
+                + "  \"kind\": \"PodList\",\n"
+                + "  \"items\": [\n"
+                + "    {\n"
+                + "      \"spec\": {\n"
+                + "        \"containers\": [\n"
+                + "          {\n"
+                + "            \"ports\": [\n"
+                + "              {\n"
+                + "                \"containerPort\": 5701\n"
+                + "              }\n"
+                + "            ]\n"
+                + "          }\n"
+                + "        ]\n"
+                + "      },\n"
+                + "      \"status\": {\n"
+                + "        \"podIP\": \"192.168.0.25\",\n"
+                + "        \"containerStatuses\": [\n"
+                + "          {\n"
+                + "            \"ready\": true\n"
+                + "          }\n"
+                + "        ]\n"
+                + "      }\n"
+                + "    },\n"
+                + "    {\n"
+                + "      \"spec\": {\n"
+                + "        \"containers\": [\n"
+                + "          {\n"
+                + "            \"ports\": [\n"
+                + "              {\n"
+                + "                \"containerPort\": 5702\n"
+                + "              }\n"
+                + "            ]\n"
+                + "          }\n"
+                + "        ]\n"
+                + "      },\n"
+                + "      \"status\": {\n"
+                + "        \"podIP\": \"172.17.0.5\",\n"
+                + "        \"containerStatuses\": [\n"
+                + "          {\n"
+                + "            \"ready\": true\n"
+                + "          }\n"
+                + "        ]\n"
+                + "      }\n"
+                + "    }\n"
+                + "  ]\n"
+                + "}";
+    }
+
+    private static String endpointsListResponse() {
+        //language=JSON
+        return "{\n"
+                + "  \"kind\": \"EndpointsList\",\n"
+                + "  \"items\": [\n"
+                + "    {\n"
+                + "      \"metadata\": {\n"
+                + "        \"name\": \"my-release-hazelcast\"\n"
+                + "      },\n"
+                + "      \"subsets\": [\n"
+                + "        {\n"
+                + "          \"addresses\": [\n"
+                + "            {\n"
+                + "              \"ip\": \"192.168.0.25\",\n"
+                + "              \"nodeName\": \"node-name-1\"\n"
+                + "            },\n"
+                + "            {\n"
+                + "              \"ip\": \"172.17.0.5\",\n"
+                + "              \"nodeName\": \"node-name-2\"\n"
+                + "            }\n"
+                + "          ],\n"
+                + "          \"ports\": [\n"
+                + "            {\n"
+                + "              \"port\": 5701\n"
+                + "            }\n"
+                + "          ]\n"
+                + "        }\n"
+                + "      ]\n"
+                + "    },\n"
+                + "    {\n"
+                + "      \"metadata\": {\n"
+                + "        \"name\": \"service-0\"\n"
+                + "      },\n"
+                + "      \"subsets\": [\n"
+                + "        {\n"
+                + "          \"addresses\": [\n"
+                + "            {\n"
+                + "              \"ip\": \"192.168.0.25\",\n"
+                + "              \"nodeName\": \"node-name-1\"\n"
+                + "            }\n"
+                + "          ],\n"
+                + "          \"ports\": [\n"
+                + "            {\n"
+                + "              \"port\": 5701\n"
+                + "            }\n"
+                + "          ]\n"
+                + "        }\n"
+                + "      ]\n"
+                + "    },\n"
+                + "    {\n"
+                + "      \"metadata\": {\n"
+                + "        \"name\": \"service-1\"\n"
+                + "      },\n"
+                + "      \"subsets\": [\n"
+                + "        {\n"
+                + "          \"addresses\": [\n"
+                + "            {\n"
+                + "              \"ip\": \"172.17.0.5\",\n"
+                + "              \"nodeName\": \"node-name-2\"\n"
+                + "            }\n"
+                + "          ],\n"
+                + "          \"ports\": [\n"
+                + "            {\n"
+                + "              \"port\": 5702\n"
+                + "            }\n"
+                + "          ]\n"
+                + "        }\n"
+                + "      ]\n"
+                + "    }\n"
+                + "  ]\n"
+                + "}";
+    }
+
+    @Test
+    public void forbidden() {
+        // given
+        String forbiddenBody = "\"reason\":\"Forbidden\"";
+        stubFor(get(urlEqualTo(String.format("/api/v1/namespaces/%s/pods", NAMESPACE)))
+                .withHeader("Authorization", equalTo(String.format("Bearer %s", TOKEN)))
+                .willReturn(aResponse().withStatus(403).withBody(forbiddenBody)));
+
+        // when
+        List<Endpoint> result = kubernetesClient.endpoints();
+
+        // then
+        assertEquals(emptyList(), result);
+    }
+
+    @Test
+    public void wrongApiToken() {
+        // given
+        String unauthorizedBody = "\"reason\":\"Unauthorized\"";
+        stubFor(get(urlEqualTo(String.format("/api/v1/namespaces/%s/pods", NAMESPACE)))
+                .withHeader("Authorization", equalTo(String.format("Bearer %s", TOKEN)))
+                .willReturn(aResponse().withStatus(401).withBody(unauthorizedBody)));
+
+        // when
+        List<Endpoint> result = kubernetesClient.endpoints();
+
+        // then
+        assertEquals(emptyList(), result);
+    }
+
+    @Test(expected = RestClientException.class)
+    public void unknownException() {
+        // given
+        String notRetriedErrorBody = "\"reason\":\"Forbidden\"";
+        stubFor(get(urlEqualTo(String.format("/api/v1/namespaces/%s/pods", NAMESPACE)))
+                .withHeader("Authorization", equalTo(String.format("Bearer %s", TOKEN)))
+                .willReturn(aResponse().withStatus(501).withBody(notRetriedErrorBody)));
+
+        // when
+        kubernetesClient.endpoints();
+    }
+
+    private static List<String> format(List<Endpoint> addresses) {
+        List<String> result = new ArrayList<String>();
+        for (Endpoint address : addresses) {
+            String ip = address.getPrivateAddress().getIp();
+            Integer port = address.getPrivateAddress().getPort();
+            boolean isReady = address.isReady();
+            result.add(toString(ip, port, isReady));
+        }
+        return result;
+    }
+
+    private static List<String> formatPublic(List<Endpoint> addresses) {
+        List<String> result = new ArrayList<String>();
+        for (Endpoint address : addresses) {
+            String ip = address.getPublicAddress().getIp();
+            Integer port = address.getPublicAddress().getPort();
+            boolean isReady = address.isReady();
+            result.add(toString(ip, port, isReady));
+        }
+        return result;
+    }
+
+    private static void stub(String url, String response) {
+        stubFor(get(urlEqualTo(url))
+                .withHeader("Authorization", equalTo(String.format("Bearer %s", TOKEN)))
+                .willReturn(aResponse().withStatus(200).withBody(response)));
+    }
+
+    private static void stub(String url, Map<String, String> queryParams, String response) {
+        MappingBuilder mappingBuilder = get(urlPathMatching(url));
+        for (String key : queryParams.keySet()) {
+            mappingBuilder = mappingBuilder.withQueryParam(key, equalTo(queryParams.get(key)));
+        }
+        stubFor(mappingBuilder
+                .withHeader("Authorization", equalTo(String.format("Bearer %s", TOKEN)))
+                .willReturn(aResponse().withStatus(200).withBody(response)));
+    }
+
+    private static String ready(String ip, Integer port) {
+        return toString(ip, port, true);
+    }
+
+    private static String notReady(String ip, Integer port) {
+        return toString(ip, port, false);
+    }
+
+    private static String toString(String ip, Integer port, boolean isReady) {
+        return String.format("%s:%s:%s", ip, port, isReady);
+    }
+}
diff --git a/src/test/java/com/hazelcast/kubernetes/RestClientTest.java b/src/test/java/com/hazelcast/kubernetes/RestClientTest.java
new file mode 100644
index 0000000..fe2e7eb
--- /dev/null
+++ b/src/test/java/com/hazelcast/kubernetes/RestClientTest.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (c) 2008-2018, Hazelcast, Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.hazelcast.kubernetes;
+
+/*
+ * Copyright (c) 2008-2018, Hazelcast, Inc. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import com.github.tomakehurst.wiremock.junit.WireMockRule;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
+import static com.github.tomakehurst.wiremock.client.WireMock.equalTo;
+import static com.github.tomakehurst.wiremock.client.WireMock.get;
+import static com.github.tomakehurst.wiremock.client.WireMock.post;
+import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
+import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
+import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
+import static org.junit.Assert.assertEquals;
+
+public class RestClientTest {
+    private static final String API_ENDPOINT = "/some/endpoint";
+    private static final String BODY_REQUEST = "some body request";
+    private static final String BODY_RESPONSE = "some body response";
+
+    @Rule
+    public WireMockRule wireMockRule = new WireMockRule(wireMockConfig().dynamicPort());
+
+    private String address;
+
+    @Before
+    public void setUp() {
+        address = String.format("http://localhost:%s", wireMockRule.port());
+    }
+
+    @Test
+    public void getSuccess() {
+        // given
+        stubFor(get(urlEqualTo(API_ENDPOINT))
+                .willReturn(aResponse().withStatus(200).withBody(BODY_RESPONSE)));
+
+        // when
+        String result = RestClient.create(String.format("%s%s", address, API_ENDPOINT)).get();
+
+        // then
+        assertEquals(BODY_RESPONSE, result);
+    }
+
+    @Test
+    public void getWithHeaderSuccess() {
+        // given
+        String headerKey = "Metadata-Flavor";
+        String headerValue = "Google";
+        stubFor(get(urlEqualTo(API_ENDPOINT))
+                .withHeader(headerKey, equalTo(headerValue))
+                .willReturn(aResponse().withStatus(200).withBody(BODY_RESPONSE)));
+
+        // when
+        String result = RestClient.create(String.format("%s%s", address, API_ENDPOINT))
+                                  .withHeader(headerKey, headerValue)
+                                  .get();
+
+        // then
+        assertEquals(BODY_RESPONSE, result);
+    }
+
+    @Test(expected = RestClientException.class)
+    public void getFailure() {
+        // given
+        stubFor(get(urlEqualTo(API_ENDPOINT))
+                .willReturn(aResponse().withStatus(500).withBody("Internal error")));
+
+        // when
+        RestClient.create(String.format("%s%s", address, API_ENDPOINT)).get();
+
+        // then
+        // throw exception
+    }
+
+    @Test
+    public void postSuccess() {
+        // given
+        stubFor(post(urlEqualTo(API_ENDPOINT))
+                .withRequestBody(equalTo(BODY_REQUEST))
+                .willReturn(aResponse().withStatus(200).withBody(BODY_RESPONSE)));
+
+        // when
+        String result = RestClient.create(String.format("%s%s", address, API_ENDPOINT))
+                                  .withBody(BODY_REQUEST)
+                                  .post();
+
+        // then
+        assertEquals(BODY_RESPONSE, result);
+    }
+}
diff --git a/src/test/java/com/hazelcast/kubernetes/RetryKubernetesClientTest.java b/src/test/java/com/hazelcast/kubernetes/RetryKubernetesClientTest.java
deleted file mode 100644
index ee24451..0000000
--- a/src/test/java/com/hazelcast/kubernetes/RetryKubernetesClientTest.java
+++ /dev/null
@@ -1,161 +0,0 @@
-/*
- * Copyright (c) 2008-2018, Hazelcast, Inc. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.hazelcast.kubernetes;
-
-import com.hazelcast.kubernetes.KubernetesClient.Endpoints;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.runners.MockitoJUnitRunner;
-
-import static org.junit.Assert.assertEquals;
-import static org.mockito.BDDMockito.given;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Mockito.when;
-
-@RunWith(MockitoJUnitRunner.class)
-public class RetryKubernetesClientTest {
-    private static final int RETRIES = 1;
-
-    private static final String NAMESPACE = "someNamespace";
-    private static final String SERVICE_LABEL = "someServiceLabel";
-    private static final String SERVICE_LABEL_VALUE = "someServiceLabelValue";
-    private static final String SERVICE_NAME = "someServiceName";
-    private static final String POD_NAME = "hazelcast-0";
-    private static final String ZONE = "us-central1-a";
-
-    private static final Endpoints ENDPOINTS = new Endpoints(null, null);
-
-    @Mock
-    KubernetesClient mockClient;
-
-    private RetryKubernetesClient client;
-
-    @Before
-    public void setUp() {
-        client = new RetryKubernetesClient(mockClient, RETRIES);
-
-        when(mockClient.endpoints(anyString())).thenReturn(ENDPOINTS);
-        when(mockClient.endpointsByLabel(anyString(), anyString(), anyString())).thenReturn(ENDPOINTS);
-        when(mockClient.endpointsByName(anyString(), anyString())).thenReturn(ENDPOINTS);
-    }
-
-    @Test
-    public void endpoints() {
-        // given
-        given(mockClient.endpoints(NAMESPACE)).willThrow(KubernetesClientException.class).willReturn(ENDPOINTS);
-
-        // when
-        Endpoints result = client.endpoints(NAMESPACE);
-
-        // then
-        assertEquals(ENDPOINTS, result);
-    }
-
-    @Test(expected = KubernetesClientException.class)
-    public void endpointsRetriesExceeded() {
-        // given
-        given(mockClient.endpoints(NAMESPACE)).willThrow(KubernetesClientException.class)
-                                              .willThrow(KubernetesClientException.class).willReturn(ENDPOINTS);
-
-        // when
-        client.endpoints(NAMESPACE);
-
-        // then
-        // throws exception
-    }
-
-    @Test
-    public void endpointsByLabel() {
-        // given
-        given(mockClient.endpointsByLabel(NAMESPACE, SERVICE_LABEL, SERVICE_LABEL_VALUE))
-                .willThrow(KubernetesClientException.class).willReturn(ENDPOINTS);
-
-        // when
-        Endpoints result = client.endpointsByLabel(NAMESPACE, SERVICE_LABEL, SERVICE_LABEL_VALUE);
-
-        // then
-        assertEquals(ENDPOINTS, result);
-    }
-
-    @Test(expected = KubernetesClientException.class)
-    public void endpointsByLabelRetriesExceeded() {
-        // given
-        given(mockClient.endpointsByLabel(NAMESPACE, SERVICE_LABEL, SERVICE_LABEL_VALUE))
-                .willThrow(KubernetesClientException.class)
-                .willThrow(KubernetesClientException.class).willReturn(ENDPOINTS);
-
-        // when
-        client.endpointsByLabel(NAMESPACE, SERVICE_LABEL, SERVICE_LABEL_VALUE);
-
-        // then
-        // throws exception
-    }
-
-    @Test
-    public void endpointsByName() {
-        // given
-        given(mockClient.endpointsByName(NAMESPACE, SERVICE_NAME))
-                .willThrow(KubernetesClientException.class).willReturn(ENDPOINTS);
-
-        // when
-        Endpoints result = client.endpointsByName(NAMESPACE, SERVICE_NAME);
-
-        // then
-        assertEquals(ENDPOINTS, result);
-    }
-
-    @Test(expected = KubernetesClientException.class)
-    public void endpointsByNameRetriesExceeded() {
-        // given
-        given(mockClient.endpointsByName(NAMESPACE, SERVICE_NAME))
-                .willThrow(KubernetesClientException.class)
-                .willThrow(KubernetesClientException.class).willReturn(ENDPOINTS);
-
-        // when
-        client.endpointsByName(NAMESPACE, SERVICE_NAME);
-
-        // then
-        // throws exception
-    }
-
-    @Test
-    public void zone() {
-        // given
-        given(mockClient.zone(NAMESPACE, POD_NAME)).willThrow(KubernetesClientException.class).willReturn(ZONE);
-
-        // when
-        String result = client.zone(NAMESPACE, POD_NAME);
-
-        // then
-        assertEquals(ZONE, result);
-    }
-
-    @Test(expected = KubernetesClientException.class)
-    public void zoneRetriesExceeded() {
-        // given
-        given(mockClient.zone(NAMESPACE, POD_NAME)).willThrow(KubernetesClientException.class)
-                                                   .willThrow(KubernetesClientException.class).willReturn(ZONE);
-
-        // when
-        client.zone(NAMESPACE, POD_NAME);
-
-        // then
-        // throws exception
-    }
-}
\ No newline at end of file