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.5.0"; 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}