001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2017-2018 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.InetAddress; 023import java.net.MalformedURLException; 024 025import java.util.Collections; 026import java.util.HashMap; 027import java.util.Map; 028import java.util.Objects; 029 030import hapi.services.tiller.ReleaseServiceGrpc; 031import hapi.services.tiller.ReleaseServiceGrpc.ReleaseServiceBlockingStub; 032import hapi.services.tiller.ReleaseServiceGrpc.ReleaseServiceFutureStub; 033import hapi.services.tiller.ReleaseServiceGrpc.ReleaseServiceStub; 034import hapi.services.tiller.Tiller.GetVersionResponse; 035 036import hapi.version.VersionOuterClass.VersionOrBuilder; 037 038import io.fabric8.kubernetes.client.Config; 039import io.fabric8.kubernetes.client.ConfigAware; 040import io.fabric8.kubernetes.client.DefaultKubernetesClient; // for javadoc only 041import io.fabric8.kubernetes.client.HttpClientAware; 042import io.fabric8.kubernetes.client.KubernetesClient; 043import io.fabric8.kubernetes.client.KubernetesClientException; // for javadoc only 044import io.fabric8.kubernetes.client.LocalPortForward; 045 046import io.grpc.ManagedChannel; 047import io.grpc.ManagedChannelBuilder; 048import io.grpc.Metadata; 049 050import io.grpc.stub.MetadataUtils; 051 052import okhttp3.OkHttpClient; 053 054import org.microbean.development.annotation.Issue; 055 056import org.microbean.kubernetes.Pods; 057 058/** 059 * A convenience class for communicating with a <a 060 * href="https://docs.helm.sh/glossary/#tiller" 061 * target="_parent">Tiller server</a>. 062 * 063 * @author <a href="https://about.me/lairdnelson" 064 * target="_parent">Laird Nelson</a> 065 * 066 * @see ReleaseServiceGrpc 067 */ 068public class Tiller implements ConfigAware<Config>, Closeable { 069 070 071 /* 072 * Static fields. 073 */ 074 075 076 /** 077 * The version of Tiller {@link Tiller} instances expect. 078 * 079 * <p>This field is never {@code null}.</p> 080 */ 081 public static final String VERSION = "2.8.1"; 082 083 /** 084 * The Kubernetes namespace into which Tiller server instances are 085 * most commonly installed. 086 * 087 * <p>This field is never {@code null}.</p> 088 */ 089 public static final String DEFAULT_NAMESPACE = "kube-system"; 090 091 /** 092 * The port on which Tiller server instances most commonly listen. 093 */ 094 public static final int DEFAULT_PORT = 44134; 095 096 /** 097 * The Kubernetes labels with which most Tiller instances are 098 * annotated. 099 * 100 * <p>This field is never {@code null}.</p> 101 */ 102 public static final Map<String, String> DEFAULT_LABELS; 103 104 /** 105 * A {@link Metadata} that ensures that certain Tiller-related 106 * headers are passed with every gRPC call. 107 * 108 * <p>This field is never {@code null}.</p> 109 */ 110 private static final Metadata metadata = new Metadata(); 111 112 113 /* 114 * Static initializer. 115 */ 116 117 118 /** 119 * Static initializer; initializes the {@link #DEFAULT_LABELS} 120 * {@code static} field (among others). 121 */ 122 static { 123 final Map<String, String> labels = new HashMap<>(); 124 labels.put("name", "tiller"); 125 labels.put("app", "helm"); 126 DEFAULT_LABELS = Collections.unmodifiableMap(labels); 127 metadata.put(Metadata.Key.of("x-helm-api-client", Metadata.ASCII_STRING_MARSHALLER), VERSION); 128 } 129 130 131 /* 132 * Instance fields. 133 */ 134 135 136 /** 137 * The {@link Config} available at construction time. 138 * 139 * <p>This field may be {@code null}.</p> 140 * 141 * @see #getConfiguration() 142 */ 143 private final Config config; 144 145 /** 146 * The {@link LocalPortForward} being used to communicate (most 147 * commonly) with a Kubernetes pod housing a Tiller server. 148 * 149 * <p>This field may be {@code null}.</p> 150 * 151 * @see #Tiller(LocalPortForward) 152 */ 153 private final LocalPortForward portForward; 154 155 /** 156 * The {@link ManagedChannel} over which communications with a 157 * Tiller server will be conducted. 158 * 159 * <p>This field is never {@code null}.</p> 160 */ 161 private final ManagedChannel channel; 162 163 164 /* 165 * Constructors. 166 */ 167 168 169 /** 170 * Creates a new {@link Tiller} that will use the supplied {@link 171 * ManagedChannel} for communication. 172 * 173 * @param channel the {@link ManagedChannel} over which 174 * communications will be conducted; must not be {@code null} 175 * 176 * @exception NullPointerException if {@code channel} is {@code 177 * null} 178 */ 179 public Tiller(final ManagedChannel channel) { 180 super(); 181 Objects.requireNonNull(channel); 182 this.config = null; 183 this.portForward = null; 184 this.channel = channel; 185 } 186 187 /** 188 * Creates a new {@link Tiller} that will use information from the 189 * supplied {@link LocalPortForward} to establish a communications 190 * channel with the Tiller server. 191 * 192 * @param portForward the {@link LocalPortForward} to use; must not 193 * be {@code null} 194 * 195 * @exception NullPointerException if {@code portForward} is {@code 196 * null} 197 */ 198 public Tiller(final LocalPortForward portForward) { 199 super(); 200 Objects.requireNonNull(portForward); 201 this.config = null; 202 this.portForward = null; // yes, null 203 this.channel = this.buildChannel(portForward); 204 } 205 206 /** 207 * Creates a new {@link Tiller} that will forward a local port to 208 * port {@code 44134} on a Pod housing Tiller in the {@code 209 * kube-system} namespace running in the Kubernetes cluster with 210 * which the supplied {@link KubernetesClient} is capable of 211 * communicating. 212 * 213 * <p>The {@linkplain Pods#getFirstReadyPod(Listable) first ready 214 * Pod} with a {@code name} label whose value is {@code tiller} and 215 * with an {@code app} label whose value is {@code helm} is deemed 216 * to be the pod housing the Tiller instance to connect to. (This 217 * duplicates the default logic of the {@code helm} command line 218 * executable.)</p> 219 * 220 * @param <T> a {@link KubernetesClient} implementation that is also 221 * an {@link HttpClientAware} implementation, such as {@link 222 * DefaultKubernetesClient} 223 * 224 * @param client the {@link KubernetesClient}-and-{@link 225 * HttpClientAware} implementation that can communicate with a 226 * Kubernetes cluster; must not be {@code null} 227 * 228 * @exception MalformedURLException if there was a problem 229 * identifying a Pod within the cluster that houses a Tiller instance 230 * 231 * @exception NullPointerException if {@code client} is {@code null} 232 */ 233 public <T extends HttpClientAware & KubernetesClient> Tiller(final T client) throws MalformedURLException { 234 this(client, DEFAULT_NAMESPACE, DEFAULT_PORT, DEFAULT_LABELS); 235 } 236 237 /** 238 * Creates a new {@link Tiller} that will forward a local port to 239 * port {@code 44134} on a Pod housing Tiller in the supplied 240 * namespace running in the Kubernetes cluster with which the 241 * supplied {@link KubernetesClient} is capable of communicating. 242 * 243 * <p>The {@linkplain Pods#getFirstReadyPod(Listable) first ready 244 * Pod} with a {@code name} label whose value is {@code tiller} and 245 * with an {@code app} label whose value is {@code helm} is deemed 246 * to be the pod housing the Tiller instance to connect to. (This 247 * duplicates the default logic of the {@code helm} command line 248 * executable.)</p> 249 * 250 * @param <T> a {@link KubernetesClient} implementation that is also 251 * an {@link HttpClientAware} implementation, such as {@link 252 * DefaultKubernetesClient} 253 * 254 * @param client the {@link KubernetesClient}-and-{@link 255 * HttpClientAware} implementation that can communicate with a 256 * Kubernetes cluster; must not be {@code null}; no reference to 257 * this object is retained by this {@link Tiller} instance 258 * 259 * @param namespaceHousingTiller the namespace within which a Tiller 260 * instance is hopefully running; if {@code null}, then the value of 261 * {@link #DEFAULT_NAMESPACE} will be used instead 262 * 263 * @exception MalformedURLException if there was a problem 264 * identifying a Pod within the cluster that houses a Tiller instance 265 * 266 * @exception NullPointerException if {@code client} is {@code null} 267 * 268 * @exception KubernetesClientException if there was a problem 269 * connecting to Kubernetes 270 * 271 * @exception TillerException if a ready Tiller pod could not be 272 * found and consequently a connection could not be established 273 */ 274 public <T extends HttpClientAware & KubernetesClient> Tiller(final T client, final String namespaceHousingTiller) throws MalformedURLException { 275 this(client, namespaceHousingTiller, DEFAULT_PORT, DEFAULT_LABELS); 276 } 277 278 /** 279 * Creates a new {@link Tiller} that will forward a local port to 280 * the supplied (remote) port on a Pod housing Tiller in the supplied 281 * namespace running in the Kubernetes cluster with which the 282 * supplied {@link KubernetesClient} is capable of communicating. 283 * 284 * <p>The {@linkplain Pods#getFirstReadyPod(Listable) first ready 285 * Pod} with labels matching the supplied {@code tillerLabels} is 286 * deemed to be the pod housing the Tiller instance to connect 287 * to.</p> 288 * 289 * @param <T> a {@link KubernetesClient} implementation that is also 290 * an {@link HttpClientAware} implementation, such as {@link 291 * DefaultKubernetesClient} 292 * 293 * @param client the {@link KubernetesClient}-and-{@link 294 * HttpClientAware} implementation that can communicate with a 295 * Kubernetes cluster; must not be {@code null}; no reference to 296 * this object is retained by this {@link Tiller} instance 297 * 298 * @param namespaceHousingTiller the namespace within which a Tiller 299 * instance is hopefully running; if {@code null}, then the value of 300 * {@link #DEFAULT_NAMESPACE} will be used instead 301 * 302 * @param tillerPort the remote port to attempt to forward a local 303 * port to; normally {@code 44134} 304 * 305 * @param tillerLabels a {@link Map} representing the Kubernetes 306 * labels (and their values) identifying a Pod housing a Tiller 307 * instance; if {@code null} then the value of {@link 308 * #DEFAULT_LABELS} will be used instead 309 * 310 * @exception MalformedURLException if there was a problem 311 * identifying a Pod within the cluster that houses a Tiller instance 312 * 313 * @exception NullPointerException if {@code client} is {@code null} 314 * 315 * @exception KubernetesClientException if there was a problem 316 * connecting to Kubernetes 317 * 318 * @exception TillerException if a ready Tiller pod could not be 319 * found and consequently a connection could not be established 320 */ 321 public <T extends HttpClientAware & KubernetesClient> Tiller(final T client, 322 String namespaceHousingTiller, 323 int tillerPort, 324 Map<String, String> tillerLabels) throws MalformedURLException { 325 super(); 326 Objects.requireNonNull(client); 327 this.config = client.getConfiguration(); 328 if (namespaceHousingTiller == null || namespaceHousingTiller.isEmpty()) { 329 namespaceHousingTiller = DEFAULT_NAMESPACE; 330 } 331 if (tillerPort <= 0) { 332 tillerPort = DEFAULT_PORT; 333 } 334 if (tillerLabels == null) { 335 tillerLabels = DEFAULT_LABELS; 336 } 337 final OkHttpClient httpClient = client.getHttpClient(); 338 if (httpClient == null) { 339 throw new IllegalArgumentException("client", new IllegalStateException("client.getHttpClient() == null")); 340 } 341 LocalPortForward portForward = null; 342 343 this.portForward = Pods.forwardPort(httpClient, client.pods().inNamespace(namespaceHousingTiller).withLabels(tillerLabels), tillerPort); 344 if (this.portForward == null) { 345 throw new TillerException("Could not forward port to a Ready Tiller pod's port " + tillerPort + " in namespace " + namespaceHousingTiller + " with labels " + tillerLabels); 346 } 347 this.channel = this.buildChannel(this.portForward); 348 } 349 350 351 /* 352 * Instance methods. 353 */ 354 355 356 /** 357 * Returns any {@link Config} available at construction time. 358 * 359 * <p>This method may return {@code null}.</p> 360 * 361 * @return a {@link Config}, or {@code null} 362 */ 363 @Override 364 public Config getConfiguration() { 365 return this.config; 366 } 367 368 369 /** 370 * Creates a {@link ManagedChannel} for communication with Tiller 371 * from the information contained in the supplied {@link 372 * LocalPortForward}. 373 * 374 * <p><strong>Note:</strong> This method is (deliberately) called 375 * from constructors so must have stateless semantics.</p> 376 * 377 * <p>This method never returns {@code null}.</p> 378 * 379 * <p>Overrides of this method must not return {@code null}.</p> 380 * 381 * @param portForward a {@link LocalPortForward}; must not be {@code 382 * null} 383 * 384 * @return a non-{@code null} {@link ManagedChannel} 385 * 386 * @exception NullPointerException if {@code portForward} is {@code 387 * null} 388 * 389 * @exception IllegalArgumentException if {@code portForward}'s 390 * {@link LocalPortForward#getLocalAddress()} method returns {@code 391 * null} 392 */ 393 @Issue(id = "42", uri = "https://github.com/microbean/microbean-helm/issues/42") 394 protected ManagedChannel buildChannel(final LocalPortForward portForward) { 395 Objects.requireNonNull(portForward); 396 @Issue(id = "43", uri = "https://github.com/microbean/microbean-helm/issues/43") 397 final InetAddress localAddress = portForward.getLocalAddress(); 398 if (localAddress == null) { 399 throw new IllegalArgumentException("portForward", new IllegalStateException("portForward.getLocalAddress() == null")); 400 } 401 final String hostAddress = localAddress.getHostAddress(); 402 if (hostAddress == null) { 403 throw new IllegalArgumentException("portForward", new IllegalStateException("portForward.getLocalAddress().getHostAddress() == null")); 404 } 405 return ManagedChannelBuilder.forAddress(hostAddress, portForward.getLocalPort()).usePlaintext(true).build(); 406 } 407 408 /** 409 * Closes this {@link Tiller} after use; any {@link 410 * LocalPortForward} or {@link ManagedChannel} <strong>used or 411 * created</strong> by or for this {@link Tiller} instance will be 412 * closed or {@linkplain ManagedChannel#shutdown() shut down} 413 * appropriately. 414 * 415 * @exception IOException if there was a problem closing the 416 * underlying connection to a Tiller instance 417 * 418 * @see LocalPortForward#close() 419 * 420 * @see ManagedChannel#shutdown() 421 */ 422 @Override 423 public void close() throws IOException { 424 if (this.channel != null) { 425 this.channel.shutdown(); 426 } 427 if (this.portForward != null) { 428 this.portForward.close(); 429 } 430 } 431 432 /** 433 * Returns the gRPC-generated {@link ReleaseServiceBlockingStub} 434 * object that represents the capabilities of the Tiller server. 435 * 436 * <p>This method will never return {@code null}.</p> 437 * 438 * <p>Overrides of this method must never return {@code null}.</p> 439 * 440 * @return a non-{@code null} {@link ReleaseServiceBlockingStub} 441 * 442 * @see ReleaseServiceBlockingStub 443 */ 444 public ReleaseServiceBlockingStub getReleaseServiceBlockingStub() { 445 ReleaseServiceBlockingStub returnValue = null; 446 if (this.channel != null) { 447 returnValue = MetadataUtils.attachHeaders(ReleaseServiceGrpc.newBlockingStub(this.channel), metadata); 448 } 449 return returnValue; 450 } 451 452 /** 453 * Returns the gRPC-generated {@link ReleaseServiceFutureStub} 454 * object that represents the capabilities of the Tiller server. 455 * 456 * <p>This method will never return {@code null}.</p> 457 * 458 * <p>Overrides of this method must never return {@code null}.</p> 459 * 460 * @return a non-{@code null} {@link ReleaseServiceFutureStub} 461 * 462 * @see ReleaseServiceFutureStub 463 */ 464 public ReleaseServiceFutureStub getReleaseServiceFutureStub() { 465 ReleaseServiceFutureStub returnValue = null; 466 if (this.channel != null) { 467 returnValue = MetadataUtils.attachHeaders(ReleaseServiceGrpc.newFutureStub(this.channel), metadata); 468 } 469 return returnValue; 470 } 471 472 /** 473 * Returns the gRPC-generated {@link ReleaseServiceStub} 474 * object that represents the capabilities of the Tiller server. 475 * 476 * <p>This method will never return {@code null}.</p> 477 * 478 * <p>Overrides of this method must never return {@code null}.</p> 479 * 480 * @return a non-{@code null} {@link ReleaseServiceStub} 481 * 482 * @see ReleaseServiceStub 483 */ 484 public ReleaseServiceStub getReleaseServiceStub() { 485 ReleaseServiceStub returnValue = null; 486 if (this.channel != null) { 487 returnValue = MetadataUtils.attachHeaders(ReleaseServiceGrpc.newStub(this.channel), metadata); 488 } 489 return returnValue; 490 } 491 492 public VersionOrBuilder getVersion() throws IOException { 493 final ReleaseServiceBlockingStub stub = this.getReleaseServiceBlockingStub(); 494 assert stub != null; 495 final GetVersionResponse response = stub.getVersion(null); 496 assert response != null; 497 return response.getVersion(); 498 } 499 500}