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