001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
002 *
003 * Copyright © 2017 MicroBean.
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
014 * implied.  See the License for the specific language governing
015 * permissions and limitations under the License.
016 */
017package org.microbean.kubernetes;
018
019import java.io.Closeable; // for javadoc only
020
021import java.net.MalformedURLException;
022import java.net.URL;
023
024import io.fabric8.kubernetes.api.model.Pod;
025import io.fabric8.kubernetes.api.model.PodCondition;
026import io.fabric8.kubernetes.api.model.PodList;
027import io.fabric8.kubernetes.api.model.PodStatus;
028
029import io.fabric8.kubernetes.client.LocalPortForward;
030
031import io.fabric8.kubernetes.client.dsl.Listable;
032
033import io.fabric8.kubernetes.client.dsl.base.OperationSupport;
034
035import io.fabric8.kubernetes.client.dsl.internal.PortForwarderWebsocket;
036
037import okhttp3.OkHttpClient;
038
039/**
040 * A utility class that helps with operations concerning {@link Pod}s.
041 *
042 * @author <a href="https://about.me/lairdnelson"
043 * target="_parent">Laird Nelson</a>
044 *
045 * @see #forwardPort(OkHttpClient, Listable, int)
046 *
047 * @see #isReady(Pod)
048 *
049 * @see Pod
050 */
051public final class Pods {
052
053
054  /*
055   * Constructors.
056   */
057
058  /**
059   * Creates a new {@link Pods} instance.
060   */
061  private Pods() {
062    super();
063  }
064
065
066  /*
067   * Static methods.
068   */
069  
070
071  /**
072   * Forwards an arbitrary local port to the supplied {@code
073   * remotePort} on the {@linkplain #getFirstReadyPod(Listable) first
074   * ready <code>Pod</code>} in the supplied {@link Listable} using
075   * the supplied {@link OkHttpClient}.
076   *
077   * <p>This method may return {@code null}.</p>
078   *
079   * @param httpClient an {@link OkHttpClient} used to communicate
080   * with Kubernetes; may be {@code null} in which case {@code null}
081   * will be returned
082   *
083   * @param pods a {@link Listable} of {@link PodList}s; <strong>must
084   * also be an instance of {@link OperationSupport}</strong> or
085   * {@code null} will be returned
086   *
087   * @param remotePort the port on the {@link Pod} that is ultimately
088   * located to which traffic should be forwarded
089   *
090   * @return a {@link LocalPortForward} representing the port
091   * forwarding operation, or {@code null}; must be {@linkplain
092   * Closeable#close() closed} if non-{@code null}
093   *
094   * @exception MalformedURLException if a {@link URL} to the
095   * Kubernetes resource representing the {@link Pod} in question
096   * could not be constructed
097   *
098   * @see PortForwarderWebsocket#PortForwarderWebsocket(OkHttpClient)
099   *
100   * @see <a
101   * href="https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-conditions">Pod
102   * conditions documentation</a>
103   */
104  public static final LocalPortForward forwardPort(final OkHttpClient httpClient, final Listable<? extends PodList> pods, final int remotePort) throws MalformedURLException {
105    return forwardPort(httpClient, pods, remotePort, 0);
106  }
107
108  public static final LocalPortForward forwardPort(final OkHttpClient httpClient, final Listable<? extends PodList> pods, final int remotePort, final int localPort) throws MalformedURLException {
109    LocalPortForward returnValue = null;
110    if (httpClient != null && pods instanceof OperationSupport) {
111      final String urlBase = ((OperationSupport)pods).getNamespacedUrl().toExternalForm();
112      assert urlBase != null;
113      final Pod readyPod = getFirstReadyPod(pods);
114      if (readyPod != null) {
115        final String name = readyPod.getMetadata().getName();
116        assert name != null;
117        final URL url = new URL(new StringBuilder(urlBase).append("/").append(name).toString());
118        PortForwarderWebsocket portForwarderWebsocket = new PortForwarderWebsocket(httpClient);
119        returnValue = localPort <= 0 ? portForwarderWebsocket.forward(url, remotePort) 
120        : portForwarderWebsocket.forward(url, remotePort, localPort);
121      }
122    }
123    return returnValue;
124  }
125
126  /**
127   * Returns {@link Boolean#TRUE} if the supplied {@link Pod} is known
128   * to be ready, {@link Boolean#FALSE} if it is known to be not ready
129   * and {@code null} if its readiness status is unknown.
130   *
131   * <p>This method may return {@code null}.</p>
132   *
133   * @param pod the {@link Pod} to check; may be {@code null} in which
134   * case {@code null} will be returned
135   *
136   * @return {@link Boolean#TRUE} if the supplied {@link Pod} is known
137   * to be ready, {@link Boolean#FALSE} if it is known to be not ready
138   * and {@code null} if its readiness status is unknown
139   *
140   * @see #isReady(PodStatus)
141   *
142   * @see <a
143   * href="https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-conditions">Pod
144   * conditions documentation</a>
145   */
146  public static final Boolean isReady(final Pod pod) {
147    Boolean returnValue = null;
148    if (pod != null) {
149      returnValue = isReady(pod.getStatus());
150    }
151    return returnValue;
152  }
153
154  /**
155   * Returns {@link Boolean#TRUE} if the supplied {@link PodStatus} is
156   * known to be ready, {@link Boolean#FALSE} if it is known to be not
157   * ready and {@code null} if its readiness status is unknown.
158   *
159   * <p>This method may return {@code null}.</p>
160   *
161   * @param podStatus the {@link PodStatus} to check; may be {@code
162   * null} in which case {@code null} will be returned
163   *
164   * @return {@link Boolean#TRUE} if the supplied {@link PodStatus} is
165   * known to be ready, {@link Boolean#FALSE} if it is known to be not
166   * ready and {@code null} if its readiness status is unknown
167   *
168   * @see #isReady(Iterable)
169   *
170   * @see <a
171   * href="https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-conditions">Pod
172   * conditions documentation</a>
173   */
174  public static final Boolean isReady(final PodStatus podStatus) {
175    Boolean returnValue = null;
176    if (podStatus != null) {
177      returnValue = isReady(podStatus.getConditions());
178    }
179    return returnValue;
180  }
181
182  /**
183   * Returns {@link Boolean#TRUE} if the supplied {@link Iterable} of
184   * {@link PodCondition}s contains a {@link PodCondition} whose
185   * {@link PodCondition#getType()} method returns {@code Ready} and
186   * whose {@link PodCondition#getStatus()} method returns {@code
187   * True}.
188   *
189   * <p>If the supplied {@link Iterable} of
190   * {@link PodCondition}s <em>also</em> contains a {@link PodCondition} whose
191   * {@link PodCondition#getType()} method returns {@code Ready} and
192   * whose {@link PodCondition#getStatus()} method returns {@code
193   * False}, then {@code null} is returned.</p>
194   *
195   * <p>If instead the supplied {@link Iterable} of
196   * {@link PodCondition}s contains a {@link PodCondition} whose
197   * {@link PodCondition#getType()} method returns {@code Ready} and
198   * whose {@link PodCondition#getStatus()} method returns {@code
199   * False} and does not <em>also</em> contains a {@link PodCondition} whose
200   * {@link PodCondition#getType()} method returns {@code Ready} and
201   * whose {@link PodCondition#getStatus()} method returns {@code
202   * True}, then {@link Boolean#FALSE} is returned.</p>
203   *
204   * <p>{@code null} is returned in all other cases.</p>
205   *
206   * <p>This method may return {@code null}.</p>
207   *
208   * @param podConditions an {@link Iterable} of {@link
209   * PodCondition}s; may be {@code null} in which case {@code null}
210   * will be returned
211   *
212   * @return {@link Boolean#TRUE} if the supplied {@link Iterable} of
213   * {@link PodCondition}s represents a state of affairs known to
214   * represent the readiness of the {@link PodStatus} they describe;
215   * {@link Boolean#FALSE} if the supplied {@link Iterable} of {@link
216   * PodCondition}s represents a state of affairs known to represent
217   * the unreadiness of the {@link PodStatus} they describe, or {@code
218   * null} in all other cases
219   *
220   * @see <a
221   * href="https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-conditions">Pod
222   * conditions documentation</a>
223   */
224  public static final Boolean isReady(final Iterable<? extends PodCondition> podConditions) {
225    Boolean returnValue = null;
226    if (podConditions != null) {
227      for (final PodCondition condition : podConditions) {
228        if (condition != null) {
229          final String conditionType = condition.getType();
230          // See https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-conditions.
231          if ("Ready".equals(conditionType)) {
232            final String conditionStatus = condition.getStatus();
233            switch (conditionStatus) {
234            case "True":
235              if (Boolean.FALSE.equals(returnValue)) {
236                returnValue = null;
237              } else {
238                returnValue = Boolean.TRUE;
239              }
240              break;
241            case "False":
242              if (Boolean.TRUE.equals(returnValue)) {
243                returnValue = null;
244              } else {
245                returnValue = Boolean.FALSE;
246              }
247              break;
248            case "Unknown":
249              returnValue = null;
250              break;
251            default:
252              throw new IllegalStateException("Unexpected value for PodCondition of type Ready: " + conditionStatus);
253            }
254          }
255        }
256      }
257    }
258    return returnValue;
259  }
260
261  /**
262   * Returns the first {@link Pod} encountered in the supplied {@link
263   * Iterable} of {@link Pod}s for which the {@link #isReady(Pod)}
264   * method returns {@link Boolean#TRUE}, or {@code null}.
265   *
266   * <p>This method may return {@code null}.</p>
267   *
268   * @param pods an {@link Iterable} of {@link Pod}s to check; may be
269   * {@code null} in which case {@code null} will be returned
270   *
271   * @return a {@link Pod} known to be ready, or {@code null}
272   *
273   * @see #isReady(Pod)
274   *
275   * @see <a
276   * href="https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-conditions">Pod
277   * conditions documentation</a>
278   */
279  public static final Pod getFirstReadyPod(final Iterable<? extends Pod> pods) {
280    Pod returnValue = null;
281    if (pods != null) {
282      for (final Pod pod : pods) {
283        if (Boolean.TRUE.equals(isReady(pod))) {
284          returnValue = pod;
285          break;
286        }
287      }
288    }
289    return returnValue;
290  }
291
292  /**
293   * Returns the first {@link Pod} encountered in the supplied {@link
294   * PodList} of {@link Pod}s for which the {@link #isReady(Pod)}
295   * method returns {@link Boolean#TRUE}, or {@code null}.
296   *
297   * <p>This method may return {@code null}.</p>
298   *
299   * @param podList a {@link PodList} of {@link Pod}s to check; may be
300   * {@code null} in which case {@code null} will be returned
301   *
302   * @return a {@link Pod} known to be ready, or {@code null}
303   *
304   * @see #getFirstReadyPod(Iterable)
305   *
306   * @see #isReady(Pod)
307   *
308   * @see <a
309   * href="https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-conditions">Pod
310   * conditions documentation</a>
311   */
312  public static final Pod getFirstReadyPod(final PodList podList) {
313    Pod returnValue = null;
314    if (podList != null) {
315      returnValue = getFirstReadyPod(podList.getItems());
316    }
317    return returnValue;
318  }
319
320  /**
321   * Returns the first {@link Pod} reachable from the supplied {@link
322   * Listable} of {@link PodList}s for which the {@link #isReady(Pod)}
323   * method returns {@link Boolean#TRUE}, or {@code null}.
324   *
325   * <p>This method may return {@code null}.</p>
326   *
327   * @param podsResource a {@link Listable} of {@link PodList}s
328   * representing {@link Pod}s to check; may be {@code null} in which
329   * case {@code null} will be returned
330   *
331   * @return a {@link Pod} known to be ready, or {@code null}
332   *
333   * @see #getFirstReadyPod(PodList)
334   *
335   * @see #isReady(Pod)
336   *
337   * @see <a
338   * href="https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#pod-conditions">Pod
339   * conditions documentation</a>
340   */
341  public static final Pod getFirstReadyPod(final Listable<? extends PodList> podsResource) {
342    Pod returnValue = null;
343    if (podsResource != null) {
344      returnValue = getFirstReadyPod(podsResource.list());
345    }
346    return returnValue;
347  }
348  
349}