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