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