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.helm;
018
019import java.io.Closeable;
020import java.io.IOException;
021
022import java.net.MalformedURLException;
023
024import java.util.Collections;
025import java.util.HashMap;
026import java.util.Map;
027import java.util.Objects;
028
029import hapi.services.tiller.ReleaseServiceGrpc;
030import hapi.services.tiller.ReleaseServiceGrpc.ReleaseServiceBlockingStub;
031import hapi.services.tiller.ReleaseServiceGrpc.ReleaseServiceFutureStub;
032import hapi.services.tiller.ReleaseServiceGrpc.ReleaseServiceStub;
033
034import io.fabric8.kubernetes.client.DefaultKubernetesClient; // for javadoc only
035import io.fabric8.kubernetes.client.HttpClientAware;
036import io.fabric8.kubernetes.client.KubernetesClient;
037import io.fabric8.kubernetes.client.LocalPortForward;
038
039import io.grpc.ManagedChannel;
040import io.grpc.ManagedChannelBuilder;
041import io.grpc.Metadata;
042
043import io.grpc.stub.MetadataUtils;
044
045import org.microbean.kubernetes.Pods;
046
047/**
048 * A convenience class for communicating with a <a
049 * href="https://docs.helm.sh/glossary/#tiller"
050 * target="_parent">Tiller server</a>.
051 *
052 * @author <a href="https://about.me/lairdnelson"
053 * target="_parent">Laird Nelson</a>
054 *
055 * @see ReleaseServiceGrpc
056 */
057public class Tiller implements Closeable {
058
059
060  /*
061   * Static fields.
062   */
063
064
065  /**
066   * The version of Tiller {@link Tiller} instances expect.
067   *
068   * <p>This field is never {@code null}.</p>
069   */
070  public static final String VERSION = "2.4.2";
071
072  /**
073   * The Kubernetes namespace into which Tiller server instances are
074   * most commonly installed.
075   *
076   * <p>This field is never {@code null}.</p>
077   */
078  public static final String DEFAULT_NAMESPACE = "kube-system";
079
080  /**
081   * The port on which Tiller server instances most commonly listen.
082   */
083  public static final int DEFAULT_PORT = 44134;
084
085  /**
086   * The Kubernetes labels with which most Tiller instances are
087   * annotated.
088   *
089   * <p>This field is never {@code null}.</p>
090   */
091  public static final Map<String, String> DEFAULT_LABELS;
092  
093  /**
094   * A {@link Metadata} that ensures that certain Tiller-related
095   * headers are passed with every gRPC call.
096   *
097   * <p>This field is never {@code null}.</p>
098   */
099  private static final Metadata metadata = new Metadata();
100
101
102  /*
103   * Static initializer.
104   */
105  
106
107  /**
108   * Static initializer; initializes the {@link #DEFAULT_LABELS}
109   * {@code static} field (among others).
110   */
111  static {
112    final Map<String, String> labels = new HashMap<>();
113    labels.put("name", "tiller");
114    labels.put("app", "helm");
115    DEFAULT_LABELS = Collections.unmodifiableMap(labels);
116    metadata.put(Metadata.Key.of("x-helm-api-client", Metadata.ASCII_STRING_MARSHALLER), VERSION);
117  }
118
119
120  /*
121   * Instance fields.
122   */
123  
124
125  /**
126   * The {@link LocalPortForward} being used to communicate (most
127   * commonly) with a Kubernetes pod housing a Tiller server.
128   *
129   * <p>This field may be {@code null}.</p>
130   *
131   * @see #Tiller(LocalPortForward)
132   */
133  private final LocalPortForward portForward;
134
135  /**
136   * The {@link ManagedChannel} over which communications with a
137   * Tiller server will be conducted.
138   *
139   * <p>This field is never {@code null}.</p>
140   */
141  private final ManagedChannel channel;
142
143
144  /*
145   * Constructors.
146   */
147
148
149  /**
150   * Creates a new {@link Tiller} that will use the supplied {@link
151   * ManagedChannel} for communication.
152   *
153   * @param channel the {@link ManagedChannel} over which
154   * communications will be conducted; must not be {@code null}
155   *
156   * @exception NullPointerException if {@code channel} is {@code
157   * null}
158   */
159  public Tiller(final ManagedChannel channel) {
160    super();
161    Objects.requireNonNull(channel);
162    this.portForward = null;
163    this.channel = channel;
164  }
165
166  /**
167   * Creates a new {@link Tiller} that will use information from the
168   * supplied {@link LocalPortForward} to establish a communications
169   * channel with the Tiller server.
170   *
171   * @param portForward the {@link LocalPortForward} to use; must not
172   * be {@code null}
173   *
174   * @exception NullPointerException if {@code portForward} is {@code
175   * null}
176   */
177  public Tiller(final LocalPortForward portForward) {
178    super();
179    Objects.requireNonNull(portForward);
180    this.portForward = null; // yes, null
181    this.channel = this.buildChannel(portForward);
182  }
183
184  /**
185   * Creates a new {@link Tiller} that will forward a local port to
186   * port {@code 44134} on a Pod housing Tiller in the {@code
187   * kube-system} namespace running in the Kubernetes cluster with
188   * which the supplied {@link KubernetesClient} is capable of
189   * communicating.
190   *
191   * <p>The {@linkplain Pods#getFirstReadyPod(Listable) first ready
192   * Pod} with a {@code name} label whose value is {@code tiller} and
193   * with an {@code app} label whose value is {@code helm} is deemed
194   * to be the pod housing the Tiller instance to connect to.  (This
195   * duplicates the default logic of the {@code helm} command line
196   * executable.)</p>
197   *
198   * @param <T> a {@link KubernetesClient} implementation that is also
199   * an {@link HttpClientAware} implementation, such as {@link
200   * DefaultKubernetesClient}
201   *
202   * @param client the {@link KubernetesClient}-and-{@link
203   * HttpClientAware} implementation that can communicate with a
204   * Kubernetes cluster; must not be {@code null}
205   *
206   * @exception MalformedURLException if there was a problem
207   * identifying a Pod within the cluster that houses a Tiller instance
208   *
209   * @exception NullPointerException if {@code client} is {@code null}
210   */
211  public <T extends HttpClientAware & KubernetesClient> Tiller(final T client) throws MalformedURLException {
212    this(client, DEFAULT_NAMESPACE, DEFAULT_PORT, DEFAULT_LABELS);
213  }
214
215  /**
216   * Creates a new {@link Tiller} that will forward a local port to
217   * port {@code 44134} on a Pod housing Tiller in the supplied
218   * namespace running in the Kubernetes cluster with which the
219   * supplied {@link KubernetesClient} is capable of communicating.
220   *
221   * <p>The {@linkplain Pods#getFirstReadyPod(Listable) first ready
222   * Pod} with a {@code name} label whose value is {@code tiller} and
223   * with an {@code app} label whose value is {@code helm} is deemed
224   * to be the pod housing the Tiller instance to connect to.  (This
225   * duplicates the default logic of the {@code helm} command line
226   * executable.)</p>
227   *
228   * @param <T> a {@link KubernetesClient} implementation that is also
229   * an {@link HttpClientAware} implementation, such as {@link
230   * DefaultKubernetesClient}
231   *
232   * @param client the {@link KubernetesClient}-and-{@link
233   * HttpClientAware} implementation that can communicate with a
234   * Kubernetes cluster; must not be {@code null}
235   *
236   * @param namespaceHousingTiller the namespace within which a Tiller
237   * instance is hopefully running; if {@code null}, then the value of
238   * {@link #DEFAULT_NAMESPACE} will be used instead
239   *
240   * @exception MalformedURLException if there was a problem
241   * identifying a Pod within the cluster that houses a Tiller instance
242   *
243   * @exception NullPointerException if {@code client} is {@code null}
244   */
245  public <T extends HttpClientAware & KubernetesClient> Tiller(final T client, final String namespaceHousingTiller) throws MalformedURLException {
246    this(client, namespaceHousingTiller, DEFAULT_PORT, DEFAULT_LABELS);
247  }
248
249  /**
250   * Creates a new {@link Tiller} that will forward a local port to
251   * the supplied (remote) port on a Pod housing Tiller in the supplied
252   * namespace running in the Kubernetes cluster with which the
253   * supplied {@link KubernetesClient} is capable of communicating.
254   *
255   * <p>The {@linkplain Pods#getFirstReadyPod(Listable) first ready
256   * Pod} with labels matching the supplied {@code tillerLabels} is
257   * deemed to be the pod housing the Tiller instance to connect
258   * to.</p>
259   *
260   * @param <T> a {@link KubernetesClient} implementation that is also
261   * an {@link HttpClientAware} implementation, such as {@link
262   * DefaultKubernetesClient}
263   *
264   * @param client the {@link KubernetesClient}-and-{@link
265   * HttpClientAware} implementation that can communicate with a
266   * Kubernetes cluster; must not be {@code null}
267   *
268   * @param namespaceHousingTiller the namespace within which a Tiller
269   * instance is hopefully running; if {@code null}, then the value of
270   * {@link #DEFAULT_NAMESPACE} will be used instead
271   *
272   * @param tillerPort the remote port to attempt to forward a local
273   * port to; normally {@code 44134}
274   *
275   * @param tillerLabels a {@link Map} representing the Kubernetes
276   * labels (and their values) identifying a Pod housing a Tiller
277   * instance; if {@code null} then the value of {@link
278   * #DEFAULT_LABELS} will be used instead
279   *
280   * @exception MalformedURLException if there was a problem
281   * identifying a Pod within the cluster that houses a Tiller instance
282   *
283   * @exception NullPointerException if {@code client} is {@code null}
284   */
285  public <T extends HttpClientAware & KubernetesClient> Tiller(final T client,
286                                                               String namespaceHousingTiller,
287                                                               int tillerPort,
288                                                               Map<String, String> tillerLabels) throws MalformedURLException {
289    super();
290    Objects.requireNonNull(client);
291    if (namespaceHousingTiller == null || namespaceHousingTiller.isEmpty()) {
292      namespaceHousingTiller = DEFAULT_NAMESPACE;
293    }
294    if (tillerPort <= 0) {
295      tillerPort = DEFAULT_PORT;
296    }
297    if (tillerLabels == null) {
298      tillerLabels = DEFAULT_LABELS;
299    }
300    this.portForward = Pods.forwardPort(client.getHttpClient(), client.pods().inNamespace(namespaceHousingTiller).withLabels(tillerLabels), tillerPort);
301    this.channel = this.buildChannel(this.portForward);
302  }
303
304
305  /*
306   * Instance methods.
307   */
308  
309
310  /**
311   * Creates a {@link ManagedChannel} for communication with Tiller
312   * from the information contained in the supplied {@link
313   * LocalPortForward}.
314   *
315   * <p><strong>Note:</strong> This method is (deliberately) called
316   * from constructors so must have stateless semantics.</p>
317   *
318   * <p>This method never returns {@code null}.</p>
319   *
320   * <p>Overrides of this method must not return {@code null}.</p>
321   *
322   * @param portForward a {@link LocalPortForward}; must not be {@code
323   * null}
324   *
325   * @return a non-{@code null} {@link ManagedChannel}
326   *
327   * @exception NullPointerException if {@code portForward} is {@code
328   * null}
329   */
330  protected ManagedChannel buildChannel(final LocalPortForward portForward) {
331    return ManagedChannelBuilder.forAddress(portForward.getLocalAddress().getHostAddress(), portForward.getLocalPort()).usePlaintext(true).build();
332  }
333
334  /**
335   * Closes this {@link Tiller} after use.
336   *
337   * @exception IOException if there was a problem closing the
338   * underlying connection to a Tiller instance
339   */
340  @Override
341  public void close() throws IOException {
342    if (this.channel != null) {
343      this.channel.shutdownNow();
344    }
345    if (this.portForward != null) {
346      this.portForward.close();
347    }
348  }
349
350  /**
351   * Returns the gRPC-generated {@link ReleaseServiceBlockingStub}
352   * object that represents the capabilities of the Tiller server.
353   *
354   * <p>This method will never return {@code null}.</p>
355   *
356   * <p>Overrides of this method must never return {@code null}.</p>
357   *
358   * @return a non-{@code null} {@link ReleaseServiceBlockingStub}
359   *
360   * @see ReleaseServiceBlockingStub
361   */
362  public ReleaseServiceBlockingStub getReleaseServiceBlockingStub() {
363    ReleaseServiceBlockingStub returnValue = null;
364    if (this.channel != null) {
365      returnValue = MetadataUtils.attachHeaders(ReleaseServiceGrpc.newBlockingStub(this.channel), metadata);
366    }
367    return returnValue;
368  }
369
370  /**
371   * Returns the gRPC-generated {@link ReleaseServiceFutureStub}
372   * object that represents the capabilities of the Tiller server.
373   *
374   * <p>This method will never return {@code null}.</p>
375   *
376   * <p>Overrides of this method must never return {@code null}.</p>
377   *
378   * @return a non-{@code null} {@link ReleaseServiceFutureStub}
379   *
380   * @see ReleaseServiceFutureStub
381   */
382  public ReleaseServiceFutureStub getReleaseServiceFutureStub() {
383    ReleaseServiceFutureStub returnValue = null;
384    if (this.channel != null) {
385      returnValue = MetadataUtils.attachHeaders(ReleaseServiceGrpc.newFutureStub(this.channel), metadata);
386    }
387    return returnValue;
388  }
389
390  /**
391   * Returns the gRPC-generated {@link ReleaseServiceStub}
392   * object that represents the capabilities of the Tiller server.
393   *
394   * <p>This method will never return {@code null}.</p>
395   *
396   * <p>Overrides of this method must never return {@code null}.</p>
397   *
398   * @return a non-{@code null} {@link ReleaseServiceStub}
399   *
400   * @see ReleaseServiceStub
401   */  
402  public ReleaseServiceStub getReleaseServiceStub() {
403    ReleaseServiceStub returnValue = null;
404    if (this.channel != null) {
405      returnValue = MetadataUtils.attachHeaders(ReleaseServiceGrpc.newStub(this.channel), metadata);
406    }
407    return returnValue;
408  }
409
410}