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.BufferedInputStream; 020import java.io.ByteArrayOutputStream; 021import java.io.InputStream; 022import java.io.IOException; 023 024import java.net.URI; 025import java.net.URL; 026 027import java.util.Arrays; 028import java.util.ArrayList; 029import java.util.Base64; 030import java.util.HashMap; 031import java.util.List; 032import java.util.Map; 033import java.util.Objects; 034 035import io.fabric8.kubernetes.client.DefaultKubernetesClient; 036import io.fabric8.kubernetes.client.KubernetesClient; 037import io.fabric8.kubernetes.client.KubernetesClientException; 038 039import io.fabric8.kubernetes.api.model.Container; 040import io.fabric8.kubernetes.api.model.ContainerPort; 041import io.fabric8.kubernetes.api.model.EnvVar; 042import io.fabric8.kubernetes.api.model.HTTPGetAction; 043import io.fabric8.kubernetes.api.model.IntOrString; 044import io.fabric8.kubernetes.api.model.ObjectMeta; 045import io.fabric8.kubernetes.api.model.PodSpec; 046import io.fabric8.kubernetes.api.model.PodTemplateSpec; 047import io.fabric8.kubernetes.api.model.Probe; 048import io.fabric8.kubernetes.api.model.Secret; 049import io.fabric8.kubernetes.api.model.SecretVolumeSource; 050import io.fabric8.kubernetes.api.model.Service; 051import io.fabric8.kubernetes.api.model.ServicePort; 052import io.fabric8.kubernetes.api.model.ServiceSpec; 053import io.fabric8.kubernetes.api.model.Status; 054import io.fabric8.kubernetes.api.model.Volume; 055import io.fabric8.kubernetes.api.model.VolumeMount; 056 057import io.fabric8.kubernetes.api.model.extensions.Deployment; 058import io.fabric8.kubernetes.api.model.extensions.DeploymentSpec; 059import io.fabric8.kubernetes.api.model.extensions.DeploymentBuilder; 060 061/** 062 * A class that idiomatically but faithfully emulates the 063 * Tiller-installing behavior of the {@code helm init} command. 064 * 065 * <p>In general, this class follows the logic as expressed in <a 066 * href="https://github.com/kubernetes/helm/blob/master/cmd/helm/installer/install.go">the 067 * {@code install.go} source code from the Helm project</a>, 068 * problematic or not. The intent is to have an installer, usable as 069 * an idiomatic Java library, that behaves just like {@code helm 070 * init}.</p> 071 * 072 * <p><strong>Note:</strong> This class is experimental and its API is 073 * subject to change without notice.</p> 074 * 075 * @author <a href="https://about.me/lairdnelson" 076 * target="_parent">Laird Nelson</a> 077 * 078 * @see #init() 079 * 080 * @see <a 081 * href="https://github.com/kubernetes/helm/blob/master/cmd/helm/installer/install.go">The 082 * {@code install.go} source code from the Helm project</a> 083 */ 084public class TillerInstaller { 085 086 087 /* 088 * Static fields. 089 */ 090 091 092 /* 093 * Atomic static fields. 094 */ 095 096 private static final Integer ONE = Integer.valueOf(1); 097 098 private static final String DEFAULT_IMAGE_PULL_POLICY = "IfNotPresent"; 099 100 private static final String DEFAULT_NAME = "tiller"; 101 102 private static final String DEFAULT_NAMESPACE = "kube-system"; 103 104 private static final String TILLER_TLS_CERTS_PATH = "/etc/certs"; 105 106 /** 107 * The version of Tiller to install. 108 */ 109 public static final String VERSION = "2.4.2"; 110 111 /* 112 * Derivative static fields. 113 */ 114 115 private static final String DEFAULT_IMAGE_NAME = "gcr.io/kubernetes-helm/" + DEFAULT_NAME + ":v" + VERSION; 116 117 private static final String DEFAULT_DEPLOYMENT_NAME = DEFAULT_NAME + "-deploy"; 118 119 private static final String SECRET_NAME = DEFAULT_NAME + "-secret"; 120 121 122 /* 123 * Instance fields. 124 */ 125 126 127 private final KubernetesClient kubernetesClient; 128 129 private final String tillerNamespace; 130 131 132 /* 133 * Constructors. 134 */ 135 136 137 public TillerInstaller() { 138 this(new DefaultKubernetesClient()); 139 } 140 141 public TillerInstaller(final KubernetesClient kubernetesClient) { 142 super(); 143 Objects.requireNonNull(kubernetesClient); 144 this.kubernetesClient = kubernetesClient; 145 final String tillerNamespace = System.getenv("TILLER_NAMESPACE"); 146 if (tillerNamespace == null || tillerNamespace.isEmpty()) { 147 this.tillerNamespace = DEFAULT_NAMESPACE; 148 } else { 149 this.tillerNamespace = tillerNamespace; 150 } 151 } 152 153 154 /* 155 * Instance methods. 156 */ 157 158 159 public void init() { 160 try { 161 this.init(false, null, null, null, null, null, null, null, false, false, false, null, null, null); 162 } catch (final IOException willNotHappen) { 163 throw new AssertionError(willNotHappen); 164 } 165 } 166 167 public void init(final boolean upgrade) { 168 try { 169 this.init(upgrade, null, null, null, null, null, null, null, false, false, false, null, null, null); 170 } catch (final IOException willNotHappen) { 171 throw new AssertionError(willNotHappen); 172 } 173 } 174 175 public void init(final boolean upgrade, 176 String namespace, 177 final String deploymentName, 178 final String serviceName, 179 Map<String, String> labels, 180 final String serviceAccountName, 181 final String imageName, 182 final String imagePullPolicy, 183 final boolean hostNetwork, 184 final boolean tls, 185 final boolean verifyTls, 186 final URI tlsKeyUri, 187 final URI tlsCertUri, 188 final URI tlsCaCertUri) 189 throws IOException { 190 namespace = normalizeNamespace(namespace); 191 labels = normalizeLabels(labels); 192 try { 193 this.install(namespace, 194 deploymentName, 195 serviceName, 196 labels, 197 serviceAccountName, 198 imageName, 199 imagePullPolicy, 200 hostNetwork, 201 tls, 202 verifyTls, 203 tlsKeyUri, 204 tlsCertUri, 205 tlsCaCertUri); 206 } catch (final KubernetesClientException kubernetesClientException) { 207 final Status status = kubernetesClientException.getStatus(); 208 if (status == null || !"AlreadyExists".equals(status.getReason())) { 209 throw kubernetesClientException; 210 } else if (upgrade) { 211 this.upgrade(namespace, 212 deploymentName, 213 serviceName, 214 serviceAccountName, 215 imageName, 216 imagePullPolicy, 217 labels); 218 } 219 } 220 } 221 222 public void install() { 223 try { 224 this.install(null, null, null, null, null, null, null, false, false, false, null, null, null); 225 } catch (final IOException willNotHappen) { 226 throw new AssertionError(willNotHappen); 227 } 228 } 229 230 public void install(String namespace, 231 final String deploymentName, 232 final String serviceName, 233 Map<String, String> labels, 234 final String serviceAccountName, 235 final String imageName, 236 final String imagePullPolicy, 237 final boolean hostNetwork, 238 final boolean tls, 239 final boolean verifyTls, 240 final URI tlsKeyUri, 241 final URI tlsCertUri, 242 final URI tlsCaCertUri) 243 throws IOException { 244 namespace = normalizeNamespace(namespace); 245 labels = normalizeLabels(labels); 246 final Deployment deployment = 247 this.createDeployment(namespace, 248 deploymentName, 249 labels, 250 serviceAccountName, 251 imageName, 252 imagePullPolicy, 253 hostNetwork, 254 tls, 255 verifyTls); 256 257 this.kubernetesClient.extensions().deployments().inNamespace(namespace).create(deployment); 258 259 final Service service = this.createService(namespace, serviceName, labels); 260 this.kubernetesClient.services().inNamespace(namespace).create(service); 261 262 if (tls) { 263 final Secret secret = 264 this.createSecret(namespace, 265 tlsKeyUri, 266 tlsCertUri, 267 tlsCaCertUri, 268 labels); 269 this.kubernetesClient.secrets().inNamespace(namespace).create(secret); 270 } 271 272 } 273 274 public void upgrade() { 275 this.upgrade(null, null, null, null, null, null, null); 276 } 277 278 public void upgrade(String namespace, 279 final String deploymentName, 280 String serviceName, 281 final String serviceAccountName, 282 String imageName, 283 String imagePullPolicy, 284 final Map<String, String> labels) { 285 namespace = normalizeNamespace(namespace); 286 serviceName = normalizeServiceName(serviceName); 287 288 this.kubernetesClient.extensions() 289 .deployments() 290 .inNamespace(namespace) 291 .withName(normalizeDeploymentName(deploymentName)) 292 .edit() 293 .editSpec() 294 .editTemplate() 295 .editSpec() 296 .editContainer(0) 297 .withImage(normalizeImageName(imageName)) 298 .withImagePullPolicy(normalizeImagePullPolicy(imagePullPolicy)) 299 .and() 300 .withServiceAccountName(normalizeServiceAccountName(serviceAccountName)) 301 .endSpec() 302 .endTemplate() 303 .endSpec() 304 .done(); 305 306 // TODO: this way of emulating install.go's check to see if the 307 // Service exists...not sure it's right 308 final Service service = this.kubernetesClient.services() 309 .inNamespace(namespace) 310 .withName(serviceName) 311 .get(); 312 if (service == null) { 313 this.createService(namespace, serviceName, normalizeLabels(labels)); 314 } 315 316 } 317 318 protected Service createService(String namespace, 319 String serviceName, 320 Map<String, String> labels) { 321 labels = normalizeLabels(labels); 322 323 final Service service = new Service(); 324 325 final ObjectMeta metadata = new ObjectMeta(); 326 metadata.setNamespace(normalizeNamespace(namespace)); 327 metadata.setName(normalizeServiceName(serviceName)); 328 metadata.setLabels(labels); 329 service.setMetadata(metadata); 330 331 service.setSpec(this.createServiceSpec(labels)); 332 return service; 333 } 334 335 protected Deployment createDeployment(String namespace, 336 String deploymentName, 337 Map<String, String> labels, 338 final String serviceAccountName, 339 final String imageName, 340 final String imagePullPolicy, 341 final boolean hostNetwork, 342 final boolean tls, 343 final boolean verifyTls) { 344 labels = normalizeLabels(labels); 345 346 final Deployment deployment = new Deployment(); 347 348 final ObjectMeta metadata = new ObjectMeta(); 349 metadata.setNamespace(normalizeNamespace(namespace)); 350 metadata.setName(normalizeDeploymentName(deploymentName)); 351 metadata.setLabels(labels); 352 deployment.setMetadata(metadata); 353 354 deployment.setSpec(this.createDeploymentSpec(labels, serviceAccountName, imageName, imagePullPolicy, namespace, hostNetwork, tls, verifyTls)); 355 return deployment; 356 } 357 358 protected Secret createSecret(String namespace, 359 final URI tlsKeyUri, 360 final URI tlsCertUri, 361 final URI tlsCaCertUri, 362 Map<String, String> labels) 363 throws IOException { 364 labels = normalizeLabels(labels); 365 366 final Secret secret = new Secret(); 367 secret.setType("Opaque"); 368 369 final Map<String, String> secretData = new HashMap<>(); 370 371 try (final InputStream tlsKeyStream = read(tlsKeyUri)) { 372 if (tlsKeyStream != null) { 373 secretData.put("tls.key", Base64.getEncoder().encodeToString(toByteArray(tlsKeyStream))); 374 } 375 } 376 377 try (final InputStream tlsCertStream = read(tlsCertUri)) { 378 if (tlsCertStream != null) { 379 secretData.put("tls.crt", Base64.getEncoder().encodeToString(toByteArray(tlsCertStream))); 380 } 381 } 382 383 try (final InputStream tlsCaCertStream = read(tlsCaCertUri)) { 384 if (tlsCaCertStream != null) { 385 secretData.put("ca.crt", Base64.getEncoder().encodeToString(toByteArray(tlsCaCertStream))); 386 } 387 } 388 389 secret.setData(secretData); 390 391 final ObjectMeta metadata = new ObjectMeta(); 392 metadata.setNamespace(normalizeNamespace(namespace)); 393 metadata.setName(SECRET_NAME); 394 metadata.setLabels(labels); 395 secret.setMetadata(metadata); 396 397 return secret; 398 } 399 400 protected DeploymentSpec createDeploymentSpec(Map<String, String> labels, 401 final String serviceAccountName, 402 final String imageName, 403 final String imagePullPolicy, 404 final String namespace, 405 final boolean hostNetwork, 406 final boolean tls, 407 final boolean verifyTls) { 408 labels = normalizeLabels(labels); 409 final DeploymentSpec deploymentSpec = new DeploymentSpec(); 410 final PodTemplateSpec podTemplateSpec = new PodTemplateSpec(); 411 final ObjectMeta metadata = new ObjectMeta(); 412 metadata.setLabels(labels); 413 podTemplateSpec.setMetadata(metadata); 414 final PodSpec podSpec = new PodSpec(); 415 podSpec.setServiceAccountName(normalizeServiceAccountName(serviceAccountName)); 416 podSpec.setContainers(Arrays.asList(this.createContainer(imageName, imagePullPolicy, namespace, tls, verifyTls))); 417 podSpec.setHostNetwork(Boolean.valueOf(hostNetwork)); 418 final Map<String, String> nodeSelector = new HashMap<>(); 419 nodeSelector.put("beta.kubernetes.io/os", "linux"); 420 podSpec.setNodeSelector(nodeSelector); 421 if (tls) { 422 final Volume volume = new Volume(); 423 volume.setName(DEFAULT_NAME + "-certs"); 424 final SecretVolumeSource secretVolumeSource = new SecretVolumeSource(); 425 secretVolumeSource.setSecretName(SECRET_NAME); 426 volume.setSecret(secretVolumeSource); 427 podSpec.setVolumes(Arrays.asList(volume)); 428 } 429 podTemplateSpec.setSpec(podSpec); 430 deploymentSpec.setTemplate(podTemplateSpec); 431 return deploymentSpec; 432 } 433 434 protected Container createContainer(final String imageName, 435 final String imagePullPolicy, 436 final String namespace, 437 final boolean tls, 438 final boolean verifyTls) { 439 final Container container = new Container(); 440 container.setName(DEFAULT_NAME); 441 container.setImage(normalizeImageName(imageName)); 442 container.setImagePullPolicy(normalizeImagePullPolicy(imagePullPolicy)); 443 444 final ContainerPort containerPort = new ContainerPort(); 445 containerPort.setContainerPort(Integer.valueOf(44134)); 446 containerPort.setName(DEFAULT_NAME); 447 container.setPorts(Arrays.asList(containerPort)); 448 449 final List<EnvVar> env = new ArrayList<>(); 450 451 final EnvVar tillerNamespace = new EnvVar(); 452 tillerNamespace.setName("TILLER_NAMESPACE"); 453 tillerNamespace.setValue(normalizeNamespace(namespace)); 454 env.add(tillerNamespace); 455 456 if (tls) { 457 final EnvVar tlsVerify = new EnvVar(); 458 tlsVerify.setName("TILLER_TLS_VERIFY"); 459 tlsVerify.setValue(verifyTls ? "1" : ""); 460 env.add(tlsVerify); 461 462 final EnvVar tlsEnable = new EnvVar(); 463 tlsEnable.setName("TILLER_TLS_ENABLE"); 464 tlsEnable.setValue("1"); 465 env.add(tlsEnable); 466 467 final EnvVar tlsCerts = new EnvVar(); 468 tlsCerts.setName("TILLER_TLS_CERTS"); 469 tlsCerts.setValue(TILLER_TLS_CERTS_PATH); 470 env.add(tlsCerts); 471 } 472 473 container.setEnv(env); 474 475 476 final IntOrString port44135 = new IntOrString(Integer.valueOf(44135)); 477 478 final HTTPGetAction livenessHttpGetAction = new HTTPGetAction(); 479 livenessHttpGetAction.setPath("/liveness"); 480 livenessHttpGetAction.setPort(port44135); 481 final Probe livenessProbe = new Probe(); 482 livenessProbe.setHttpGet(livenessHttpGetAction); 483 livenessProbe.setInitialDelaySeconds(ONE); 484 livenessProbe.setTimeoutSeconds(ONE); 485 container.setLivenessProbe(livenessProbe); 486 487 final HTTPGetAction readinessHttpGetAction = new HTTPGetAction(); 488 readinessHttpGetAction.setPath("/readiness"); 489 readinessHttpGetAction.setPort(port44135); 490 final Probe readinessProbe = new Probe(); 491 readinessProbe.setHttpGet(readinessHttpGetAction); 492 readinessProbe.setInitialDelaySeconds(ONE); 493 readinessProbe.setTimeoutSeconds(ONE); 494 container.setReadinessProbe(readinessProbe); 495 496 if (tls) { 497 final VolumeMount volumeMount = new VolumeMount(); 498 volumeMount.setName(DEFAULT_NAME + "-certs"); 499 volumeMount.setReadOnly(true); 500 volumeMount.setMountPath(TILLER_TLS_CERTS_PATH); 501 container.setVolumeMounts(Arrays.asList(volumeMount)); 502 } 503 504 return container; 505 } 506 507 protected ServiceSpec createServiceSpec(Map<String, String> labels) { 508 labels = normalizeLabels(labels); 509 final ServiceSpec serviceSpec = new ServiceSpec(); 510 serviceSpec.setType("ClusterIP"); 511 512 final ServicePort servicePort = new ServicePort(); 513 servicePort.setName(DEFAULT_NAME); 514 servicePort.setPort(Integer.valueOf(44134)); 515 servicePort.setTargetPort(new IntOrString(DEFAULT_NAME)); 516 serviceSpec.setPorts(Arrays.asList(servicePort)); 517 518 serviceSpec.setSelector(labels); 519 return serviceSpec; 520 } 521 522 protected final String normalizeNamespace(String namespace) { 523 if (namespace == null || namespace.isEmpty()) { 524 namespace = this.tillerNamespace; 525 if (namespace == null || namespace.isEmpty()) { 526 namespace = DEFAULT_NAMESPACE; 527 } 528 } 529 return namespace; 530 } 531 532 533 /* 534 * Static methods. 535 */ 536 537 538 protected static final Map<String, String> normalizeLabels(Map<String, String> labels) { 539 if (labels == null) { 540 labels = new HashMap<>(); 541 } 542 if (!labels.containsKey("app")) { 543 labels.put("app", "helm"); 544 } 545 if (!labels.containsKey("name")) { 546 labels.put("name", DEFAULT_NAME); 547 } 548 return labels; 549 } 550 551 protected static final String normalizeDeploymentName(final String deploymentName) { 552 if (deploymentName == null || deploymentName.isEmpty()) { 553 return DEFAULT_DEPLOYMENT_NAME; 554 } else { 555 return deploymentName; 556 } 557 } 558 559 protected static final String normalizeImageName(final String imageName) { 560 if (imageName == null || imageName.isEmpty()) { 561 return DEFAULT_IMAGE_NAME; 562 } else { 563 return imageName; 564 } 565 } 566 567 protected static final String normalizeImagePullPolicy(final String imagePullPolicy) { 568 if (!"Always".equals(imagePullPolicy) && 569 !"IfNotPresent".equals(imagePullPolicy) && 570 !"Never".equals(imagePullPolicy)) { 571 return DEFAULT_IMAGE_PULL_POLICY; 572 } else { 573 return imagePullPolicy; 574 } 575 } 576 577 protected static final String normalizeServiceAccountName(final String serviceAccountName) { 578 return serviceAccountName == null ? "" : serviceAccountName; 579 } 580 581 protected static final String normalizeServiceName(final String serviceName) { 582 if (serviceName == null || serviceName.isEmpty()) { 583 return DEFAULT_DEPLOYMENT_NAME; // yes, DEFAULT_*DEPLOYMENT*_NAME 584 } else { 585 return serviceName; 586 } 587 } 588 589 private static final InputStream read(final URI uri) throws IOException { 590 final InputStream returnValue; 591 if (uri == null) { 592 returnValue = null; 593 } else { 594 final URL url = uri.toURL(); 595 final InputStream uriStream = url.openStream(); 596 if (uriStream == null) { 597 returnValue = null; 598 } else { 599 returnValue = new BufferedInputStream(uriStream); 600 } 601 } 602 return returnValue; 603 } 604 605 private static final byte[] toByteArray(final InputStream inputStream) throws IOException { 606 // Interesting historical anecdotes at https://stackoverflow.com/a/1264737/208288. 607 byte[] returnValue = null; 608 if (inputStream != null) { 609 final ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 610 returnValue = new byte[4096]; // arbitrary size 611 int bytesRead; 612 while ((bytesRead = inputStream.read(returnValue, 0, returnValue.length)) != -1) { 613 buffer.write(returnValue, 0, bytesRead); 614 } 615 buffer.flush(); 616 returnValue = buffer.toByteArray(); 617 } 618 return returnValue; 619 } 620 621}