Repository: hazelcast/hazelcast-kubernetes (java)

Issue:
Validate properties
The input properties should be validated:
- `service-dns-timeout`: cannot be negative
- `kubernetes-api-retries`: cannot be negative
- If DNS Lookup parameters are specified, the Kubernetes API cannot be specified and vice versa
- ...

It may also be possible to refactor parameters validation and parsing to a separate class.

Pull request:
Add validation for input properties, refactor, add unit tests
Changes:
- Refactor handling input properties to a separate class `KubernetesConfig`
- Add validation for the input properties (i.e. non-negative values, enabled either Kubernetes API or DNS Lookup)
- Add Unit tests

fix #135 

Patch:
diff --git a/src/main/java/com/hazelcast/kubernetes/HazelcastKubernetesDiscoveryStrategy.java b/src/main/java/com/hazelcast/kubernetes/HazelcastKubernetesDiscoveryStrategy.java
index d9848e2..77e70b2 100644
--- a/src/main/java/com/hazelcast/kubernetes/HazelcastKubernetesDiscoveryStrategy.java
+++ b/src/main/java/com/hazelcast/kubernetes/HazelcastKubernetesDiscoveryStrategy.java
@@ -16,48 +16,20 @@
 
 package com.hazelcast.kubernetes;
 
-import com.hazelcast.config.properties.PropertyDefinition;
+import com.hazelcast.kubernetes.KubernetesConfig.DiscoveryMode;
 import com.hazelcast.logging.ILogger;
-import com.hazelcast.nio.IOUtil;
 import com.hazelcast.spi.discovery.AbstractDiscoveryStrategy;
 import com.hazelcast.spi.discovery.DiscoveryNode;
 import com.hazelcast.spi.partitiongroup.PartitionGroupMetaData;
-import com.hazelcast.util.StringUtil;
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
 import java.net.InetAddress;
 import java.net.UnknownHostException;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-import static com.hazelcast.kubernetes.KubernetesProperties.KUBERNETES_API_RETIRES;
