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