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