-import static com.hazelcast.kubernetes.KubernetesProperties.KUBERNETES_API_TOKEN;
-import static com.hazelcast.kubernetes.KubernetesProperties.KUBERNETES_CA_CERTIFICATE;
-import static com.hazelcast.kubernetes.KubernetesProperties.KUBERNETES_MASTER_URL;
-import static com.hazelcast.kubernetes.KubernetesProperties.KUBERNETES_SYSTEM_PREFIX;
-import static com.hazelcast.kubernetes.KubernetesProperties.NAMESPACE;
-import static com.hazelcast.kubernetes.KubernetesProperties.RESOLVE_NOT_READY_ADDRESSES;
-import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_DNS;
-import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_DNS_TIMEOUT;
-import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_LABEL_NAME;
-import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_LABEL_VALUE;
-import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_NAME;
-import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_PORT;
-
 final class HazelcastKubernetesDiscoveryStrategy
         extends AbstractDiscoveryStrategy {
-
-    private static final String DEFAULT_MASTER_URL = "https://kubernetes.default.svc";
-    private static final int DEFAULT_SERVICE_DNS_TIMEOUT_SECONDS = 5;
-    private static final int DEFAULT_KUBERNETES_API_RETRIES = 3;
-
-    private final String namespace;
-
     private final KubernetesClient client;
     private final EndpointResolver endpointResolver;
 
@@ -66,68 +38,25 @@ final class HazelcastKubernetesDiscoveryStrategy
     HazelcastKubernetesDiscoveryStrategy(ILogger logger, Map<String, Comparable> properties) {
         super(logger, properties);
 
-        String serviceDns = getOrNull(properties, KUBERNETES_SYSTEM_PREFIX, SERVICE_DNS);
-        int serviceDnsTimeout
-                = getOrDefault(properties, KUBERNETES_SYSTEM_PREFIX, SERVICE_DNS_TIMEOUT, DEFAULT_SERVICE_DNS_TIMEOUT_SECONDS);
-        String serviceName = getOrNull(properties, KUBERNETES_SYSTEM_PREFIX, SERVICE_NAME);
-        int port = getOrDefault(properties, KUBERNETES_SYSTEM_PREFIX, SERVICE_PORT, 0);
-        String serviceLabel = getOrNull(properties, KUBERNETES_SYSTEM_PREFIX, SERVICE_LABEL_NAME);
-        String serviceLabelValue = getOrDefault(properties, KUBERNETES_SYSTEM_PREFIX, SERVICE_LABEL_VALUE, "true");
-        namespace = getOrDefault(properties, KUBERNETES_SYSTEM_PREFIX, NAMESPACE, getNamespaceOrDefault());
-        Boolean resolveNotReadyAddresses = getOrDefault(properties, KUBERNETES_SYSTEM_PREFIX, RESOLVE_NOT_READY_ADDRESSES, false);
-        int kubernetesApiRetries
-                = getOrDefault(properties, KUBERNETES_SYSTEM_PREFIX, KUBERNETES_API_RETIRES, DEFAULT_KUBERNETES_API_RETRIES);
-        String kubernetesMaster = getOrDefault(properties, KUBERNETES_SYSTEM_PREFIX, KUBERNETES_MASTER_URL, DEFAULT_MASTER_URL);
-        String apiToken = getApiToken(properties);
-        String caCertificate = caCertificate(properties);
+        KubernetesConfig config = new KubernetesConfig(properties);
+        logger.info(config.toString());
 
-        logger.info("Kubernetes Discovery properties: { "
-                + "service-dns: " + serviceDns + ", "
-                + "service-dns-timeout: " + serviceDnsTimeout + ", "
-                + "service-name: " + serviceName + ", "
-                + "service-port: " + port + ", "
-                + "service-label: " + serviceLabel + ", "
-                + "service-label-value: " + serviceLabelValue + ", "
-                + "namespace: " + namespace + ", "
-                + "resolve-not-ready-addresses: " + resolveNotReadyAddresses + ", "
-                + "kubernetes-api-retries: " + kubernetesApiRetries + ", "
-                + "kubernetes-master: " + kubernetesMaster + "}");
+        client = buildKubernetesClient(config);
 
-        client = buildKubernetesClient(namespace, kubernetesMaster, apiToken, caCertificate, kubernetesApiRetries);
-        if (serviceDns != null) {
-            endpointResolver = new DnsEndpointResolver(logger, serviceDns, port, serviceDnsTimeout);
+        if (DiscoveryMode.DNS_LOOKUP.equals(config.getMode())) {
+            endpointResolver = new DnsEndpointResolver(logger, config.getServiceDns(), config.getServicePort(),
+                    config.getServiceDnsTimeout());
         } else {
-            endpointResolver = new KubernetesApiEndpointResolver(logger, serviceName, port, serviceLabel, serviceLabelValue,
-                    resolveNotReadyAddresses, client);
+            endpointResolver = new KubernetesApiEndpointResolver(logger, config.getServiceName(), config.getServicePort(),
+                    config.getServiceLabelName(), config.getServiceLabelValue(), config.isResolveNotReadyAddresses(), client);
         }
-        logger.info("Kubernetes Discovery activated resolver: " + endpointResolver.getClass().getSimpleName());
-    }
 
-    private String getApiToken(Map<String, Comparable> properties) {
-        String apiToken = getOrNull(properties, KUBERNETES_SYSTEM_PREFIX, KUBERNETES_API_TOKEN);
-        if (apiToken == null) {
-            apiToken = readAccountToken();
-        }
-        return apiToken;
+        logger.info("Kubernetes Discovery activated with mode: " + config.getMode().name());
     }
 
-    private String caCertificate(Map<String, Comparable> properties) {
-        String caCertificate = getOrNull(properties, KUBERNETES_SYSTEM_PREFIX, KUBERNETES_CA_CERTIFICATE);
-        if (caCertificate == null) {
-            caCertificate = readCaCertificate();
-        }
-        return caCertificate;
-    }
-
-    private String getNamespaceOrDefault() {
-        String namespace = System.getenv("KUBERNETES_NAMESPACE");
-        if (namespace == null) {
-            namespace = System.getenv("OPENSHIFT_BUILD_NAMESPACE");
-            if (namespace == null) {
-                namespace = "default";
-            }
-        }
-        return namespace;
+    private static KubernetesClient buildKubernetesClient(KubernetesConfig config) {
+        return new KubernetesClient(config.getNamespace(), config.getKubernetesMasterUrl(), config.getKubernetesApiToken(),
+                config.getKubernetesCaCertificate(), config.getKubernetesApiRetries());
     }
 
     public void start() {
@@ -172,60 +101,6 @@ final class HazelcastKubernetesDiscoveryStrategy
         endpointResolver.destroy();
     }
 
-    protected <T extends Comparable> T getOrNull(Map<String, Comparable> properties, String prefix, PropertyDefinition property) {
-        return getOrDefault(properties, prefix, property, null);
-    }
-
-    protected <T extends Comparable> T getOrDefault(Map<String, Comparable> properties, String prefix,
-                                                    PropertyDefinition property, T defaultValue) {
-        if (property == null) {
-            return defaultValue;
-        }
-
-        Comparable value = readProperty(prefix, property);
-        if (value == null) {
-            value = properties.get(property.key());
-        }
-
-        if (value == null) {
-            return defaultValue;
-        }
-
-        return (T) value;
-    }
-
-    private Comparable readProperty(String prefix, PropertyDefinition property) {
-        if (prefix != null) {
-            String p = getProperty(prefix, property);
-            String v = System.getProperty(p);
-            if (StringUtil.isNullOrEmpty(v)) {
-                v = System.getenv(p);
-                if (StringUtil.isNullOrEmpty(v)) {
-                    v = System.getenv(cIdentifierLike(p));
-                }
-            }
-
-            if (!StringUtil.isNullOrEmpty(v)) {
-                return property.typeConverter().convert(v);
-            }
-        }
-        return null;
-    }
-
-    private String cIdentifierLike(String property) {
-        property = property.toUpperCase();
-        property = property.replace(".", "_");
-        return property.replace("-", "_");
-    }
-
-    private String getProperty(String prefix, PropertyDefinition property) {
-        StringBuilder sb = new StringBuilder(prefix);
-        if (prefix.charAt(prefix.length() - 1) != '.') {
-            sb.append('.');
-        }
-        return sb.append(property.key()).toString();
-    }
-
     abstract static class EndpointResolver {
         protected final ILogger logger;
 
@@ -253,34 +128,4 @@ final class HazelcastKubernetesDiscoveryStrategy
             return null;
         }
     }
-
-    private KubernetesClient buildKubernetesClient(String namespace, String kubernetesMaster, String accessToken,
-                                                   String caCertificate, int retries) {
-        return new KubernetesClient(namespace, kubernetesMaster, accessToken, caCertificate, retries);
-    }
-
-    @SuppressFBWarnings("DMI_HARDCODED_ABSOLUTE_FILENAME")
-    private static String readAccountToken() {
-        return readFileContents("/var/run/secrets/kubernetes.io/serviceaccount/token");
-    }
-
-    @SuppressFBWarnings("DMI_HARDCODED_ABSOLUTE_FILENAME")
-    private static String readCaCertificate() {
-        return readFileContents("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt");
-    }
-
-    protected static String readFileContents(String tokenFile) {
-        InputStream is = null;
-        try {
-            File file = new File(tokenFile);
-            byte[] data = new byte[(int) file.length()];
-            is = new FileInputStream(file);
-            is.read(data);
-            return new String(data, "UTF-8");
-        } catch (IOException e) {
-            throw new RuntimeException("Could not get token file", e);
-        } finally {
-            IOUtil.closeResource(is);
-        }
-    }
 }
diff --git a/src/main/java/com/hazelcast/kubernetes/KubernetesConfig.java b/src/main/java/com/hazelcast/kubernetes/KubernetesConfig.java
new file mode 100644
index 0000000..874bfc3
--- /dev/null
+++ b/src/main/java/com/hazelcast/kubernetes/KubernetesConfig.java
@@ -0,0 +1,297 @@
+/*
+ * 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.config.InvalidConfigurationException;
+import com.hazelcast.config.properties.PropertyDefinition;
+import com.hazelcast.nio.IOUtil;
+import com.hazelcast.util.StringUtil;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+
+import static com.hazelcast.kubernetes.KubernetesProperties.KUBERNETES_API_RETIRES;
+import static com.hazelcast.kubernetes.KubernetesProperties.KUBERNETES_API_TOKEN;
+import static com.hazelcast.kubernetes.KubernetesProperties.KUBERNETES_CA_CERTIFICATE;
+import static com.hazelcast.kubernetes.KubernetesProperties.KUBERNETES_MASTER_URL;
+import static com.hazelcast.kubernetes.KubernetesProperties.KUBERNETES_SYSTEM_PREFIX;
+import static com.hazelcast.kubernetes.KubernetesProperties.NAMESPACE;
+import static com.hazelcast.kubernetes.KubernetesProperties.RESOLVE_NOT_READY_ADDRESSES;
+import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_DNS;
+import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_DNS_TIMEOUT;
+import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_LABEL_NAME;
+import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_LABEL_VALUE;
+import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_NAME;
+import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_PORT;
+
+/**
+ * Responsible for fetching, parsing, and validating Hazelcast Kubernetes Discovery Strategy input properties.
+ */
+final class KubernetesConfig {
+    private static final String DEFAULT_MASTER_URL = "https://kubernetes.default.svc";
+    private static final int DEFAULT_SERVICE_DNS_TIMEOUT_SECONDS = 5;
+    private static final int DEFAULT_KUBERNETES_API_RETRIES = 3;
+
+    // Parameters for DNS Lookup mode
+    private final String serviceDns;
+    private final int serviceDnsTimeout;
+
+    // Parameters for Kubernetes API mode
+    private final String serviceName;
+    private final String serviceLabelName;
+    private final String serviceLabelValue;
+    private final String namespace;
+    private final boolean resolveNotReadyAddresses;
+    private final int kubernetesApiRetries;
+    private final String kubernetesMasterUrl;
+    private final String kubernetesApiToken;
+    private final String kubernetesCaCertificate;
+
+    // Parameters for both DNS Lookup and Kubernetes API modes
+    private final int servicePort;
+
+    KubernetesConfig(Map<String, Comparable> properties) {
+        this.serviceDns = getOrNull(properties, KUBERNETES_SYSTEM_PREFIX, SERVICE_DNS);
+        this.serviceDnsTimeout
+                = getOrDefault(properties, KUBERNETES_SYSTEM_PREFIX, SERVICE_DNS_TIMEOUT, DEFAULT_SERVICE_DNS_TIMEOUT_SECONDS);
+        this.serviceName = getOrNull(properties, KUBERNETES_SYSTEM_PREFIX, SERVICE_NAME);
+        this.serviceLabelName = getOrNull(properties, KUBERNETES_SYSTEM_PREFIX, SERVICE_LABEL_NAME);
+        this.serviceLabelValue = getOrDefault(properties, KUBERNETES_SYSTEM_PREFIX, SERVICE_LABEL_VALUE, "true");
+        this.namespace = getOrDefault(properties, KUBERNETES_SYSTEM_PREFIX, NAMESPACE, getNamespaceOrDefault());
+        this.resolveNotReadyAddresses = getOrDefault(properties, KUBERNETES_SYSTEM_PREFIX, RESOLVE_NOT_READY_ADDRESSES, false);
+        this.kubernetesApiRetries
+                = getOrDefault(properties, KUBERNETES_SYSTEM_PREFIX, KUBERNETES_API_RETIRES, DEFAULT_KUBERNETES_API_RETRIES);
+        this.kubernetesMasterUrl = getOrDefault(properties, KUBERNETES_SYSTEM_PREFIX, KUBERNETES_MASTER_URL, DEFAULT_MASTER_URL);
+        this.kubernetesApiToken = getApiToken(properties);
+        this.kubernetesCaCertificate = caCertificate(properties);
+        this.servicePort = getOrDefault(properties, KUBERNETES_SYSTEM_PREFIX, SERVICE_PORT, 0);
+
+        validateConfig();
+    }
+
+    private String getNamespaceOrDefault() {
+        String namespace = System.getenv("KUBERNETES_NAMESPACE");
+        if (namespace == null) {
+            namespace = System.getenv("OPENSHIFT_BUILD_NAMESPACE");
+            if (namespace == null) {
+                namespace = "default";
+            }
+        }
+        return namespace;
+    }
+
+    private String getApiToken(Map<String, Comparable> properties) {
+        String apiToken = getOrNull(properties, KUBERNETES_SYSTEM_PREFIX, KUBERNETES_API_TOKEN);
+        if (apiToken == null) {
+            apiToken = readAccountToken();
+        }
+        return apiToken;
+    }
+
+    private String caCertificate(Map<String, Comparable> properties) {
+        String caCertificate = getOrNull(properties, KUBERNETES_SYSTEM_PREFIX, KUBERNETES_CA_CERTIFICATE);
+        if (caCertificate == null) {
+            caCertificate = readCaCertificate();
+        }
+        return caCertificate;
+    }
+
+    @SuppressFBWarnings("DMI_HARDCODED_ABSOLUTE_FILENAME")
+    private static String readAccountToken() {
+        return readFileContents("/var/run/secrets/kubernetes.io/serviceaccount/token");
+    }
+
+    @SuppressFBWarnings("DMI_HARDCODED_ABSOLUTE_FILENAME")
+    private static String readCaCertificate() {
+        return readFileContents("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt");
+    }
+
+    static String readFileContents(String tokenFile) {
+        InputStream is = null;
+        try {
+            File file = new File(tokenFile);
+            byte[] data = new byte[(int) file.length()];
+            is = new FileInputStream(file);
+            is.read(data);
+            return new String(data, "UTF-8");
+        } catch (IOException e) {
+            throw new RuntimeException("Could not get token file", e);
+        } finally {
+            IOUtil.closeResource(is);
+        }
+    }
+
+    private <T extends Comparable> T getOrNull(Map<String, Comparable> properties, String prefix, PropertyDefinition property) {
+        return getOrDefault(properties, prefix, property, null);
+    }
+
+    private <T extends Comparable> T getOrDefault(Map<String, Comparable> properties, String prefix,
+                                                  PropertyDefinition property, T defaultValue) {
+        if (property == null) {
+            return defaultValue;
+        }
+
+        Comparable value = readProperty(prefix, property);
+        if (value == null) {
+            value = properties.get(property.key());
+        }
+
+        if (value == null) {
+            return defaultValue;
+        }
+
+        return (T) value;
+    }
+
+    private Comparable readProperty(String prefix, PropertyDefinition property) {
+        if (prefix != null) {
+            String p = getProperty(prefix, property);
+            String v = System.getProperty(p);
+            if (StringUtil.isNullOrEmpty(v)) {
+                v = System.getenv(p);
+                if (StringUtil.isNullOrEmpty(v)) {
+                    v = System.getenv(cIdentifierLike(p));
+                }
+            }
+
+            if (!StringUtil.isNullOrEmpty(v)) {
+                return property.typeConverter().convert(v);
+            }
+        }
+        return null;
+    }
+
+    private String cIdentifierLike(String property) {
+        property = property.toUpperCase();
+        property = property.replace(".", "_");
+        return property.replace("-", "_");
+    }
+
+    private String getProperty(String prefix, PropertyDefinition property) {
+        StringBuilder sb = new StringBuilder(prefix);
+        if (prefix.charAt(prefix.length() - 1) != '.') {
+            sb.append('.');
+        }
+        return sb.append(property.key()).toString();
+    }
+
+    private void validateConfig() {
+        if (serviceDns != null && (serviceName != null || serviceLabelName != null)) {
+            throw new InvalidConfigurationException(
+                    String.format("Properties '%s' and ('%s' or '%s') cannot be defined at the same time",
+                            SERVICE_DNS.key(), SERVICE_NAME.key(), SERVICE_LABEL_NAME.key()));
+        }
+        if (serviceName != null && serviceLabelName != null) {
+            throw new InvalidConfigurationException(
+                    String.format("Properties '%s' and '%s' cannot be defined at the same time",
+                            SERVICE_NAME.key(), SERVICE_LABEL_NAME.key()));
+
+        }
+        if (serviceDnsTimeout < 0) {
+            throw new InvalidConfigurationException(
+                    String.format("Property '%s' cannot be a negative number", SERVICE_DNS_TIMEOUT.key()));
+        }
+        if (kubernetesApiRetries < 0) {
+            throw new InvalidConfigurationException(
+                    String.format("Property '%s' cannot be a negative number", KUBERNETES_API_RETIRES.key()));
+        }
+        if (servicePort < 0) {
+            throw new InvalidConfigurationException(
+                    String.format("Property '%s' cannot be a negative number", SERVICE_PORT.key()));
+        }
+    }
+
+    DiscoveryMode getMode() {
+        if (serviceDns != null) {
+            return DiscoveryMode.DNS_LOOKUP;
+        } else {
+            return DiscoveryMode.KUBERNETES_API;
+        }
+    }
+
+    String getServiceDns() {
+        return serviceDns;
+    }
+
+    int getServiceDnsTimeout() {
+        return serviceDnsTimeout;
+    }
+
+    String getServiceName() {
+        return serviceName;
+    }
+
+    String getServiceLabelName() {
+        return serviceLabelName;
+    }
+
+    String getServiceLabelValue() {
+        return serviceLabelValue;
+    }
+
+    String getNamespace() {
+        return namespace;
+    }
+
+    boolean isResolveNotReadyAddresses() {
+        return resolveNotReadyAddresses;
+    }
+
+    int getKubernetesApiRetries() {
+        return kubernetesApiRetries;
+    }
+
+    String getKubernetesMasterUrl() {
+        return kubernetesMasterUrl;
+    }
+
+    String getKubernetesApiToken() {
+        return kubernetesApiToken;
+    }
+
+    String getKubernetesCaCertificate() {
+        return kubernetesCaCertificate;
+    }
+
+    int getServicePort() {
+        return servicePort;
+    }
+
+    @Override
+    public String toString() {
+        return "Kubernetes Discovery properties: { "
+                + "service-dns: " + serviceDns + ", "
+                + "service-dns-timeout: " + serviceDnsTimeout + ", "
+                + "service-name: " + serviceName + ", "
+                + "service-port: " + servicePort + ", "
+                + "service-label: " + serviceLabelName + ", "
+                + "service-label-value: " + serviceLabelValue + ", "
+                + "namespace: " + namespace + ", "
+                + "resolve-not-ready-addresses: " + resolveNotReadyAddresses + ", "
+                + "kubernetes-api-retries: " + kubernetesApiRetries + ", "
+                + "kubernetes-master: " + kubernetesMasterUrl + "}";
+    }
+
+    enum DiscoveryMode {
+        DNS_LOOKUP,
+        KUBERNETES_API
+    }
+}


Test patch:
diff --git a/src/test/java/com/hazelcast/kubernetes/HazelcastKubernetesDiscoveryStrategyTest.java b/src/test/java/com/hazelcast/kubernetes/HazelcastKubernetesDiscoveryStrategyTest.java
deleted file mode 100644
index bd8e656..0000000
--- a/src/test/java/com/hazelcast/kubernetes/HazelcastKubernetesDiscoveryStrategyTest.java
+++ /dev/null
@@ -1,54 +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.nio.IOUtil;
-import org.junit.Assert;
-import org.junit.Test;
-
-import java.io.BufferedWriter;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.nio.charset.Charset;
-
-public class HazelcastKubernetesDiscoveryStrategyTest {
-
-    @Test
-    public void testReadFileContents()
-            throws IOException {
-        String expectedContents = "Hello, world!\nThis is a test with Unicode ✓.";
-        String testFile = createTestFile(expectedContents);
-        String actualContents = HazelcastKubernetesDiscoveryStrategy.readFileContents(testFile);
-        Assert.assertEquals(expectedContents, actualContents);
-    }
-
-    private static String createTestFile(String expectedContents)
-            throws IOException {
-        File temp = File.createTempFile("test", ".tmp");
-        temp.deleteOnExit();
-        BufferedWriter bufferedWriter = null;
-        try {
-            bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(temp), Charset.forName("UTF-8")));
-            bufferedWriter.write(expectedContents);
-        } finally {
-            IOUtil.closeResource(bufferedWriter);
-        }
-        return temp.getAbsolutePath();
-    }
-}
diff --git a/src/test/java/com/hazelcast/kubernetes/KubernetesConfigTest.java b/src/test/java/com/hazelcast/kubernetes/KubernetesConfigTest.java
new file mode 100644
index 0000000..2875aee
--- /dev/null
+++ b/src/test/java/com/hazelcast/kubernetes/KubernetesConfigTest.java
@@ -0,0 +1,233 @@
+/*
+ * 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.config.InvalidConfigurationException;
+import com.hazelcast.nio.IOUtil;
+import org.junit.Test;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.hazelcast.kubernetes.KubernetesConfig.DiscoveryMode;
+import static com.hazelcast.kubernetes.KubernetesProperties.KUBERNETES_API_RETIRES;
+import static com.hazelcast.kubernetes.KubernetesProperties.KUBERNETES_API_TOKEN;
+import static com.hazelcast.kubernetes.KubernetesProperties.KUBERNETES_CA_CERTIFICATE;
+import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_DNS;
+import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_DNS_TIMEOUT;
+import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_LABEL_NAME;
+import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_LABEL_VALUE;
+import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_NAME;
+import static com.hazelcast.kubernetes.KubernetesProperties.SERVICE_PORT;
+import static org.junit.Assert.assertEquals;
+
+public class KubernetesConfigTest {
+    private static final String TEST_API_TOKEN = "api-token";
+    private static final String TEST_CA_CERTIFICATE = "ca-certificate";
+
+    @Test
+    public void dnsLookupMode() {
+        // given
+        String serviceDns = "hazelcast.default.svc.cluster.local";
+        int serviceDnsTimeout = 10;
+        int servicePort = 5703;
+
+        Map<String, Comparable> properties = createProperties();
+        properties.put(SERVICE_DNS.key(), serviceDns);
+        properties.put(SERVICE_DNS_TIMEOUT.key(), serviceDnsTimeout);
+        properties.put(SERVICE_PORT.key(), servicePort);
+
+        // when
+        KubernetesConfig config = new KubernetesConfig(properties);
+
+        // then
+        assertEquals(DiscoveryMode.DNS_LOOKUP, config.getMode());
+        assertEquals(serviceDns, config.getServiceDns());
+        assertEquals(serviceDnsTimeout, config.getServiceDnsTimeout());
+        assertEquals(servicePort, config.getServicePort());
+    }
+
+    @Test
+    public void kubernetesApiModeDefault() {
+        // given
+        Map<String, Comparable> properties = createProperties();
+
+        // when
+        KubernetesConfig config = new KubernetesConfig(properties);
+
+        // then
+        assertEquals(DiscoveryMode.KUBERNETES_API, config.getMode());
+        assertEquals("default", config.getNamespace());
+        assertEquals(false, config.isResolveNotReadyAddresses());
+        assertEquals(TEST_API_TOKEN, config.getKubernetesApiToken());
+        assertEquals(TEST_CA_CERTIFICATE, config.getKubernetesCaCertificate());
+    }
+
+    @Test
+    public void kubernetesApiModeServiceName() {
+        // given
+        String serviceName = "service-name";
+        Map<String, Comparable> properties = createProperties();
+        properties.put(SERVICE_NAME.key(), serviceName);
+
+        // when
+        KubernetesConfig config = new KubernetesConfig(properties);
+
+        // then
+        assertEquals(DiscoveryMode.KUBERNETES_API, config.getMode());
+        assertEquals(serviceName, config.getServiceName());
+    }
+
+    @Test
+    public void kubernetesApiModeServiceLabel() {
+        // given
+        String serviceLabelName = "service-label-name";
+        String serviceLabelValue = "service-label-value";
+        Map<String, Comparable> properties = createProperties();
+        properties.put(KubernetesProperties.SERVICE_LABEL_NAME.key(), serviceLabelName);
+        properties.put(SERVICE_LABEL_VALUE.key(), serviceLabelValue);
+
+        // when
+        KubernetesConfig config = new KubernetesConfig(properties);
+
+        // then
+        assertEquals(DiscoveryMode.KUBERNETES_API, config.getMode());
+        assertEquals(serviceLabelName, config.getServiceLabelName());
+        assertEquals(serviceLabelValue, config.getServiceLabelValue());
+    }
+
+    @Test(expected = InvalidConfigurationException.class)
+    public void invalidConfigurationBothModesConfigured() {
+        // given
+        Map<String, Comparable> properties = createProperties();
+        properties.put(SERVICE_NAME.key(), "service-name");
+        properties.put(SERVICE_DNS.key(), "service-dns");
+
+        // when
+        new KubernetesConfig(properties);
+
+        // then
+        // throws exception
+    }
+
+    @Test(expected = InvalidConfigurationException.class)
+    public void invalidConfigurationBothModesConfiguredServiceLabel() {
+        // given
+        Map<String, Comparable> properties = createProperties();
+        properties.put(SERVICE_LABEL_NAME.key(), "service-label-name");
+        properties.put(SERVICE_LABEL_VALUE.key(), "service-label-value");
+        properties.put(SERVICE_DNS.key(), "service-dns");
+
+        // when
+        new KubernetesConfig(properties);
+
+        // then
+        // throws exception
+    }
+
+    @Test(expected = InvalidConfigurationException.class)
+    public void invalidConfigurationBothServiceNameAndLabel() {
+        // given
+        Map<String, Comparable> properties = createProperties();
+        properties.put(SERVICE_NAME.key(), "service-name");
+        properties.put(SERVICE_LABEL_NAME.key(), "service-label-name");
+        properties.put(SERVICE_LABEL_VALUE.key(), "service-label-value");
+
+        // when
+        new KubernetesConfig(properties);
+
+        // then
+        // throws exception
+    }
+
+    @Test(expected = InvalidConfigurationException.class)
+    public void invalidServiceDnsTimeout() {
+        // given
+        Map<String, Comparable> properties = createProperties();
+        properties.put(SERVICE_DNS.key(), "service-dns");
+        properties.put(SERVICE_DNS_TIMEOUT.key(), -1);
+
+        // when
+        new KubernetesConfig(properties);
+
+        // then
+        // throws exception
+    }
+
+    @Test(expected = InvalidConfigurationException.class)
+    public void invalidKubernetesApiRetries() {
+        // given
+        Map<String, Comparable> properties = createProperties();
+        properties.put(KUBERNETES_API_RETIRES.key(), -1);
+
+        // when
+        new KubernetesConfig(properties);
+
+        // then
+        // throws exception
+    }
+
+    @Test(expected = InvalidConfigurationException.class)
+    public void invalidServicePort() {
+        // given
+        Map<String, Comparable> properties = createProperties();
+        properties.put(SERVICE_PORT.key(), -1);
+
+        // when
+        new KubernetesConfig(properties);
+
+        // then
+        // throws exception
+    }
+
+    private static Map<String, Comparable> createProperties() {
+        Map<String, Comparable> properties = new HashMap<String, Comparable>();
+        // Predefined test properties
+        properties.put(KUBERNETES_API_TOKEN.key(), TEST_API_TOKEN);
+        properties.put(KUBERNETES_CA_CERTIFICATE.key(), TEST_CA_CERTIFICATE);
+        return properties;
+    }
+
+    @Test
+    public void readFileContents()
+            throws IOException {
+        String expectedContents = "Hello, world!\nThis is a test with Unicode ✓.";
+        String testFile = createTestFile(expectedContents);
+        String actualContents = KubernetesConfig.readFileContents(testFile);
+        assertEquals(expectedContents, actualContents);
+    }
+
+    private static String createTestFile(String expectedContents)
+            throws IOException {
+        File temp = File.createTempFile("test", ".tmp");
+        temp.deleteOnExit();
+        BufferedWriter bufferedWriter = null;
+        try {
+            bufferedWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(temp), Charset.forName("UTF-8")));
+            bufferedWriter.write(expectedContents);
+        } finally {
+            IOUtil.closeResource(bufferedWriter);
+        }
+        return temp.getAbsolutePath();
+    }
+}