From 36caf46941593a07f1676c072806e68eca466b07 Mon Sep 17 00:00:00 2001 From: Rohan Kumar Date: Fri, 27 Jul 2018 15:01:47 +0530 Subject: [PATCH] Fix #1139 : Make it easy to get the URL of a service. + Implemented getUrl() method for service + Fixed an arquillian dependency error that comes up while running tests in IntelliJ --- CHANGELOG.md | 4 + .../client/AutoAdaptableKubernetesClient.java | 2 +- .../client/DefaultKubernetesClient.java | 2 +- .../kubernetes/client/KubernetesClient.java | 2 +- .../client/ServiceToURLProvider.java | 37 ++++ .../kubernetes/client/URLFromEnvVarsImpl.java | 50 +++++ .../kubernetes/client/URLFromIngressImpl.java | 46 +++++ .../client/URLFromNodePortImpl.java | 68 ++++++ .../client/dsl/ServiceResource.java | 20 ++ .../dsl/internal/ServiceOperationsImpl.java | 51 ++++- .../client/osgi/ManagedKubernetesClient.java | 2 +- .../client/utils/URLFromServiceImplUtil.java | 194 ++++++++++++++++++ .../kubernetes/client/utils/URLUtils.java | 40 ++++ ...ic8.kubernetes.client.ServiceToURLProvider | 19 ++ .../kubernetes/examples/ServiceExample.java | 75 +++++++ kubernetes-itests/pom.xml | 2 +- .../java/io/fabric8/kubernetes/ServiceIT.java | 71 ++++++- .../src/test/resources/test-ingress.yml | 35 ++++ .../client/DefaultOpenShiftClient.java | 2 +- .../client/URLFromOpenshiftRouteImpl.java | 87 ++++++++ .../client/osgi/ManagedOpenShiftClient.java | 2 +- ...ic8.kubernetes.client.ServiceToURLProvider | 17 ++ 22 files changed, 807 insertions(+), 21 deletions(-) create mode 100644 kubernetes-client/src/main/java/io/fabric8/kubernetes/client/ServiceToURLProvider.java create mode 100644 kubernetes-client/src/main/java/io/fabric8/kubernetes/client/URLFromEnvVarsImpl.java create mode 100644 kubernetes-client/src/main/java/io/fabric8/kubernetes/client/URLFromIngressImpl.java create mode 100644 kubernetes-client/src/main/java/io/fabric8/kubernetes/client/URLFromNodePortImpl.java create mode 100644 kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/ServiceResource.java create mode 100644 kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/URLFromServiceImplUtil.java create mode 100644 kubernetes-client/src/main/resources/META-INF/services/io.fabric8.kubernetes.client.ServiceToURLProvider create mode 100644 kubernetes-examples/src/main/java/io/fabric8/kubernetes/examples/ServiceExample.java create mode 100644 kubernetes-itests/src/test/resources/test-ingress.yml create mode 100644 openshift-client/src/main/java/io/fabric8/openshift/client/URLFromOpenshiftRouteImpl.java create mode 100644 openshift-client/src/main/resources/META-INF/services/io.fabric8.kubernetes.client.ServiceToURLProvider diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b64ff30341..766383e9b34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ * Fix #1147: Cluster context was being ignored when loading the Config from a kubeconfig file + * Fix #1158: Add support for label selectors in the mock server + + * Fix #1139 : Make it easy to get the URL of a service. + Improvements * Added Kubernetes/Openshift examples for client.getVersion() diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/AutoAdaptableKubernetesClient.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/AutoAdaptableKubernetesClient.java index 2b5f988973e..d39c2266405 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/AutoAdaptableKubernetesClient.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/AutoAdaptableKubernetesClient.java @@ -224,7 +224,7 @@ public MixedOperation> services() { + public MixedOperation> services() { return delegate.services(); } diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/DefaultKubernetesClient.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/DefaultKubernetesClient.java index e4ddeafe677..4ac7e1ffba0 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/DefaultKubernetesClient.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/DefaultKubernetesClient.java @@ -201,7 +201,7 @@ public MixedOperation> services() { + public MixedOperation> services() { return new ServiceOperationsImpl(httpClient, getConfiguration(), getNamespace()); } diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/KubernetesClient.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/KubernetesClient.java index 72899992ba0..6293d99ce26 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/KubernetesClient.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/KubernetesClient.java @@ -135,7 +135,7 @@ public interface KubernetesClient extends Client { MixedOperation> secrets(); - MixedOperation> services(); + MixedOperation> services(); MixedOperation> serviceAccounts(); diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/ServiceToURLProvider.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/ServiceToURLProvider.java new file mode 100644 index 00000000000..c97e51222a5 --- /dev/null +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/ServiceToURLProvider.java @@ -0,0 +1,37 @@ +/** + * Copyright (C) 2015 Red Hat, Inc. + * + * 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 io.fabric8.kubernetes.client; + +import io.fabric8.kubernetes.api.model.Service; + +public interface ServiceToURLProvider { + enum ServiceToUrlImplPriority { + FIRST(0), SECOND(1), THIRD(2), FOURTH(3); + + private final int value; + + ServiceToUrlImplPriority(final int newVal) { + value = newVal; + } + + public int getValue() { return value; } + } + + ServiceToUrlImplPriority getPriority(); + + String getURL(Service service, String portName, String namespace, KubernetesClient client); +} diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/URLFromEnvVarsImpl.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/URLFromEnvVarsImpl.java new file mode 100644 index 00000000000..e5427343433 --- /dev/null +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/URLFromEnvVarsImpl.java @@ -0,0 +1,50 @@ +/** + * Copyright (C) 2015 Red Hat, Inc. + * + * 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 io.fabric8.kubernetes.client; + +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.client.utils.URLFromServiceImplUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class URLFromEnvVarsImpl implements ServiceToURLProvider { + public static Logger logger = LoggerFactory.getLogger(URLFromEnvVarsImpl.class); + + public static final String ANNOTATION_EXPOSE_URL = "fabric8.io/exposeUrl"; + + @Override + public String getURL(Service service, String portName, String namespace, KubernetesClient client) { + String serviceHost = URLFromServiceImplUtil.serviceToHostOrBlank(service.getMetadata().getName()); + String servicePort = URLFromServiceImplUtil.serviceToPortOrBlank(service.getMetadata().getName(), ""); + String serviceProtocol = URLFromServiceImplUtil.serviceToProtocol(service.getSpec().getPorts().iterator().next().getProtocol(), ""); + + if(!serviceHost.isEmpty() && !servicePort.isEmpty() && !serviceProtocol.isEmpty()) { + return serviceProtocol + "://" + serviceHost + ":" + servicePort; + } else { + String answer = URLFromServiceImplUtil.getOrCreateAnnotations(service).get(ANNOTATION_EXPOSE_URL); + if(answer != null && !answer.isEmpty()) { + return answer; + } + } + return null; + } + + @Override + public ServiceToUrlImplPriority getPriority() { + return ServiceToUrlImplPriority.THIRD; + } +} diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/URLFromIngressImpl.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/URLFromIngressImpl.java new file mode 100644 index 00000000000..46fc2129100 --- /dev/null +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/URLFromIngressImpl.java @@ -0,0 +1,46 @@ +/** + * Copyright (C) 2015 Red Hat, Inc. + * + * 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 io.fabric8.kubernetes.client; + +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServicePort; +import io.fabric8.kubernetes.api.model.extensions.*; +import io.fabric8.kubernetes.client.utils.URLFromServiceImplUtil; + +public class URLFromIngressImpl implements ServiceToURLProvider { + + @Override + public String getURL(Service service, String portName, String namespace, KubernetesClient client) { + ServicePort port = URLFromServiceImplUtil.getServicePortByName(service, portName); + String serviceName = service.getMetadata().getName(); + if(port == null) { + throw new RuntimeException("Couldn't find port: " + portName + " for service " + service.getMetadata().getName()); + } + + IngressList ingresses = client.extensions().ingresses().inNamespace(namespace).list(); + if(ingresses != null && !ingresses.getItems().isEmpty()) { + return URLFromServiceImplUtil.getURLFromIngressList(ingresses.getItems(), namespace, serviceName, port); + } + return null; + } + + @Override + public ServiceToUrlImplPriority getPriority() { + return ServiceToUrlImplPriority.FIRST; + } + +} diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/URLFromNodePortImpl.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/URLFromNodePortImpl.java new file mode 100644 index 00000000000..f6800280021 --- /dev/null +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/URLFromNodePortImpl.java @@ -0,0 +1,68 @@ +/** + * Copyright (C) 2015 Red Hat, Inc. + * + * 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 io.fabric8.kubernetes.client; + +import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.client.utils.URLFromServiceImplUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +public class URLFromNodePortImpl implements ServiceToURLProvider { + public static Logger logger = LoggerFactory.getLogger(URLFromNodePortImpl.class); + + public String getURL(Service service, String portName, String namespace, KubernetesClient client) { + boolean bFound = false; + ServicePort port = URLFromServiceImplUtil.getServicePortByName(service, portName); + String serviceProto = port.getProtocol(); + String clusterIP = null; + Integer portNumber = 0; + Integer nodePort = port.getNodePort(); + if(nodePort != null) { + try { + NodeList nodeList = client.nodes().list(); + if(nodeList != null && nodeList.getItems() != null) { + for(Node item : nodeList.getItems()) { + NodeStatus status = item.getStatus(); + if(!bFound && status != null) { + List addresses = status.getAddresses(); + for(NodeAddress address : addresses) { + String ip = address.getAddress(); + if (!ip.isEmpty()) { + clusterIP = ip; + portNumber = nodePort; + bFound = true; + break; + } + } + } + } + } + } catch (KubernetesClientException exception) { + logger.warn("Could not find a node! " + exception); + } + } + return (serviceProto + "://" + clusterIP + ":" + portNumber).toLowerCase(); + } + + @Override + public ServiceToUrlImplPriority getPriority() { + return ServiceToUrlImplPriority.SECOND; + } + +} diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/ServiceResource.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/ServiceResource.java new file mode 100644 index 00000000000..43450b875a0 --- /dev/null +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/ServiceResource.java @@ -0,0 +1,20 @@ +/** + * Copyright (C) 2015 Red Hat, Inc. + * + * 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 io.fabric8.kubernetes.client.dsl; + +public interface ServiceResource extends Resource { + String getURL(String portName); +} diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/ServiceOperationsImpl.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/ServiceOperationsImpl.java index 624fb3fb342..03b4ef2addb 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/ServiceOperationsImpl.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/dsl/internal/ServiceOperationsImpl.java @@ -15,23 +15,19 @@ */ package io.fabric8.kubernetes.client.dsl.internal; -import io.fabric8.kubernetes.api.model.Endpoints; -import io.fabric8.kubernetes.client.dsl.Resource; -import okhttp3.OkHttpClient; -import io.fabric8.kubernetes.api.model.DoneableService; -import io.fabric8.kubernetes.api.model.Service; -import io.fabric8.kubernetes.api.model.ServiceBuilder; -import io.fabric8.kubernetes.api.model.ServiceList; +import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.client.*; import io.fabric8.kubernetes.client.Config; -import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.dsl.ServiceResource; +import io.fabric8.kubernetes.client.utils.URLUtils; +import okhttp3.OkHttpClient; import io.fabric8.kubernetes.client.dsl.base.HasMetadataOperation; -import java.util.Map; -import java.util.TreeMap; +import java.util.*; import java.util.concurrent.TimeUnit; -public class ServiceOperationsImpl extends HasMetadataOperation> { +public class ServiceOperationsImpl extends HasMetadataOperation> implements ServiceResource { public ServiceOperationsImpl(OkHttpClient client, Config config, String namespace) { this(client, config, null, namespace, null, true, null, null, false, -1, new TreeMap(), new TreeMap(), new TreeMap(), new TreeMap(), new TreeMap()); @@ -83,4 +79,37 @@ public Service waitUntilReady(long amount, TimeUnit timeUnit) throws Interrupted return get(); } + + public String getURL(String portName) { + String clusterIP = getMandatory().getSpec().getClusterIP(); + if("None".equals(clusterIP)) { + throw new IllegalStateException("Service: " + getMandatory().getMetadata().getName() + " in namespace " + + namespace + " is head-less. Search for endpoints instead"); + } + + ServiceLoader urlProvider = ServiceLoader.load(ServiceToURLProvider.class, Thread.currentThread().getContextClassLoader()); + Iterator iterator = urlProvider.iterator(); + List servicesList = new ArrayList<>(); + + while(iterator.hasNext()) { + servicesList.add(iterator.next()); + } + + // Sort all loaded implementations according to priority + Collections.sort(servicesList, new ServiceToUrlSortComparator()); + for(ServiceToURLProvider serviceToURLProvider : servicesList) { + String url = serviceToURLProvider.getURL(getMandatory(), portName, namespace, new DefaultKubernetesClient(client, getConfig())); + if(url != null && URLUtils.isValidURL(url)) { + return url; + } + } + + return null; + } + + public class ServiceToUrlSortComparator implements Comparator { + public int compare(ServiceToURLProvider first, ServiceToURLProvider second) { + return first.getPriority().getValue() - second.getPriority().getValue(); + } + } } diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/osgi/ManagedKubernetesClient.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/osgi/ManagedKubernetesClient.java index 23ef91bec6b..544b4a63726 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/osgi/ManagedKubernetesClient.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/osgi/ManagedKubernetesClient.java @@ -247,7 +247,7 @@ public MixedOperation> return delegate.pods(); } - public MixedOperation> services() { + public MixedOperation> services() { return delegate.services(); } diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/URLFromServiceImplUtil.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/URLFromServiceImplUtil.java new file mode 100644 index 00000000000..6729a5ae229 --- /dev/null +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/URLFromServiceImplUtil.java @@ -0,0 +1,194 @@ +/** + * Copyright (C) 2015 Red Hat, Inc. + * + * 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 io.fabric8.kubernetes.client.utils; + +import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.api.model.extensions.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +public class URLFromServiceImplUtil { + public static Logger logger = LoggerFactory.getLogger(URLFromServiceImplUtil.class); + public static final String DEFAULT_PROTO = "tcp"; + private static final String HOST_SUFFIX = "_SERVICE_HOST"; + private static final String PORT_SUFFIX = "_SERVICE_PORT"; + private static final String PROTO_SUFFIX = "_TCP_PROTO"; + + public static String serviceToHostOrBlank(String serviceName) { + return getEnvVarOrSystemProperty(toServiceHostEnvironmentVariable(serviceName), ""); + } + + private static String getEnvVarOrSystemProperty(String envVarName, String defaultValue) { + String answer = null; + try { + answer = System.getenv(envVarName); + } catch (Exception e) { + logger.warn("Failed to look up environment variable $" + envVarName + ". " + e, e); + } + if (answer == null || answer.isEmpty()) { + answer = System.getProperty(envVarName, defaultValue); + } + if (!answer.isEmpty()) { + return answer; + } else { + return defaultValue; + } + } + + public static String serviceToProtocol(String serviceName, String servicePort) { + return getEnvVarOrSystemProperty(toEnvVariable(serviceName + PORT_SUFFIX + "_" + servicePort + PROTO_SUFFIX), DEFAULT_PROTO); + } + + public static Map getOrCreateAnnotations(HasMetadata entity) { + ObjectMeta metadata = getOrCreateMetadata(entity); + Map answer = metadata.getAnnotations(); + if (answer == null) { + // use linked so the annotations can be in the FIFO order + answer = new LinkedHashMap<>(); + metadata.setAnnotations(answer); + } + return answer; + } + + public static ObjectMeta getOrCreateMetadata(HasMetadata entity) { + ObjectMeta metadata = entity.getMetadata(); + if (metadata == null) { + metadata = new ObjectMeta(); + entity.setMetadata(metadata); + } + return metadata; + } + + public static String serviceToPortOrBlank(String serviceName, String portName) { + String envVarName = toServicePortEnvironmentVariable(serviceName, portName); + return getEnvVarOrSystemProperty(envVarName, ""); + } + + public static String toServicePortEnvironmentVariable(String serviceName, String portName) { + String name = serviceName + PORT_SUFFIX + (portName.isEmpty() ? "_" + portName : ""); + return toEnvVariable(name); + } + + private static String toServiceHostEnvironmentVariable(String serviceName) { + return toEnvVariable(serviceName + HOST_SUFFIX); + } + + public static String toEnvVariable(String serviceName) { + return serviceName.toUpperCase().replaceAll("-", "_"); + } + + public static String getURLFromIngressList(List ingressList, String namespace, String serviceName, ServicePort port) { + for(Ingress item : ingressList) { + String ns = getNamespace(item); + if(Objects.equals(ns, namespace) && item.getSpec() != null) { + return getURLFromIngressSpec(item.getSpec(), serviceName, port); + } + } + return null; + } + + public static String getURLFromIngressSpec(IngressSpec spec, String serviceName, ServicePort port) { + List ingressRules = spec.getRules(); + if(ingressRules != null && !ingressRules.isEmpty()) { + for(IngressRule rule : ingressRules) { + HTTPIngressRuleValue http = rule.getHttp(); + if(http != null && http.getPaths() != null) { + return getURLFromIngressRules(http.getPaths(), spec, serviceName, port, rule); + } + } + } + return null; + } + + public static String getURLFromIngressRules(List paths, IngressSpec spec, String serviceName, ServicePort port, IngressRule rule) { + for(HTTPIngressPath path : paths) { + IngressBackend backend = path.getBackend(); + if(backend != null) { + String backendServiceName = backend.getServiceName(); + if(serviceName.equals(backendServiceName) && portsMatch(port, backend.getServicePort())) { + String pathPostFix = path.getPath(); + if(spec.getTls() != null) { + return getURLFromTLSHost(rule, pathPostFix); + } + String answer = rule.getHost(); + if(answer != null && !answer.isEmpty()) { + pathPostFix = fixPathPostFixIfEmpty(pathPostFix); + return "http://" + URLUtils.pathJoin(answer, pathPostFix); + } + } + } + } + return null; + } + + public static String getURLFromTLSHost(IngressRule rule, String pathPostFix) { + String host = rule.getHost(); + if (!host.isEmpty()) { + pathPostFix = fixPathPostFixIfEmpty(pathPostFix); + return "https://" + URLUtils.pathJoin(host, pathPostFix); + } + return null; + } + + private static String fixPathPostFixIfEmpty(String pathPostFix) { + return pathPostFix.isEmpty() ? "/" : pathPostFix; + } + + private static boolean portsMatch(ServicePort servicePort, IntOrString intOrString) { + if (intOrString != null) { + Integer port = servicePort.getPort(); + Integer intVal = intOrString.getIntVal(); + String strVal = intOrString.getStrVal(); + if (intVal != null) { + if (port != null) { + return port.intValue() == intVal.intValue(); + } else { + /// should we find the port by name now? + } + } else if (strVal != null ){ + return strVal.equals(servicePort.getName()); + } + } + return false; + } + + public static String getNamespace(HasMetadata entity) { + if (entity != null) { + return entity.getMetadata() != null ? entity.getMetadata().getNamespace() : null; + } else { + return null; + } + } + + public static ServicePort getServicePortByName(Service service, String portName) { + if (portName.isEmpty()) { + return service.getSpec().getPorts().iterator().next(); + } + + for (ServicePort servicePort : service.getSpec().getPorts()) { + if (Objects.equals(servicePort.getName(), portName)) { + return servicePort; + } + } + return null; + } +} diff --git a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/URLUtils.java b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/URLUtils.java index 2a5259400c1..864a16fcb68 100644 --- a/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/URLUtils.java +++ b/kubernetes-client/src/main/java/io/fabric8/kubernetes/client/utils/URLUtils.java @@ -15,8 +15,10 @@ */ package io.fabric8.kubernetes.client.utils; +import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; +import java.net.URL; public class URLUtils { private URLUtils() {} @@ -106,4 +108,42 @@ private static StringBuilder getQueryParams(URI firstPart, String[] parts, int i private static boolean containsQueryParam(URI uri) { return uri.getQuery() != null; } + + /** + * Joins all the given strings, ignoring nulls so that they form a URL with / between the paths without a // if the previous path ends with / and the next path starts with / unless a path item is blank + * + * @returns the strings concatenated together with / while avoiding a double // between non blank strings. + */ + public static String pathJoin(String... strings) { + StringBuilder buffer = new StringBuilder(); + for (String string : strings) { + if (string == null) { + continue; + } + if (buffer.length() > 0) { + boolean bufferEndsWithSeparator = buffer.toString().endsWith("/"); + boolean stringStartsWithSeparator = string.startsWith("/"); + if (bufferEndsWithSeparator) { + if (stringStartsWithSeparator) { + string = string.substring(1); + } + } else { + if (!stringStartsWithSeparator) { + buffer.append("/"); + } + } + } + buffer.append(string); + } + return buffer.toString(); + } + + public static boolean isValidURL(String url) { + try { + URI u = new URI(url); + } catch (URISyntaxException exception) { + return false; + } + return url.contains("null") ? false : true; + } } diff --git a/kubernetes-client/src/main/resources/META-INF/services/io.fabric8.kubernetes.client.ServiceToURLProvider b/kubernetes-client/src/main/resources/META-INF/services/io.fabric8.kubernetes.client.ServiceToURLProvider new file mode 100644 index 00000000000..4d1efcf4172 --- /dev/null +++ b/kubernetes-client/src/main/resources/META-INF/services/io.fabric8.kubernetes.client.ServiceToURLProvider @@ -0,0 +1,19 @@ +# +# Copyright (C) 2015 Red Hat, Inc. +# +# 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. +# + +io.fabric8.kubernetes.client.URLFromEnvVarsImpl +io.fabric8.kubernetes.client.URLFromIngressImpl +io.fabric8.kubernetes.client.URLFromNodePortImpl diff --git a/kubernetes-examples/src/main/java/io/fabric8/kubernetes/examples/ServiceExample.java b/kubernetes-examples/src/main/java/io/fabric8/kubernetes/examples/ServiceExample.java new file mode 100644 index 00000000000..6bdddd1bc10 --- /dev/null +++ b/kubernetes-examples/src/main/java/io/fabric8/kubernetes/examples/ServiceExample.java @@ -0,0 +1,75 @@ +/** + * Copyright (C) 2015 Red Hat, Inc. + * + * 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 io.fabric8.kubernetes.examples; + +import io.fabric8.kubernetes.api.model.IntOrString; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServiceBuilder; +import io.fabric8.kubernetes.api.model.extensions.Ingress; +import io.fabric8.kubernetes.api.model.extensions.IngressBuilder; +import io.fabric8.kubernetes.client.DefaultKubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; + +public class ServiceExample { + private static final Logger logger = LoggerFactory.getLogger(ServiceExample.class); + + public static void main(String args[]) { + try (final KubernetesClient client = new DefaultKubernetesClient()) { + Service service = new ServiceBuilder() + .withNewMetadata() + .withName("my-service") + .endMetadata() + .withNewSpec() + .withSelector(Collections.singletonMap("app", "MyApp")) + .addNewPort() + .withName("test-port") + .withProtocol("TCP") + .withPort(80) + .withTargetPort(new IntOrString(9376)) + .endPort() + .withType("LoadBalancer") + .endSpec() + .withNewStatus() + .withNewLoadBalancer() + .addNewIngress() + .withIp("146.148.47.155") + .endIngress() + .endLoadBalancer() + .endStatus() + .build(); + + service = client.services().inNamespace(client.getNamespace()).create(service); + log("Created service with name ", service.getMetadata().getName()); + + String serviceURL = client.services().inNamespace(client.getNamespace()).withName(service.getMetadata().getName()).getURL("test-port"); + log("Service URL", serviceURL); + + } + } + + private static void log(String action, Object obj) { + logger.info("{}: {}", action, obj); + } + + private static void log(String action) { + logger.info(action); + } +} diff --git a/kubernetes-itests/pom.xml b/kubernetes-itests/pom.xml index 80a7e43113f..1c489dbc285 100644 --- a/kubernetes-itests/pom.xml +++ b/kubernetes-itests/pom.xml @@ -63,7 +63,7 @@ org.arquillian.cube - arquillian-cube-openshift + arquillian-cube-openshift-starter ${arquillian.cube.version} test diff --git a/kubernetes-itests/src/test/java/io/fabric8/kubernetes/ServiceIT.java b/kubernetes-itests/src/test/java/io/fabric8/kubernetes/ServiceIT.java index 43b54b6bc1f..6f059ca92f4 100644 --- a/kubernetes-itests/src/test/java/io/fabric8/kubernetes/ServiceIT.java +++ b/kubernetes-itests/src/test/java/io/fabric8/kubernetes/ServiceIT.java @@ -17,9 +17,13 @@ package io.fabric8.kubernetes; import io.fabric8.kubernetes.api.model.*; +import io.fabric8.kubernetes.api.model.extensions.Ingress; import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.openshift.api.model.RouteBuilder; +import io.fabric8.openshift.client.OpenShiftClient; import org.arquillian.cube.kubernetes.api.Session; import org.arquillian.cube.kubernetes.impl.requirement.RequiresKubernetes; +import org.arquillian.cube.openshift.impl.enricher.external.OpenshiftClientResourceProvider; import org.arquillian.cube.requirement.ArquillianConditionalRunner; import org.jboss.arquillian.test.api.ArquillianResource; import org.junit.After; @@ -51,12 +55,39 @@ public class ServiceIT { public void init() { currentNamespace = session.getNamespace(); svc1 = new ServiceBuilder() - .withNewMetadata().withName("svc1").endMetadata() - .withNewSpec().withSelector(Collections.singletonMap("app", "MyApp")).addNewPort().withName("http").withProtocol("TCP").withPort(80).withTargetPort(new IntOrString(9376)).endPort().endSpec() + .withNewMetadata() + .withName("svc1") + .endMetadata() + .withNewSpec() + .withSelector(Collections.singletonMap("app", "MyApp")) + .addNewPort() + .withName("http") + .withProtocol("TCP") + .withPort(80) + .withTargetPort(new IntOrString(9376)) + .endPort() + .withType("LoadBalancer") + .endSpec() + .withNewStatus() + .withNewLoadBalancer() + .addNewIngress() + .withIp("146.148.47.155") + .endIngress() + .endLoadBalancer() + .endStatus() .build(); svc2 = new ServiceBuilder() .withNewMetadata().withName("svc2").endMetadata() - .withNewSpec().withType("ExternalName").withExternalName("my.database.example.com").endSpec() + .withNewSpec().withType("ExternalName").withExternalName("my.database.example.com") + .addNewPort().withName("80").withProtocol("TCP").withPort(80).endPort() + .endSpec() + .withNewStatus() + .withNewLoadBalancer() + .addNewIngress() + .withIp("146.148.47.155") + .endIngress() + .endLoadBalancer() + .endStatus() .build(); client.services().inNamespace(currentNamespace).createOrReplace(svc1); @@ -101,6 +132,40 @@ public void delete() { assertTrue(bDeleted); } + @Test + public void getURL() { + // Testing NodePort Impl + String url = client.services().inNamespace(currentNamespace).withName("svc1").getURL("http"); + assertNotNull(url); + + // Testing Ingress Impl + Ingress ingress = client.extensions().ingresses().load(getClass().getResourceAsStream("/test-ingress.yml")).get(); + client.extensions().ingresses().inNamespace(currentNamespace).create(ingress); + + url = client.services().inNamespace(currentNamespace).withName("svc2").getURL("80"); + assertNotNull(url); + + // Testing OpenShift Route Impl + Service svc3 = client.services().inNamespace(currentNamespace).create(new ServiceBuilder() + .withNewMetadata().withName("svc3").endMetadata() + .withNewSpec() + .addNewPort().withName("80").withProtocol("TCP").withPort(80).endPort() + .endSpec() + .build()); + + OpenShiftClient openshiftClient = client.adapt(OpenShiftClient.class); + openshiftClient.routes().inNamespace(currentNamespace).create(new RouteBuilder() + .withNewMetadata().withName(svc3.getMetadata().getName()).endMetadata() + .withNewSpec() + .withHost("www.example.com") + .withNewTo().withName(svc3.getMetadata().getName()).withKind("Service").endTo() + .endSpec() + .build()); + + url = client.services().inNamespace(currentNamespace).withName("svc3").getURL("80"); + assertNotNull(url); + } + @After public void cleanup() throws InterruptedException { client.services().inNamespace(currentNamespace).delete(); diff --git a/kubernetes-itests/src/test/resources/test-ingress.yml b/kubernetes-itests/src/test/resources/test-ingress.yml new file mode 100644 index 00000000000..a96099df746 --- /dev/null +++ b/kubernetes-itests/src/test/resources/test-ingress.yml @@ -0,0 +1,35 @@ +# +# Copyright (C) 2015 Red Hat, Inc. +# +# 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. +# + +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + name: test + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / +spec: + rules: + - host: foo.bar.com + http: + paths: + - path: /foo + backend: + serviceName: svc2 + servicePort: 80 + - path: /bar + backend: + serviceName: s2 + servicePort: 80 diff --git a/openshift-client/src/main/java/io/fabric8/openshift/client/DefaultOpenShiftClient.java b/openshift-client/src/main/java/io/fabric8/openshift/client/DefaultOpenShiftClient.java index bdd6af25a44..358fe07b567 100644 --- a/openshift-client/src/main/java/io/fabric8/openshift/client/DefaultOpenShiftClient.java +++ b/openshift-client/src/main/java/io/fabric8/openshift/client/DefaultOpenShiftClient.java @@ -306,7 +306,7 @@ public MixedOperation> services() { + public MixedOperation> services() { return delegate.services(); } diff --git a/openshift-client/src/main/java/io/fabric8/openshift/client/URLFromOpenshiftRouteImpl.java b/openshift-client/src/main/java/io/fabric8/openshift/client/URLFromOpenshiftRouteImpl.java new file mode 100644 index 00000000000..b82b19e4cb7 --- /dev/null +++ b/openshift-client/src/main/java/io/fabric8/openshift/client/URLFromOpenshiftRouteImpl.java @@ -0,0 +1,87 @@ +/** + * Copyright (C) 2015 Red Hat, Inc. + * + * 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 io.fabric8.openshift.client; + +import io.fabric8.kubernetes.api.model.RootPaths; +import io.fabric8.kubernetes.api.model.Service; +import io.fabric8.kubernetes.api.model.ServicePort; +import io.fabric8.kubernetes.client.KubernetesClient; +import io.fabric8.kubernetes.client.KubernetesClientException; +import io.fabric8.kubernetes.client.ServiceToURLProvider; +import io.fabric8.kubernetes.client.utils.URLFromServiceImplUtil; +import io.fabric8.openshift.api.model.Route; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +public class URLFromOpenshiftRouteImpl implements ServiceToURLProvider { + public static final Logger logger = LoggerFactory.getLogger(URLFromOpenshiftRouteImpl.class); + private final ConcurrentMap IS_OPENSHIFT = new ConcurrentHashMap<>(); + + @Override + public String getURL(Service service, String portName, String namespace, KubernetesClient client) { + String serviceName = service.getMetadata().getName(); + ServicePort port = URLFromServiceImplUtil.getServicePortByName(service, portName); + if(port != null && port.getName() != null && isOpenShift(client)) { + try { + String serviceProtocol = port.getProtocol(); + OpenShiftClient openShiftClient = client.adapt(OpenShiftClient.class); + Route route = openShiftClient.routes().inNamespace(namespace).withName(service.getMetadata().getName()).get(); + if (route != null) { + return (serviceProtocol + "://" + route.getSpec().getHost()).toLowerCase(); + } + } catch (KubernetesClientException e) { + if(e.getCode() == HttpURLConnection.HTTP_FORBIDDEN) { + logger.warn("Could not lookup route:" + serviceName + " in namespace:"+ namespace +", due to: " + e.getMessage()); + } + } + } + return null; + } + + @Override + public ServiceToUrlImplPriority getPriority() { + return ServiceToUrlImplPriority.FOURTH; + } + + public boolean isOpenShift(KubernetesClient client) { + URL masterUrl = client.getMasterUrl(); + if (IS_OPENSHIFT.containsKey(masterUrl)) { + return IS_OPENSHIFT.get(masterUrl); + } else { + RootPaths rootPaths = client.rootPaths(); + if (rootPaths != null) { + List paths = rootPaths.getPaths(); + if (paths != null) { + for (String path : paths) { + if (java.util.Objects.equals("/oapi", path) || java.util.Objects.equals("oapi", path)) { + IS_OPENSHIFT.putIfAbsent(masterUrl, true); + return true; + } + } + } + } + } + IS_OPENSHIFT.putIfAbsent(masterUrl, false); + return false; + } +} diff --git a/openshift-client/src/main/java/io/fabric8/openshift/client/osgi/ManagedOpenShiftClient.java b/openshift-client/src/main/java/io/fabric8/openshift/client/osgi/ManagedOpenShiftClient.java index 9680a0ba8ea..3cb423169ff 100644 --- a/openshift-client/src/main/java/io/fabric8/openshift/client/osgi/ManagedOpenShiftClient.java +++ b/openshift-client/src/main/java/io/fabric8/openshift/client/osgi/ManagedOpenShiftClient.java @@ -424,7 +424,7 @@ public MixedOperation> services() { + public MixedOperation> services() { return delegate.services(); } diff --git a/openshift-client/src/main/resources/META-INF/services/io.fabric8.kubernetes.client.ServiceToURLProvider b/openshift-client/src/main/resources/META-INF/services/io.fabric8.kubernetes.client.ServiceToURLProvider new file mode 100644 index 00000000000..a09f81824dd --- /dev/null +++ b/openshift-client/src/main/resources/META-INF/services/io.fabric8.kubernetes.client.ServiceToURLProvider @@ -0,0 +1,17 @@ +# +# Copyright (C) 2015 Red Hat, Inc. +# +# 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. +# + +io.fabric8.openshift.client.URLFromOpenshiftRouteImpl