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.chart.repository; 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.URISyntaxException; 026import java.net.URL; 027 028import java.nio.ByteBuffer; 029 030import java.nio.file.LinkOption; // for javadoc only 031import java.nio.file.StandardCopyOption; 032import java.nio.file.Files; 033import java.nio.file.Path; 034import java.nio.file.Paths; 035 036import java.nio.file.attribute.FileAttribute; // for javadoc only 037 038import java.security.MessageDigest; 039import java.security.NoSuchAlgorithmException; 040 041import java.util.Collection; 042import java.util.Collections; 043import java.util.Iterator; 044import java.util.Map; 045import java.util.Objects; 046import java.util.LinkedHashSet; 047import java.util.Set; 048import java.util.SortedSet; 049import java.util.SortedMap; 050import java.util.TreeSet; 051import java.util.TreeMap; 052 053import java.util.zip.GZIPInputStream; 054 055import javax.xml.bind.DatatypeConverter; 056 057import com.github.zafarkhaja.semver.ParseException; 058import com.github.zafarkhaja.semver.Version; 059 060import hapi.chart.ChartOuterClass.Chart; 061import hapi.chart.MetadataOuterClass.Metadata; 062import hapi.chart.MetadataOuterClass.MetadataOrBuilder; 063 064import org.kamranzafar.jtar.TarInputStream; 065 066import org.microbean.development.annotation.Experimental; 067 068import org.microbean.helm.chart.Metadatas; 069import org.microbean.helm.chart.TapeArchiveChartLoader; 070 071import org.microbean.helm.chart.resolver.AbstractChartResolver; 072import org.microbean.helm.chart.resolver.ChartResolverException; 073 074import org.yaml.snakeyaml.Yaml; 075 076/** 077 * An {@link AbstractChartResolver} that {@linkplain #resolve(String, 078 * String) resolves} <a 079 * href="https://docs.helm.sh/developing_charts/#charts">Helm 080 * charts</a> from <a 081 * href="https://docs.helm.sh/developing_charts/#create-a-chart-repository">a 082 * given Helm chart repository</a>. 083 * 084 * @author <a href="https://about.me/lairdnelson" 085 * target="_parent">Laird Nelson</a> 086 * 087 * @see #resolve(String, String) 088 */ 089@Experimental 090public class ChartRepository extends AbstractChartResolver { 091 092 093 /* 094 * Instance fields. 095 */ 096 097 098 /** 099 * An {@linkplain Path#isAbsolute() absolute} {@link Path} 100 * representing a directory where Helm chart archives may be stored. 101 * 102 * <p>This field will never be {@code null}.</p> 103 */ 104 private final Path archiveCacheDirectory; 105 106 /** 107 * An {@linkplain Path#isAbsolute() absolute} or relative {@link 108 * Path} representing a local copy of a chart repository's <a 109 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 110 * index.yaml}</a> file. 111 * 112 * <p>If the value of this field is a relative {@link Path}, then it 113 * will be considered to be relative to the value of the {@link 114 * #indexCacheDirectory} field.</p> 115 * 116 * <p>This field will never be {@code null}.</p> 117 * 118 * @see #getCachedIndexPath() 119 */ 120 private final Path cachedIndexPath; 121 122 /** 123 * The {@link Index} object representing the chart repository index 124 * as described canonically by its <a 125 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 126 * index.yaml}</a> file. 127 * 128 * <p>This field may be {@code null}.</p> 129 * 130 * @see #getIndex() 131 * 132 * @see #downloadIndex() 133 */ 134 private transient Index index; 135 136 /** 137 * An {@linkplain Path#isAbsolute() absolute} {@link Path} 138 * representing a directory that the value of the {@link 139 * #cachedIndexPath} field will be considered to be relative to. 140 * 141 * <p>This field may be {@code null}, in which case it is guaranteed 142 * that the {@link #cachedIndexPath} field's value is {@linkplain 143 * Path#isAbsolute() absolute}.</p> 144 */ 145 private final Path indexCacheDirectory; 146 147 /** 148 * The name of this {@link ChartRepository}. 149 * 150 * <p>This field is never {@code null}.</p> 151 * 152 * @see #getName() 153 */ 154 private final String name; 155 156 /** 157 * The {@link URI} representing the root of the chart repository 158 * represented by this {@link ChartRepository}. 159 * 160 * <p>This field is never {@code null}.</p> 161 * 162 * @see #getUri() 163 */ 164 private final URI uri; 165 166 167 /* 168 * Constructors. 169 */ 170 171 172 /** 173 * Creates a new {@link ChartRepository} whose {@linkplain 174 * #getCachedIndexPath() cached index path} will be a {@link Path} 175 * relative to the absolute directory represented by the value of 176 * the {@code helm.home} system property, or the value of the {@code 177 * HELM_HOME} environment variable, and bearing a name consisting of 178 * the supplied {@code name} suffixed with {@code -index.yaml}. 179 * 180 * @param name the name of this {@link ChartRepository}; must not be 181 * {@code null} 182 * 183 * @param uri the {@linkplain URI#isAbsolute() absolute} {@link URI} 184 * to the root of this {@link ChartRepository}; must not be {@code 185 * null} 186 * 187 * @exception NullPointerException if either {@code name} or {@code 188 * uri} is {@code null} 189 * 190 * @exception IllegalArgumentException if {@code uri} is {@linkplain 191 * URI#isAbsolute() not absolute}, or if there is no existing "Helm 192 * home" directory 193 * 194 * @see #ChartRepository(String, URI, Path, Path, Path) 195 * 196 * @see #getName() 197 * 198 * @see #getUri() 199 * 200 * @see #getCachedIndexPath() 201 */ 202 public ChartRepository(final String name, final URI uri) { 203 this(name, uri, null, null, null); 204 } 205 206 /** 207 * Creates a new {@link ChartRepository}. 208 * 209 * @param name the name of this {@link ChartRepository}; must not be 210 * {@code null} 211 * 212 * @param uri the {@link URI} to the root of this {@link 213 * ChartRepository}; must not be {@code null} 214 * 215 * @param cachedIndexPath a {@link Path} naming the file that will 216 * store a copy of the chart repository's {@code index.yaml} file; 217 * if {@code null} then a {@link Path} relative to the absolute 218 * directory represented by the value of the {@code helm.home} 219 * system property, or the value of the {@code HELM_HOME} 220 * environment variable, and bearing a name consisting of the 221 * supplied {@code name} suffixed with {@code -index.yaml} will be 222 * used instead 223 * 224 * @exception NullPointerException if either {@code name} or {@code 225 * uri} is {@code null} 226 * 227 * @exception IllegalArgumentException if {@code uri} is {@linkplain 228 * URI#isAbsolute() not absolute}, or if there is no existing "Helm 229 * home" directory 230 * 231 * @see #ChartRepository(String, URI, Path, Path, Path) 232 * 233 * @see #getName() 234 * 235 * @see #getUri() 236 * 237 * @see #getCachedIndexPath() 238 */ 239 public ChartRepository(final String name, final URI uri, final Path cachedIndexPath) { 240 this(name, uri, null, null, cachedIndexPath); 241 } 242 243 /** 244 * Creates a new {@link ChartRepository}. 245 * 246 * @param name the name of this {@link ChartRepository}; must not be 247 * {@code null} 248 * 249 * @param uri the {@link URI} to the root of this {@link 250 * ChartRepository}; must not be {@code null} 251 * 252 * @param archiveCacheDirectory an {@linkplain Path#isAbsolute() 253 * absolute} {@link Path} representing a directory where Helm chart 254 * archives may be stored; if {@code null} then a {@link Path} 255 * beginning with the absolute directory represented by the value of 256 * the {@code helm.home} system property, or the value of the {@code 257 * HELM_HOME} environment variable, appended with {@code 258 * cache/archive} will be used instead 259 * 260 * @param indexCacheDirectory an {@linkplain Path#isAbsolute() 261 * absolute} {@link Path} representing a directory that the supplied 262 * {@code cachedIndexPath} parameter value will be considered to be 263 * relative to; <strong>will be ignored and hence may be {@code 264 * null}</strong> if the supplied {@code cachedIndexPath} parameter 265 * value {@linkplain Path#isAbsolute()} 266 * 267 * @param cachedIndexPath a {@link Path} naming the file that will 268 * store a copy of the chart repository's {@code index.yaml} file; 269 * if {@code null} then a {@link Path} relative to the absolute 270 * directory represented by the value of the {@code helm.home} 271 * system property, or the value of the {@code HELM_HOME} 272 * environment variable, and bearing a name consisting of the 273 * supplied {@code name} suffixed with {@code -index.yaml} will be 274 * used instead 275 * 276 * @exception NullPointerException if either {@code name} or {@code 277 * uri} is {@code null} 278 * 279 * @exception IllegalArgumentException if {@code uri} is {@linkplain 280 * URI#isAbsolute() not absolute}, or if there is no existing "Helm 281 * home" directory, or if {@code archiveCacheDirectory} is 282 * non-{@code null} and either empty or not {@linkplain 283 * Path#isAbsolute()} 284 * 285 * @see #ChartRepository(String, URI, Path, Path, Path) 286 * 287 * @see #getName() 288 * 289 * @see #getUri() 290 * 291 * @see #getCachedIndexPath() 292 */ 293 public ChartRepository(final String name, final URI uri, final Path archiveCacheDirectory, Path indexCacheDirectory, Path cachedIndexPath) { 294 super(); 295 Objects.requireNonNull(name); 296 Objects.requireNonNull(uri); 297 if (!uri.isAbsolute()) { 298 throw new IllegalArgumentException("!uri.isAbsolute(): " + uri); 299 } 300 301 Path helmHome = null; 302 303 if (archiveCacheDirectory == null) { 304 helmHome = getHelmHome(); 305 assert helmHome != null; 306 this.archiveCacheDirectory = helmHome.resolve("cache/archive"); 307 assert this.archiveCacheDirectory != null; 308 assert this.archiveCacheDirectory.isAbsolute(); 309 } else if (archiveCacheDirectory.toString().isEmpty()) { 310 throw new IllegalArgumentException("archiveCacheDirectory.toString().isEmpty(): " + archiveCacheDirectory); 311 } else if (!archiveCacheDirectory.isAbsolute()) { 312 throw new IllegalArgumentException("!archiveCacheDirectory.isAbsolute(): " + archiveCacheDirectory); 313 } else { 314 this.archiveCacheDirectory = archiveCacheDirectory; 315 } 316 assert this.archiveCacheDirectory != null; 317 assert this.archiveCacheDirectory.isAbsolute(); 318 if (!Files.isDirectory(this.archiveCacheDirectory)) { 319 throw new IllegalArgumentException("!Files.isDirectory(this.archiveCacheDirectory): " + this.archiveCacheDirectory); 320 } 321 322 if (cachedIndexPath == null || cachedIndexPath.toString().isEmpty()) { 323 cachedIndexPath = Paths.get(new StringBuilder(name).append("-index.yaml").toString()); 324 } 325 assert cachedIndexPath != null; 326 327 if (cachedIndexPath.isAbsolute()) { 328 this.indexCacheDirectory = null; 329 this.cachedIndexPath = cachedIndexPath; 330 } else { 331 if (indexCacheDirectory == null) { 332 if (helmHome == null) { 333 helmHome = getHelmHome(); 334 assert helmHome != null; 335 } 336 this.indexCacheDirectory = helmHome.resolve("repository/cache"); 337 assert this.indexCacheDirectory.isAbsolute(); 338 } else if (!indexCacheDirectory.isAbsolute()) { 339 throw new IllegalArgumentException("!indexCacheDirectory.isAbsolute(): " + indexCacheDirectory); 340 } else { 341 this.indexCacheDirectory = indexCacheDirectory; 342 assert this.indexCacheDirectory.isAbsolute(); 343 } 344 if (!Files.isDirectory(this.indexCacheDirectory)) { 345 throw new IllegalArgumentException("!Files.isDirectory(this.indexCacheDirectory): " + this.indexCacheDirectory); 346 } 347 this.cachedIndexPath = this.indexCacheDirectory.resolve(cachedIndexPath); 348 } 349 assert this.cachedIndexPath != null; 350 assert this.cachedIndexPath.isAbsolute(); 351 352 this.name = name; 353 this.uri = uri; 354 } 355 356 357 /* 358 * Instance methods. 359 */ 360 361 362 /** 363 * Returns the name of this {@link ChartRepository}. 364 * 365 * <p>This method never returns {@code null}.</p> 366 * 367 * @return the non-{@code null} name of this {@link ChartRepository} 368 */ 369 public final String getName() { 370 return this.name; 371 } 372 373 /** 374 * Returns the {@link URI} of the root of this {@link 375 * ChartRepository}. 376 * 377 * <p>This method never returns {@code null}.</p> 378 * 379 * @return the non-{@code null} {@link URI} of the root of this 380 * {@link ChartRepository} 381 */ 382 public final URI getUri() { 383 return this.uri; 384 } 385 386 /** 387 * Returns a non-{@code null}, {@linkplain Path#isAbsolute() 388 * absolute} {@link Path} to the file that contains or will contain 389 * a copy of the chart repository's <a 390 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 391 * index.yaml}</a> file. 392 * 393 * <p>This method never returns {@code null}.</p> 394 * 395 * @return a non-{@code null}, {@linkplain Path#isAbsolute() 396 * absolute} {@link Path} to the file that contains or will contain 397 * a copy of the chart repository's <a 398 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 399 * index.yaml}</a> file 400 */ 401 public final Path getCachedIndexPath() { 402 return this.cachedIndexPath; 403 } 404 405 /** 406 * Returns the {@link Index} for this {@link ChartRepository}. 407 * 408 * <p>This method never returns {@code null}.</p> 409 * 410 * <p>If this method has not been invoked before on this {@link 411 * ChartRepository}, then the {@linkplain #getCachedIndexPath() 412 * cached copy} of the chart repository's <a 413 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 414 * index.yaml}</a> file is parsed into an {@link Index} and that 415 * {@link Index} is stored in an instance variable before it is 416 * returned.</p> 417 * 418 * <p>If no {@linkplain #getCachedIndexPath() cached copy} of the 419 * chart repository's <a 420 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 421 * index.yaml}</a> file exists, then one is {@linkplain 422 * #downloadIndex() downloaded} first.</p> 423 * 424 * return the {@link Index} representing the contents of this {@link 425 * ChartRepository}; never {@code null} 426 * 427 * @exception IOException if there was a problem either parsing an 428 * <a 429 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 430 * index.yaml}</a> file or downloading it 431 * 432 * @exception URISyntaxException if one of the URIs in the <a 433 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 434 * index.yaml}</a> file is invalid 435 * 436 * @see #getIndex(boolean) 437 * 438 * @see #downloadIndex() 439 */ 440 public final Index getIndex() throws IOException, URISyntaxException { 441 return this.getIndex(false); 442 } 443 444 /** 445 * Returns the {@link Index} for this {@link ChartRepository}. 446 * 447 * <p>This method never returns {@code null}.</p> 448 * 449 * <p>If this method has not been invoked before on this {@link 450 * ChartRepository}, then the {@linkplain #getCachedIndexPath() 451 * cached copy} of the chart repository's <a 452 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 453 * index.yaml}</a> file is parsed into an {@link Index} and that 454 * {@link Index} is stored in an instance variable before it is 455 * returned.</p> 456 * 457 * <p>If the {@linkplain #getCachedIndexPath() cached copy} of the 458 * chart repository's <a 459 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 460 * index.yaml}</a> file {@linkplain #isCachedIndexExpired() has 461 * expired}, then one is {@linkplain #downloadIndex() downloaded} 462 * first.</p> 463 * 464 * @param forceDownload if {@code true} then no caching will happen 465 * 466 * @return the {@link Index} representing the contents of this {@link 467 * ChartRepository}; never {@code null} 468 * 469 * @exception IOException if there was a problem either parsing an 470 * <a 471 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 472 * index.yaml}</a> file or downloading it 473 * 474 * @exception URISyntaxException if one of the URIs in the <a 475 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 476 * index.yaml}</a> file is invalid 477 * 478 * @see #getIndex(boolean) 479 * 480 * @see #downloadIndex() 481 * 482 * @see #isCachedIndexExpired() 483 */ 484 public final Index getIndex(final boolean forceDownload) throws IOException, URISyntaxException { 485 if (forceDownload || this.index == null) { 486 final Path cachedIndexPath = this.getCachedIndexPath(); 487 assert cachedIndexPath != null; 488 if (forceDownload || this.isCachedIndexExpired()) { 489 this.downloadIndexTo(cachedIndexPath); 490 } 491 this.index = Index.loadFrom(cachedIndexPath); 492 assert this.index != null; 493 } 494 return this.index; 495 } 496 497 /** 498 * Returns {@code true} if the {@linkplain #getCachedIndexPath() 499 * cached copy} of the <a 500 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 501 * index.yaml}</a> file is to be considered stale. 502 * 503 * <p>The default implementation of this method returns the negation 504 * of the return value of an invocation of the {@link 505 * Files#isRegularFile(Path, LinkOption...)} method on the return value of the 506 * {@link #getCachedIndexPath()} method.</p> 507 * 508 * @return {@code true} if the {@linkplain #getCachedIndexPath() 509 * cached copy} of the <a 510 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 511 * index.yaml}</a> file is to be considered stale; {@code false} otherwise 512 * 513 * @see #getIndex(boolean) 514 */ 515 public boolean isCachedIndexExpired() { 516 final Path cachedIndexPath = this.getCachedIndexPath(); 517 assert cachedIndexPath != null; 518 return !Files.isRegularFile(cachedIndexPath); 519 } 520 521 /** 522 * Clears the {@link Index} stored internally by this {@link 523 * ChartRepository}, paving the way for a fresh copy to be installed 524 * by the {@link #getIndex(boolean)} method, and returns the old 525 * value. 526 * 527 * <p>This method may return {@code null} if {@code 528 * #getIndex(boolean)} has not yet been called.</p> 529 * 530 * @return the {@link Index}, or {@code null} 531 */ 532 public final Index clearIndex() { 533 final Index returnValue = this.index; 534 this.index = null; 535 return returnValue; 536 } 537 538 /** 539 * Invokes the {@link #downloadIndexTo(Path)} method with the return 540 * value of the {@link #getCachedIndexPath()} method. 541 * 542 * <p>This method never returns {@code null}.</p> 543 * 544 * @return {@link Path} the {@link Path} to which the {@code 545 * index.yaml} file was downloaded; never {@code null} 546 * 547 * @exception IOException if there was a problem downloading 548 * 549 * @see #downloadIndexTo(Path) 550 */ 551 public final Path downloadIndex() throws IOException { 552 return this.downloadIndexTo(this.getCachedIndexPath()); 553 } 554 555 /** 556 * Downloads a copy of the chart repository's <a 557 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 558 * index.yaml}</a> file to the {@link Path} specified and returns 559 * the canonical representation of the {@link Path} to which the 560 * file was actually downloaded. 561 * 562 * <p>This method never returns {@code null}.</p> 563 * 564 * <p>Overrides of this method must not return {@code null}.</p> 565 * 566 * <p>The default implementation of this method actually downloads 567 * the <a 568 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 569 * index.yaml}</a> file to a {@linkplain 570 * Files#createTempFile(String, String, FileAttribute...) temporary 571 * file} first, and then {@linkplain StandardCopyOption#ATOMIC_MOVE 572 * atomically renames it}.</p> 573 * 574 * @param path the {@link Path} to download the <a 575 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">{@code 576 * index.yaml}</a> file to; may be {@code null} in which case the 577 * return value of the {@link #getCachedIndexPath()} method will be 578 * used instead 579 * 580 * @return the {@link Path} to the file; never {@code null} 581 * 582 * @exception IOException if there was a problem downloading 583 */ 584 public Path downloadIndexTo(Path path) throws IOException { 585 final URI baseUri = this.getUri(); 586 if (baseUri == null) { 587 throw new IllegalStateException("getUri() == null"); 588 } 589 final URI indexUri = baseUri.resolve("index.yaml"); 590 assert indexUri != null; 591 final URL indexUrl = indexUri.toURL(); 592 assert indexUrl != null; 593 if (path == null) { 594 path = this.getCachedIndexPath(); 595 } 596 assert path != null; 597 if (!path.isAbsolute()) { 598 assert this.indexCacheDirectory != null; 599 assert this.indexCacheDirectory.isAbsolute(); 600 path = this.indexCacheDirectory.resolve(path); 601 assert path != null; 602 assert path.isAbsolute(); 603 } 604 final Path temporaryPath = Files.createTempFile(new StringBuilder(this.getName()).append("-index-").toString(), ".yaml"); 605 assert temporaryPath != null; 606 try (final BufferedInputStream stream = new BufferedInputStream(indexUrl.openStream())) { 607 Files.copy(stream, temporaryPath, StandardCopyOption.REPLACE_EXISTING); 608 } catch (final IOException throwMe) { 609 try { 610 Files.deleteIfExists(temporaryPath); 611 } catch (final IOException suppressMe) { 612 throwMe.addSuppressed(suppressMe); 613 } 614 throw throwMe; 615 } 616 return Files.move(temporaryPath, path, StandardCopyOption.ATOMIC_MOVE); 617 } 618 619 /** 620 * Creates a new {@link Index} from the contents of the {@linkplain 621 * #getCachedIndexPath() cached copy of the chart repository's 622 * <code>index.yaml</code> file} and returns it. 623 * 624 * <p>This method never returns {@code null}.</p> 625 * 626 * <p>Overrides of this method must not return {@code null}.</p> 627 * 628 * @return a new {@link Index}; never {@code null} 629 * 630 * @exception IOException if there was a problem reading the file 631 * 632 * @exception URISyntaxException if a URI in the file was invalid 633 * 634 * @see Index#loadFrom(Path) 635 */ 636 public Index loadIndex() throws IOException, URISyntaxException { 637 Path path = this.getCachedIndexPath(); 638 assert path != null; 639 if (!path.isAbsolute()) { 640 assert this.indexCacheDirectory != null; 641 assert this.indexCacheDirectory.isAbsolute(); 642 path = this.indexCacheDirectory.resolve(path); 643 assert path != null; 644 assert path.isAbsolute(); 645 } 646 return Index.loadFrom(path); 647 } 648 649 /** 650 * Given a Helm chart name and its version, returns the local {@link 651 * Path}, representing a local copy of the Helm chart as downloaded 652 * from the chart repository represented by this {@link 653 * ChartRepository}, downloading the archive if necessary. 654 * 655 * <p>This method may return {@code null}.</p> 656 * 657 * @param chartName the name of the chart whose local {@link Path} 658 * should be returned; must not be {@code null} 659 * 660 * @param chartVersion the version of the chart to select; may be 661 * {@code null} in which case "latest" semantics are implied 662 * 663 * @return the {@link Path} to the chart archive, or {@code null} 664 * 665 * @exception IOException if there was a problem downloading 666 * 667 * @exception URISyntaxException if this {@link ChartRepository}'s 668 * {@linkplain #getIndex() associated <code>Index</code>} could not 669 * be parsed 670 * 671 * @exception NullPointerException if {@code chartName} is {@code 672 * null} 673 */ 674 public final Path getCachedChartPath(final String chartName, String chartVersion) throws IOException, URISyntaxException { 675 Objects.requireNonNull(chartName); 676 Path returnValue = null; 677 if (chartVersion == null) { 678 final Index index = this.getIndex(false); 679 assert index != null; 680 final Index.Entry entry = index.getEntry(chartName, null /* latest */); 681 if (entry != null) { 682 chartVersion = entry.getVersion(); 683 } 684 } 685 if (chartVersion != null) { 686 assert this.archiveCacheDirectory != null; 687 final StringBuilder chartKey = new StringBuilder(chartName).append("-").append(chartVersion); 688 final String chartFilename = new StringBuilder(chartKey).append(".tgz").toString(); 689 final Path cachedChartPath = this.archiveCacheDirectory.resolve(chartFilename); 690 assert cachedChartPath != null; 691 if (!Files.isRegularFile(cachedChartPath)) { 692 final Index index = this.getIndex(true); 693 assert index != null; 694 final Index.Entry entry = index.getEntry(chartName, chartVersion); 695 if (entry != null) { 696 final URI chartUri = entry.getFirstUri(); 697 if (chartUri != null) { 698 final URL chartUrl = chartUri.toURL(); 699 assert chartUrl != null; 700 final Path temporaryPath = Files.createTempFile(chartKey.append("-").toString(), ".tgz"); 701 assert temporaryPath != null; 702 try (final InputStream stream = new BufferedInputStream(chartUrl.openStream())) { 703 Files.copy(stream, temporaryPath, StandardCopyOption.REPLACE_EXISTING); 704 } catch (final IOException throwMe) { 705 try { 706 Files.deleteIfExists(temporaryPath); 707 } catch (final IOException suppressMe) { 708 throwMe.addSuppressed(suppressMe); 709 } 710 throw throwMe; 711 } 712 Files.move(temporaryPath, cachedChartPath, StandardCopyOption.ATOMIC_MOVE); 713 } 714 } 715 } 716 returnValue = cachedChartPath; 717 } 718 return returnValue; 719 } 720 721 /** 722 * {@inheritDoc} 723 * 724 * <p>This implementation calls the {@link 725 * #getCachedChartPath(String, String)} method with the supplied 726 * arguments and uses a {@link TapeArchiveChartLoader} to load the 727 * resulting archive into a {@link Chart.Builder} object.</p> 728 */ 729 @Override 730 public Chart.Builder resolve(final String chartName, String chartVersion) throws ChartResolverException { 731 Objects.requireNonNull(chartName); 732 Chart.Builder returnValue = null; 733 Path cachedChartPath = null; 734 try { 735 cachedChartPath = this.getCachedChartPath(chartName, chartVersion); 736 } catch (final IOException | URISyntaxException exception) { 737 throw new ChartResolverException(exception.getMessage(), exception); 738 } 739 if (cachedChartPath != null && Files.isRegularFile(cachedChartPath)) { 740 try (final TapeArchiveChartLoader loader = new TapeArchiveChartLoader()) { 741 returnValue = loader.load(new TarInputStream(new GZIPInputStream(new BufferedInputStream(Files.newInputStream(cachedChartPath))))); 742 } catch (final IOException exception) { 743 throw new ChartResolverException(exception.getMessage(), exception); 744 } 745 } 746 return returnValue; 747 } 748 749 /** 750 * Returns a {@link Path} representing "Helm home": the root 751 * directory for various Helm-related metadata as specified by 752 * either the {@code helm.home} system property or the {@code 753 * HELM_HOME} environment variable. 754 * 755 * <p>This method never returns {@code null}.</p> 756 * 757 * <p>No guarantee is made by this method regarding whether the 758 * returned {@link Path} actually denotes a directory.</p> 759 * 760 * @return a {@link Path} representing "Helm home"; never {@code 761 * null} 762 * 763 * @exception SecurityException if there are not sufficient 764 * permissions to read system properties or environment variables 765 */ 766 static final Path getHelmHome() { 767 String helmHome = System.getProperty("helm.home", System.getenv("HELM_HOME")); 768 if (helmHome == null) { 769 helmHome = Paths.get(System.getProperty("user.home")).resolve(".helm").toString(); 770 assert helmHome != null; 771 } 772 return Paths.get(helmHome); 773 } 774 775 776 /* 777 * Inner and nested classes. 778 */ 779 780 781 /** 782 * A class representing certain of the contents of a <a 783 * href="https://docs.helm.sh/developing_charts/#the-chart-repository-structure">Helm 784 * chart repository's {@code index.yaml} file</a>. 785 * 786 * @author <a href="https://about.me/lairdnelson" 787 * target="_parent">Laird Nelson</a> 788 */ 789 @Experimental 790 public static final class Index { 791 792 793 /* 794 * Instance fields. 795 */ 796 797 798 /** 799 * An {@linkplain Collections#unmodifiableSortedMap(SortedMap) 800 * immutable} {@link SortedMap} of {@link SortedSet}s of {@link 801 * Entry} objects whose values represent enough information to 802 * derive a URI to a Helm chart. 803 * 804 * <p>This field is never {@code null}.</p> 805 */ 806 private final SortedMap<String, SortedSet<Entry>> entries; 807 808 809 /* 810 * Constructors. 811 */ 812 813 814 /** 815 * Creates a new {@link Index}. 816 * 817 * @param entries a {@link Map} of {@link SortedSet}s of {@link 818 * Entry} objects indexed by the name of the Helm chart they 819 * describe; may be {@code null}; copied by value 820 */ 821 Index(final Map<? extends String, ? extends SortedSet<Entry>> entries) { 822 super(); 823 if (entries == null || entries.isEmpty()) { 824 this.entries = Collections.emptySortedMap(); 825 } else { 826 this.entries = Collections.unmodifiableSortedMap(deepCopy(entries)); 827 } 828 } 829 830 831 /* 832 * Instance methods. 833 */ 834 835 836 /** 837 * Creates and returns a new {@link Index} consisting of all this 838 * {@link Index} instance's {@linkplain #getEntries() entries} 839 * augmented with those entries from the supplied {@link Index} 840 * that this {@link Index} instance did not already contain. 841 * 842 * @param other the {@link Index} to merge in; may be {@code null} 843 * 844 * @return a new {@link Index} reflecting the merge operation 845 */ 846 @Experimental 847 public final Index merge(final Index other) { 848 final Index returnValue; 849 final Map<String, SortedSet<Entry>> myEntries = this.getEntries(); 850 final Map<String, SortedSet<Entry>> otherEntries; 851 if (other == null) { 852 otherEntries = null; 853 } else { 854 otherEntries = other.getEntries(); 855 } 856 if (otherEntries == null || otherEntries.isEmpty()) { 857 if (myEntries == null || myEntries.isEmpty()) { 858 returnValue = new Index(null); 859 } else { 860 returnValue = new Index(myEntries); 861 } 862 } else if (myEntries == null || myEntries.isEmpty()) { 863 returnValue = new Index(otherEntries); 864 } else { 865 final Map<String, SortedSet<Entry>> mergedEntries = deepCopy(myEntries); 866 final Set<Map.Entry<String, SortedSet<Entry>>> otherEntrySet = otherEntries.entrySet(); 867 if (otherEntrySet != null && !otherEntrySet.isEmpty()) { 868 for (final Map.Entry<? extends String, ? extends SortedSet<Entry>> otherEntrySetElement : otherEntrySet) { 869 if (otherEntrySetElement != null) { 870 final SortedSet<Entry> otherValues = otherEntrySetElement.getValue(); 871 if (otherValues != null && !otherValues.isEmpty()) { 872 for (final Entry otherEntry : otherValues) { 873 if (otherEntry != null) { 874 final String otherEntryName = otherEntry.getName(); 875 final Entry myCorrespondingEntry = this.getEntry(otherEntryName, otherEntry.getVersion()); 876 if (myCorrespondingEntry == null) { 877 SortedSet<Entry> myRelatedEntries = mergedEntries.get(otherEntryName); 878 if (myRelatedEntries == null) { 879 myRelatedEntries = new TreeSet<>(Collections.reverseOrder()); 880 mergedEntries.put(otherEntryName, myRelatedEntries); 881 } 882 assert !myRelatedEntries.contains(otherEntry); 883 myRelatedEntries.add(otherEntry); 884 } 885 } 886 } 887 } 888 } 889 } 890 } 891 returnValue = new Index(mergedEntries); 892 } 893 return returnValue; 894 } 895 896 /** 897 * Returns a non-{@code null}, {@linkplain 898 * Collections#unmodifiableMap(Map) immutable} {@link Map} of 899 * {@link SortedSet}s of {@link Entry} objects, indexed by the 900 * name of the Helm chart they describe. 901 * 902 * @return a non-{@code null}, {@linkplain 903 * Collections#unmodifiableMap(Map) immutable} {@link Map} of 904 * {@link SortedSet}s of {@link Entry} objects, indexed by the 905 * name of the Helm chart they describe 906 */ 907 public final Map<String, SortedSet<Entry>> getEntries() { 908 return this.entries; 909 } 910 911 /** 912 * Returns an {@link Entry} identified by the supplied {@code 913 * name} and {@code version}, if there is one. 914 * 915 * <p>This method may return {@code null}.</p> 916 * 917 * @param name the name of the Helm chart whose related {@link 918 * Entry} is desired; must not be {@code null} 919 * 920 * @param versionString the version of the Helm chart whose 921 * related {@link Entry} is desired; may be {@code null} in which 922 * case "latest" semantics are implied 923 * 924 * @return an {@link Entry}, or {@code null} 925 * 926 * @exception NullPointerException if {@code name} is {@code null} 927 */ 928 public final Entry getEntry(final String name, final String versionString) { 929 Objects.requireNonNull(name); 930 Entry returnValue = null; 931 final Map<String, SortedSet<Entry>> entries = this.getEntries(); 932 if (entries != null && !entries.isEmpty()) { 933 final SortedSet<Entry> entrySet = entries.get(name); 934 if (entrySet != null && !entrySet.isEmpty()) { 935 if (versionString == null) { 936 returnValue = entrySet.first(); 937 } else { 938 for (final Entry entry : entrySet) { 939 // XXX TODO FIXME: probably want to make this a 940 // constraint match, not just an equality comparison 941 if (entry != null && versionString.equals(entry.getVersion())) { 942 returnValue = entry; 943 break; 944 } 945 } 946 } 947 } 948 } 949 return returnValue; 950 } 951 952 953 /* 954 * Static methods. 955 */ 956 957 958 /** 959 * Creates a new {@link Index} whose contents are sourced from the 960 * YAML file located at the supplied {@link Path}. 961 * 962 * <p>This method never returns {@code null}.</p> 963 * 964 * @param path the {@link Path} to a YAML file whose contents are 965 * those of a <a 966 * href="https://docs.helm.sh/developing_charts/#the-index-file">Helm 967 * chart repository index</a>; must not be {@code null} 968 * 969 * @return a new {@link Index}; never {@code null} 970 * 971 * @exception IOException if there was a problem reading the file 972 * 973 * @exception URISyntaxException if one of the URIs in the file 974 * was invalid 975 * 976 * @exception NullPointerException if {@code path} is {@code null} 977 * 978 * @see #loadFrom(InputStream) 979 */ 980 public static final Index loadFrom(final Path path) throws IOException, URISyntaxException { 981 Objects.requireNonNull(path); 982 final Index returnValue; 983 try (final BufferedInputStream stream = new BufferedInputStream(Files.newInputStream(path))) { 984 returnValue = loadFrom(stream); 985 } 986 return returnValue; 987 } 988 989 /** 990 * Creates a new {@link Index} whose contents are sourced from the 991 * <a 992 * href="https://docs.helm.sh/developing_charts/#the-index-file">Helm 993 * chart repository index</a> YAML contents represented by the 994 * supplied {@link InputStream}. 995 * 996 * <p>This method never returns {@code null}.</p> 997 * 998 * @param stream the {@link InputStream} to a YAML file whose contents are 999 * those of a <a 1000 * href="https://docs.helm.sh/developing_charts/#the-index-file">Helm 1001 * chart repository index</a>; must not be {@code null} 1002 * 1003 * @return a new {@link Index}; never {@code null} 1004 * 1005 * @exception IOException if there was a problem reading the file 1006 * 1007 * @exception URISyntaxException if one of the URIs in the file 1008 * was invalid 1009 * 1010 * @exception NullPointerException if {@code path} is {@code null} 1011 */ 1012 public static final Index loadFrom(final InputStream stream) throws IOException, URISyntaxException { 1013 Objects.requireNonNull(stream); 1014 final Index returnValue; 1015 final Map<?, ?> yamlMap = new Yaml().loadAs(stream, Map.class); 1016 if (yamlMap == null || yamlMap.isEmpty()) { 1017 returnValue = new Index(null); 1018 } else { 1019 final SortedMap<String, SortedSet<Index.Entry>> sortedEntryMap = new TreeMap<>(); 1020 @SuppressWarnings("unchecked") 1021 final Map<? extends String, ? extends Collection<? extends Map<?, ?>>> entriesMap = (Map<? extends String, ? extends Collection<? extends Map<?, ?>>>)yamlMap.get("entries"); 1022 if (entriesMap != null && !entriesMap.isEmpty()) { 1023 final Collection<? extends Map.Entry<? extends String, ? extends Collection<? extends Map<?, ?>>>> entries = entriesMap.entrySet(); 1024 if (entries != null && !entries.isEmpty()) { 1025 for (final Map.Entry<? extends String, ? extends Collection<? extends Map<?, ?>>> mapEntry : entries) { 1026 if (mapEntry != null) { 1027 final String entryName = mapEntry.getKey(); 1028 if (entryName != null) { 1029 final Collection<? extends Map<?, ?>> entryContents = mapEntry.getValue(); 1030 if (entryContents != null && !entryContents.isEmpty()) { 1031 for (final Map<?, ?> entryMap : entryContents) { 1032 if (entryMap != null && !entryMap.isEmpty()) { 1033 final Metadata.Builder metadataBuilder = Metadata.newBuilder(); 1034 assert metadataBuilder != null; 1035 Metadatas.populateMetadataBuilder(metadataBuilder, entryMap); 1036 @SuppressWarnings("unchecked") 1037 final Collection<? extends String> uriStrings = (Collection<? extends String>)entryMap.get("urls"); 1038 Set<URI> uris = new LinkedHashSet<>(); 1039 if (uriStrings != null && !uriStrings.isEmpty()) { 1040 for (final String uriString : uriStrings) { 1041 if (uriString != null && !uriString.isEmpty()) { 1042 uris.add(new URI(uriString)); 1043 } 1044 } 1045 } 1046 final String digest = (String)entryMap.get("digest"); 1047 SortedSet<Index.Entry> entryObjects = sortedEntryMap.get(entryName); 1048 if (entryObjects == null) { 1049 entryObjects = new TreeSet<>(Collections.reverseOrder()); 1050 sortedEntryMap.put(entryName, entryObjects); 1051 } 1052 entryObjects.add(new Index.Entry(metadataBuilder, uris, digest)); 1053 } 1054 } 1055 } 1056 } 1057 } 1058 } 1059 } 1060 } 1061 returnValue = new Index(sortedEntryMap); 1062 } 1063 return returnValue; 1064 } 1065 1066 /** 1067 * Performs a deep copy of the supplied {@link Map} such that the 1068 * {@link SortedMap} returned has copies of the supplied {@link 1069 * Map}'s {@linkplain Map#values() values}. 1070 * 1071 * <p>This method may return {@code null} if {@code source} is 1072 * {@code null}.</p> 1073 * 1074 * <p>The {@link SortedMap} returned by this method is 1075 * mutable.</p> 1076 * 1077 * @param source the {@link Map} to copy; may be {@code null} in 1078 * which case {@code null} will be returned 1079 * 1080 * @return a mutable {@link SortedMap}, or {@code null} 1081 */ 1082 private static final SortedMap<String, SortedSet<Entry>> deepCopy(final Map<? extends String, ? extends SortedSet<Entry>> source) { 1083 final SortedMap<String, SortedSet<Entry>> returnValue; 1084 if (source == null) { 1085 returnValue = null; 1086 } else if (source.isEmpty()) { 1087 returnValue = Collections.emptySortedMap(); 1088 } else { 1089 returnValue = new TreeMap<>(); 1090 final Collection<? extends Map.Entry<? extends String, ? extends SortedSet<Entry>>> entrySet = source.entrySet(); 1091 if (entrySet != null && !entrySet.isEmpty()) { 1092 for (final Map.Entry<? extends String, ? extends SortedSet<Entry>> entry : entrySet) { 1093 final String key = entry.getKey(); 1094 final SortedSet<Entry> value = entry.getValue(); 1095 if (value == null) { 1096 returnValue.put(key, null); 1097 } else { 1098 final SortedSet<Entry> newValue = new TreeSet<>(value.comparator()); 1099 newValue.addAll(value); 1100 returnValue.put(key, newValue); 1101 } 1102 } 1103 } 1104 } 1105 return returnValue; 1106 } 1107 1108 1109 /* 1110 * Inner and nested classes. 1111 */ 1112 1113 1114 /** 1115 * An entry in a <a 1116 * href="https://docs.helm.sh/developing_charts/#the-index-file">Helm 1117 * chart repository index</a>. 1118 * 1119 * @author <a href="https://about.me/lairdnelson" 1120 * target="_parent">Laird Nelson</a> 1121 */ 1122 @Experimental 1123 public static final class Entry implements Comparable<Entry> { 1124 1125 1126 /* 1127 * Instance fields. 1128 */ 1129 1130 1131 /** 1132 * A {@link MetadataOrBuilder} representing most of the contents 1133 * of the entry. 1134 * 1135 * <p>This field is never {@code null}.</p> 1136 */ 1137 private final MetadataOrBuilder metadata; 1138 1139 /** 1140 * An {@linkplain Collections#unmodifiableSet(Set) immutable} 1141 * {@link Set} of {@link URI}s describing where the particular 1142 * Helm chart described by this {@link Entry} may be downloaded 1143 * from. 1144 * 1145 * <p>This field is never {@code null}.</p> 1146 */ 1147 private final Set<URI> uris; 1148 1149 1150 private final String digest; 1151 1152 /* 1153 * Constructors. 1154 */ 1155 1156 1157 /** 1158 * Creates a new {@link Entry}. 1159 * 1160 * @param metadata a {@link MetadataOrBuilder} representing most 1161 * of the contents of the entry; must not be {@code null} 1162 * 1163 * @param uris a {@link Collection} of {@link URI}s describing 1164 * where the particular Helm chart described by this {@link 1165 * Entry} may be downloaded from; may be {@code null}; copied by 1166 * value 1167 * 1168 * @exception NullPointerException if {@code metadata} is {@code 1169 * null} 1170 * 1171 * @see #Entry(MetadataOrBuilder, Collection, String) 1172 */ 1173 Entry(final MetadataOrBuilder metadata, final Collection<? extends URI> uris) { 1174 this(metadata, uris, null); 1175 } 1176 1177 /** 1178 * Creates a new {@link Entry}. 1179 * 1180 * @param metadata a {@link MetadataOrBuilder} representing most 1181 * of the contents of the entry; must not be {@code null} 1182 * 1183 * @param uris a {@link Collection} of {@link URI}s describing 1184 * where the particular Helm chart described by this {@link 1185 * Entry} may be downloaded from; may be {@code null}; copied by 1186 * value 1187 * 1188 * @param digest a SHA-256 message digest to be associated with 1189 * this {@link Entry}; may be {@code null} 1190 * 1191 * @exception NullPointerException if {@code metadata} is {@code 1192 * null} 1193 */ 1194 Entry(final MetadataOrBuilder metadata, final Collection<? extends URI> uris, final String digest) { 1195 super(); 1196 this.metadata = Objects.requireNonNull(metadata); 1197 if (uris == null || uris.isEmpty()) { 1198 this.uris = Collections.emptySet(); 1199 } else { 1200 this.uris = new LinkedHashSet<>(uris); 1201 } 1202 this.digest = digest; 1203 } 1204 1205 1206 /* 1207 * Instance methods. 1208 */ 1209 1210 1211 /** 1212 * Compares this {@link Entry} to the supplied {@link Entry} and 1213 * returns a value less than {@code 0} if this {@link Entry} is 1214 * "less than" the supplied {@link Entry}, {@code 1} if this 1215 * {@link Entry} is "greater than" the supplied {@link Entry} 1216 * and {@code 0} if this {@link Entry} is equal to the supplied 1217 * {@link Entry}. 1218 * 1219 * <p>{@link Entry} objects are compared by {@linkplain 1220 * #getName() name} first, then {@linkplain #getVersion() 1221 * version}.</p> 1222 * 1223 * <p>It is intended that this {@link 1224 * #compareTo(ChartRepository.Index.Entry)} method is 1225 * {@linkplain Comparable consistent with equals}.</p> 1226 * 1227 * @param her the {@link Entry} to compare; must not be {@code null} 1228 * 1229 * @return a value less than {@code 0} if this {@link Entry} is 1230 * "less than" the supplied {@link Entry}, {@code 1} if this 1231 * {@link Entry} is "greater than" the supplied {@link Entry} 1232 * and {@code 0} if this {@link Entry} is equal to the supplied 1233 * {@link Entry} 1234 * 1235 * @exception NullPointerException if the supplied {@link Entry} 1236 * is {@code null} 1237 */ 1238 @Override 1239 public final int compareTo(final Entry her) { 1240 Objects.requireNonNull(her); // see Comparable documentation 1241 1242 final String myName = this.getName(); 1243 final String herName = her.getName(); 1244 if (myName == null) { 1245 if (herName != null) { 1246 return -1; 1247 } 1248 } else if (herName == null) { 1249 return 1; 1250 } else { 1251 final int nameComparison = myName.compareTo(herName); 1252 if (nameComparison != 0) { 1253 return nameComparison; 1254 } 1255 } 1256 1257 final String myVersionString = this.getVersion(); 1258 final String herVersionString = her.getVersion(); 1259 if (myVersionString == null) { 1260 if (herVersionString != null) { 1261 return -1; 1262 } 1263 } else if (herVersionString == null) { 1264 return 1; 1265 } else { 1266 Version myVersion = null; 1267 try { 1268 myVersion = Version.valueOf(myVersionString); 1269 } catch (final IllegalArgumentException | ParseException badVersion) { 1270 myVersion = null; 1271 } 1272 Version herVersion = null; 1273 try { 1274 herVersion = Version.valueOf(herVersionString); 1275 } catch (final IllegalArgumentException | ParseException badVersion) { 1276 herVersion = null; 1277 } 1278 if (myVersion == null) { 1279 if (herVersion != null) { 1280 return -1; 1281 } 1282 } else if (herVersion == null) { 1283 return 1; 1284 } else { 1285 return myVersion.compareTo(herVersion); 1286 } 1287 } 1288 1289 return 0; 1290 } 1291 1292 /** 1293 * Returns a hashcode for this {@link Entry} based off its 1294 * {@linkplain #getName() name} and {@linkplain #getVersion() 1295 * version}. 1296 * 1297 * @return a hashcode for this {@link Entry} 1298 * 1299 * @see #compareTo(ChartRepository.Index.Entry) 1300 * 1301 * @see #equals(Object) 1302 * 1303 * @see #getName() 1304 * 1305 * @see #getVersion() 1306 */ 1307 @Override 1308 public final int hashCode() { 1309 int hashCode = 17; 1310 1311 final Object name = this.getName(); 1312 int c = name == null ? 0 : name.hashCode(); 1313 hashCode = 37 * hashCode + c; 1314 1315 final Object version = this.getVersion(); 1316 c = version == null ? 0 : version.hashCode(); 1317 hashCode = 37 * hashCode + c; 1318 1319 return hashCode; 1320 } 1321 1322 /** 1323 * Returns {@code true} if the supplied {@link Object} is an 1324 * {@link Entry} and has a {@linkplain #getName() name} and 1325 * {@linkplain #getVersion() version} equal to those of this 1326 * {@link Entry}. 1327 * 1328 * @param other the {@link Object} to test; may be {@code null} 1329 * in which case {@code false} will be returned 1330 * 1331 * @return {@code true} if this {@link Entry} is equal to the 1332 * supplied {@link Object}; {@code false} otherwise 1333 * 1334 * @see #compareTo(ChartRepository.Index.Entry) 1335 * 1336 * @see #getName() 1337 * 1338 * @see #getVersion() 1339 * 1340 * @see #hashCode() 1341 */ 1342 @Override 1343 public final boolean equals(final Object other) { 1344 if (other == this) { 1345 return true; 1346 } else if (other instanceof Entry) { 1347 final Entry her = (Entry)other; 1348 1349 final Object myName = this.getName(); 1350 if (myName == null) { 1351 if (her.getName() != null) { 1352 return false; 1353 } 1354 } else if (!myName.equals(her.getName())) { 1355 return false; 1356 } 1357 1358 final Object myVersion = this.getVersion(); 1359 if (myVersion == null) { 1360 if (her.getVersion() != null) { 1361 return false; 1362 } 1363 } else if (!myVersion.equals(her.getVersion())) { 1364 return false; 1365 } 1366 1367 return true; 1368 } else { 1369 return false; 1370 } 1371 } 1372 1373 /** 1374 * Returns the {@link MetadataOrBuilder} that comprises most of 1375 * the contents of this {@link Entry}. 1376 * 1377 * <p>This method never returns {@code null}.</p> 1378 * 1379 * @return the {@link MetadataOrBuilder} that comprises most of 1380 * the contents of this {@link Entry}; never {@code null} 1381 */ 1382 public final MetadataOrBuilder getMetadataOrBuilder() { 1383 return this.metadata; 1384 } 1385 1386 /** 1387 * Returns the return value of invoking the {@link 1388 * MetadataOrBuilder#getName()} method on the {@link 1389 * MetadataOrBuilder} returned by this {@link Entry}'s {@link 1390 * #getMetadataOrBuilder()} method. 1391 * 1392 * <p>This method may return {@code null}.</p> 1393 * 1394 * @return this {@link Entry}'s name, or {@code null} 1395 * 1396 * @see MetadataOrBuilder#getName() 1397 */ 1398 public final String getName() { 1399 final MetadataOrBuilder metadata = this.getMetadataOrBuilder(); 1400 assert metadata != null; 1401 return metadata.getName(); 1402 } 1403 1404 /** 1405 * Returns the return value of invoking the {@link 1406 * MetadataOrBuilder#getVersion()} method on the {@link 1407 * MetadataOrBuilder} returned by this {@link Entry}'s {@link 1408 * #getMetadataOrBuilder()} method. 1409 * 1410 * <p>This method may return {@code null}.</p> 1411 * 1412 * @return this {@link Entry}'s version, or {@code null} 1413 * 1414 * @see MetadataOrBuilder#getVersion() 1415 */ 1416 public final String getVersion() { 1417 final MetadataOrBuilder metadata = this.getMetadataOrBuilder(); 1418 assert metadata != null; 1419 return metadata.getVersion(); 1420 } 1421 1422 /** 1423 * Returns a non-{@code null}, {@linkplain 1424 * Collections#unmodifiableSet(Set) immutable} {@link Set} of 1425 * {@link URI}s representing the URIs from which the Helm chart 1426 * described by this {@link Entry} may be downloaded. 1427 * 1428 * <p>This method never returns {@code null}.</p> 1429 * 1430 * @return a non-{@code null}, {@linkplain 1431 * Collections#unmodifiableSet(Set) immutable} {@link Set} of 1432 * {@link URI}s representing the URIs from which the Helm chart 1433 * described by this {@link Entry} may be downloaded 1434 * 1435 * @see #getFirstUri() 1436 */ 1437 public final Set<URI> getUris() { 1438 return this.uris; 1439 } 1440 1441 /** 1442 * A convenience method that returns the first {@link URI} in 1443 * the {@link Set} of {@link URI}s returned by the {@link 1444 * #getUris()} method. 1445 * 1446 * <p>This method may return {@code null}.</p> 1447 * 1448 * @return the {@linkplain SortedSet#first() first} {@link URI} 1449 * in the {@link Set} of {@link URI}s returned by the {@link 1450 * #getUris()} method, or {@code null} 1451 * 1452 * @see #getUris() 1453 */ 1454 public final URI getFirstUri() { 1455 final Set<URI> uris = this.getUris(); 1456 final URI returnValue; 1457 if (uris == null || uris.isEmpty()) { 1458 returnValue = null; 1459 } else { 1460 final Iterator<URI> iterator = uris.iterator(); 1461 if (iterator == null || !iterator.hasNext()) { 1462 returnValue = null; 1463 } else { 1464 returnValue = iterator.next(); 1465 } 1466 } 1467 return returnValue; 1468 } 1469 1470 /** 1471 * Returns the SHA-256 message digest, in hexadecimal-encoded 1472 * {@link String} form, associated with this {@link Entry}. 1473 * 1474 * <p>This method may return {@code null}.</p> 1475 * 1476 * @return the SHA-256 message digest, in hexadecimal-encoded 1477 * {@link String} form, associated with this {@link Entry}, or 1478 * {@code null} 1479 */ 1480 public final String getDigest() { 1481 return this.digest; 1482 } 1483 1484 /** 1485 * Returns a non-{@code null} {@link String} representation of 1486 * this {@link Entry}. 1487 * 1488 * @return a non-{@code null} {@link String} representation of 1489 * this {@link Entry} 1490 */ 1491 @Override 1492 public final String toString() { 1493 String name = this.getName(); 1494 if (name == null || name.isEmpty()) { 1495 name = "unnamed"; 1496 } 1497 return new StringBuilder(name).append(" ").append(this.getVersion()).toString(); 1498 } 1499 1500 /** 1501 * Computes a SHA-256 message digest of the bytes readable from 1502 * the supplied {@link InputStream} and returns the result of 1503 * {@linkplain DatatypeConverter#printHexBinary(byte[]) 1504 * hexadecimal-encoding it}. 1505 * 1506 * <p>This method never returns {@code null}.</p> 1507 * 1508 * @param inputStream the {@link InputStream} to read from; must 1509 * not be {@code null} 1510 * 1511 * @return a {@linkplain 1512 * DatatypeConverter#printHexBinary(byte[]) hexadecimal-encoded} 1513 * SHA-256 message digest; never {@code null} 1514 * 1515 * @exception NullPointerException if {@code inputStream} is 1516 * {@code null} 1517 * 1518 * @exception IOException if an input or output error occurs 1519 */ 1520 @Experimental 1521 public static final String getDigest(final InputStream inputStream) throws IOException { 1522 Objects.requireNonNull(inputStream); 1523 MessageDigest md = null; 1524 try { 1525 md = MessageDigest.getInstance("SHA-256"); 1526 } catch (final NoSuchAlgorithmException noSuchAlgorithmException) { 1527 // SHA-256 is guaranteed to exist. 1528 throw new InternalError(noSuchAlgorithmException); 1529 } 1530 assert md != null; 1531 final ByteBuffer buffer = toByteBuffer(inputStream); 1532 assert buffer != null; 1533 md.update(buffer); 1534 return DatatypeConverter.printHexBinary(md.digest()); 1535 } 1536 1537 /** 1538 * Returns a {@link ByteBuffer} representing the supplied {@link 1539 * InputStream}. 1540 * 1541 * <p>This method never returns {@code null}.</p> 1542 * 1543 * @param stream the {@link InputStream} to represent; may be 1544 * {@code null} 1545 * 1546 * @return a non-{@code null} {@link ByteBuffer} 1547 * 1548 * @exception IOException if an input or output error occurs 1549 */ 1550 private static final ByteBuffer toByteBuffer(final InputStream stream) throws IOException { 1551 return ByteBuffer.wrap(read(stream)); 1552 } 1553 1554 /** 1555 * Fully reads the supplied {@link InputStream} into a {@code 1556 * byte} array and returns it. 1557 * 1558 * <p>This method never returns {@code null}.</p> 1559 * 1560 * @param stream the {@link InputStream} to read; may be {@code 1561 * null} 1562 * 1563 * @return a non-{@code null} {@code byte} array containing the 1564 * readable contents of the supplied {@link InputStream} 1565 * 1566 * @exception IOException if an input or output error occurs 1567 */ 1568 private static final byte[] read(final InputStream stream) throws IOException { 1569 byte[] returnValue = null; 1570 if (stream == null) { 1571 returnValue = new byte[0]; 1572 } else { 1573 try (final ByteArrayOutputStream buffer = new ByteArrayOutputStream()) { 1574 int bytesRead; 1575 final byte[] byteArray = new byte[4096]; 1576 while ((bytesRead = stream.read(byteArray, 0, byteArray.length)) != -1) { 1577 buffer.write(byteArray, 0, bytesRead); 1578 } 1579 buffer.flush(); 1580 returnValue = buffer.toByteArray(); 1581 } 1582 } 1583 return returnValue; 1584 } 1585 1586 1587 } 1588 1589 } 1590 1591}