001/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- 002 * 003 * Copyright © 2017 MicroBean. 004 * 005 * Licensed under the Apache License, Version 2.0 (the "License"); 006 * you may not use this file except in compliance with the License. 007 * You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 014 * implied. See the License for the specific language governing 015 * permissions and limitations under the License. 016 */ 017package org.microbean.helm.chart; 018 019import java.io.IOException; 020import java.io.InputStream; 021 022import java.util.Collection; 023import java.util.Collections; 024import java.util.Comparator; 025import java.util.Iterator; 026import java.util.Map; 027import java.util.Map.Entry; 028import java.util.NavigableMap; 029import java.util.NavigableSet; 030import java.util.Objects; 031import java.util.TreeMap; 032import java.util.TreeSet; 033 034import java.util.regex.Matcher; 035import java.util.regex.Pattern; 036 037import java.util.zip.GZIPInputStream; 038 039import com.google.protobuf.Any; 040import com.google.protobuf.ByteString; 041 042import hapi.chart.ChartOuterClass.Chart; 043import hapi.chart.ConfigOuterClass.Config; 044import hapi.chart.MetadataOuterClass.Maintainer; 045import hapi.chart.MetadataOuterClass.Metadata; 046import hapi.chart.TemplateOuterClass.Template; 047 048import org.kamranzafar.jtar.TarInputStream; 049 050import org.yaml.snakeyaml.Yaml; 051 052/** 053 * A partial {@link AbstractChartLoader} implementation that is capable of 054 * loading a Helm-compatible chart from any source that is {@linkplain 055 * #toNamedInputStreamEntries(Object) convertible into an 056 * <code>Iterable</code> of <code>InputStream</code>s indexed by their 057 * name}. 058 * 059 * @param <T> the type of source from which this {@link 060 * StreamOrientedChartLoader} is capable of loading Helm charts 061 * 062 * @author <a href="https://about.me/lairdnelson" 063 * target="_parent">Laird Nelson</a> 064 * 065 * @see #toNamedInputStreamEntries(Object) 066 */ 067public abstract class StreamOrientedChartLoader<T> extends AbstractChartLoader<T> { 068 069 070 /* 071 * Static fields. 072 */ 073 074 075 /** 076 * A {@link Pattern} that matches the trailing component of a file 077 * name in a valid Helm chart structure, provided it is not preceded 078 * in its path components by either {@code /templates/} or {@code 079 * /charts/}, and stores it as capturing group {@code 1}. 080 * 081 * <h2>Examples</h2> 082 * 083 * <ul> 084 * 085 * <li>Given {@code wordpress/README.md}, yields {@code 086 * README.md}.</li> 087 * 088 * <li>Given {@code wordpress/charts/mariadb/README.md}, yields 089 * nothing.</li> 090 * 091 * <li>Given {@code wordpress/templates/deployment.yaml}, yields 092 * nothing.</li> 093 * 094 * <li>Given {@code wordpress/subdirectory/file.txt}, yields {@code 095 * subdirectory/file.txt}.</li> 096 * 097 * </ul> 098 */ 099 private static final Pattern fileNamePattern = Pattern.compile("^/*[^/]+(?!.*/(?:charts|templates)/)/(.+)$"); 100 101 private static final Pattern templateFileNamePattern = Pattern.compile("^.+/(templates/[^/]+)$"); 102 103 private static final Pattern subchartFileNamePattern = Pattern.compile("^.+/charts/[^._][^/]+/(.+)$"); 104 105 /** 106 * <p>Please note that the lack of anchors ({@code ^} or {@code $}) 107 * and the leading "{@code .*?}" in this pattern's {@linkplain 108 * Pattern#toString() value} are deliberate choices.</p> 109 */ 110 private static final Pattern nonGreedySubchartsPattern = Pattern.compile(".*?/charts/[^/]+"); 111 112 private static final Pattern chartNamePattern = Pattern.compile("^.+/charts/([^/]+).*$"); 113 114 private static final Pattern basenamePattern = Pattern.compile("^.*/?(.*)$"); 115 116 117 /* 118 * Constructors. 119 */ 120 121 122 /** 123 * Creates a new {@link StreamOrientedChartLoader}. 124 */ 125 protected StreamOrientedChartLoader() { 126 super(); 127 } 128 129 130 /* 131 * Instance methods. 132 */ 133 134 135 /** 136 * Converts the supplied {@code source} into an {@link Iterable} of 137 * {@link Entry} instances whose {@linkplain Entry#getKey() keys} 138 * are names and whose {@linkplain Entry#getValue() values} are 139 * corresponding {@link InputStream}s. 140 * 141 * <p>Implementations of this method must not return {@code 142 * null}.</p> 143 * 144 * @param source the source to convert; must not be {@code null} 145 * 146 * @return an {@link Iterable} of suitable {@link Entry} instances; 147 * never {@code null} 148 * 149 * @exception NullPointerException if {@code source} is {@code null} 150 * 151 * @exception IOException if an error occurs while converting 152 */ 153 protected abstract Iterable<? extends Entry<? extends String, ? extends InputStream>> toNamedInputStreamEntries(final T source) throws IOException; 154 155 /** 156 * Creates a new {@link Chart} from the supplied {@code source} in 157 * some manner and returns it. 158 * 159 * <p>This method never returns {@code null}. 160 * 161 * <p>This method calls the {@link 162 * #load(hapi.chart.ChartOuterClass.Chart.Builder, Iterable)} method 163 * with the return value of the {@link 164 * #toNamedInputStreamEntries(Object)} method.</p> 165 * 166 * @param source the source object from which to load a new {@link 167 * Chart}; must not be {@code null} 168 * 169 * @return a new {@link Chart}; never {@code null} 170 * 171 * @exception NullPointerException if {@code source} is {@code null} 172 * 173 * @exception IllegalStateException if the {@link 174 * #load(hapi.chart.ChartOuterClass.Chart.Builder, Iterable)} method 175 * returns {@code null} 176 * 177 * @exception IOException if a problem is encountered while creating 178 * the {@link Chart} to return 179 * 180 * @see #toNamedInputStreamEntries(Object) 181 * 182 * @see #load(hapi.chart.ChartOuterClass.Chart.Builder, Iterable) 183 */ 184 @Override 185 public Chart.Builder load(final Chart.Builder parent, final T source) throws IOException { 186 Objects.requireNonNull(source); 187 final Chart.Builder returnValue = this.load(parent, toNamedInputStreamEntries(source)); 188 if (returnValue == null) { 189 throw new IllegalStateException("load(toNamedInputStreamEntries(source)) == null; source: " + source); 190 } 191 return returnValue; 192 } 193 194 /** 195 * Creates a new {@link Chart} from the supplied notional set of 196 * named {@link InputStream}s and returns it. 197 * 198 * <p>This method never returns {@code null}. 199 * 200 * <p>This method is called by the {@link #load(Object)} method.</p> 201 * 202 * @param entrySet the {@link Iterable} of {@link Entry} instances 203 * normally returned by the {@link 204 * #toNamedInputStreamEntries(Object)} method; must not be {@code 205 * null} 206 * 207 * @return a new {@link Chart}; never {@code null} 208 * 209 * @exception NullPointerException if {@code entrySet} is {@code 210 * null} 211 * 212 * @exception IOException if a problem is encountered while creating 213 * the {@link Chart} to return 214 * 215 * @see #toNamedInputStreamEntries(Object) 216 * 217 * @see #load(Object) 218 */ 219 public Chart.Builder load(final Chart.Builder parent, final Iterable<? extends Entry<? extends String, ? extends InputStream>> entrySet) throws IOException { 220 Objects.requireNonNull(entrySet); 221 final Chart.Builder rootBuilder; 222 if (parent == null) { 223 rootBuilder = Chart.newBuilder(); 224 } else { 225 rootBuilder = parent; 226 } 227 assert rootBuilder != null; 228 final NavigableMap<String, Chart.Builder> chartBuilders = new TreeMap<>(new ChartPathComparator()); 229 // XXX TODO FIXME: do we really want to say the root is null? 230 // Or should it always be a path named after the chart? 231 chartBuilders.put(null, rootBuilder); 232 for (final Entry<? extends String, ? extends InputStream> entry : entrySet) { 233 if (entry != null) { 234 this.addFile(chartBuilders, entry.getKey(), entry.getValue()); 235 } 236 } 237 return rootBuilder; 238 } 239 240 private final void addFile(final NavigableMap<String, Chart.Builder> chartBuilders, final String path, final InputStream stream) throws IOException { 241 Objects.requireNonNull(chartBuilders); 242 Objects.requireNonNull(path); 243 Objects.requireNonNull(stream); 244 245 final Chart.Builder builder = getChartBuilder(chartBuilders, path); 246 if (builder == null) { 247 throw new IllegalStateException(); 248 } 249 250 final Object templateBuilder; 251 final boolean subchartFile; 252 String fileName = getTemplateFileName(path); 253 if (fileName == null) { 254 // Not a template file, not even in a subchart. 255 templateBuilder = null; 256 fileName = getSubchartFileName(path); 257 if (fileName == null) { 258 // Not a subchart file or a template file so add it to the 259 // root builder. 260 subchartFile = false; 261 fileName = getOrdinaryFileName(path); 262 } else { 263 subchartFile = true; 264 } 265 } else { 266 subchartFile = false; 267 templateBuilder = this.createTemplateBuilder(builder, stream, fileName); 268 } 269 assert fileName != null; 270 if (templateBuilder == null) { 271 switch (fileName) { 272 case "Chart.yaml": 273 this.installMetadata(builder, stream); 274 break; 275 case "values.yaml": 276 this.installConfig(builder, stream); 277 break; 278 default: 279 if (subchartFile) { 280 if (fileName.endsWith(".prov")) { 281 // The intent in the Go code, despite its implementation, 282 // seems to be that a charts/foo.prov file should be 283 // treated as an ordinary file whose name is, well, 284 // charts/foo.prov, no matter how deep that directory 285 // hierarchy is, and despite that fact that the .prov file 286 // appears in a charts directory, which normally indicates 287 // the presence of a subchart. 288 // 289 // So ordinarily we'd be in a subchart here. Let's say we're: 290 // 291 // wordpress/charts/argle/charts/foo/charts/bar/grob/foobish/.blatz.prov. 292 // 293 // We don't want the Chart.Builder associated with 294 // wordpress/charts/argle/charts/foo/charts/bar. We want 295 // the Chart.Builder associated with 296 // wordpress/charts/argle/charts/foo. And we want the 297 // filename added to that builder to be 298 // charts/bar/grob/foobish/.blatz.prov. Let's take 299 // advantage of the sorted nature of the chartBuilders Map 300 // and look for our parent that way. 301 final Entry<String, Chart.Builder> parentChartBuilderEntry = chartBuilders.lowerEntry(path); 302 if (parentChartBuilderEntry == null) { 303 throw new IllegalStateException("chartBuilders.lowerEntry(path) == null; path: " + path); 304 } 305 final String parentChartPath = parentChartBuilderEntry.getKey(); 306 final Chart.Builder parentChartBuilder = parentChartBuilderEntry.getValue(); 307 if (parentChartBuilder == null) { 308 throw new IllegalStateException("chartBuilders.lowerEntry(path).getValue() == null; path: " + path); 309 } 310 final int prefixLength = ((parentChartPath == null ? "" : parentChartPath) + "/").length(); 311 assert path.length() > prefixLength; 312 this.installAny(parentChartBuilder, stream, path.substring(prefixLength)); 313 } else if (!(fileName.startsWith("_") || fileName.startsWith(".")) && 314 fileName.endsWith(".tgz") && 315 fileName.equals(basename(fileName))) { 316 assert fileName.indexOf('/') < 0; 317 // A subchart *file* (i.e. not a directory) that is not a 318 // .prov file, that is immediately beneath charts, that 319 // doesn't start with '.' or '_', and that ends with .tgz. 320 // Treat it as a tarball. 321 // 322 // So: wordpress/charts/foo.tgz 323 // Not: wordpress/charts/.foo.tgz 324 // Not: wordpress/charts/_foo.tgz 325 // Not: wordpress/charts/foo 326 // Not: wordpress/charts/bar/foo.tgz 327 // Not: wordpress/charts/_bar/foo.tgz 328 Chart.Builder subchartBuilder = null; 329 try (final TarInputStream tarInputStream = new TarInputStream(new GZIPInputStream(new NonClosingInputStream(stream)))) { 330 subchartBuilder = new TapeArchiveChartLoader().load(builder, tarInputStream); 331 } 332 if (subchartBuilder == null) { 333 throw new IllegalStateException("load(builder, tarInputStream) == null; path: " + path); 334 } 335 // builder.addDependencies(subchart); 336 } else { 337 // Not a .prov file under charts, nor a .tgz file, just a 338 // regular subchart file. 339 this.installAny(builder, stream, fileName); 340 } 341 } else { 342 assert !subchartFile; 343 // Not a subchart file or a template 344 this.installAny(builder, stream, fileName); 345 } 346 break; 347 } 348 } 349 } 350 351 static final String getOrdinaryFileName(final String path) { 352 String returnValue = null; 353 if (path != null) { 354 final Matcher fileMatcher = fileNamePattern.matcher(path); 355 assert fileMatcher != null; 356 if (fileMatcher.find()) { 357 returnValue = fileMatcher.group(1); 358 } 359 } 360 return returnValue; 361 } 362 363 static final String getSubchartFileName(final String path) { 364 String returnValue = null; 365 if (path != null) { 366 final Matcher subchartMatcher = subchartFileNamePattern.matcher(path); 367 assert subchartMatcher != null; 368 if (subchartMatcher.find()) { 369 returnValue = subchartMatcher.group(1); 370 } 371 } 372 return returnValue; 373 374 } 375 376 static final String getTemplateFileName(final String path) { 377 String returnValue = null; 378 if (path != null) { 379 final Matcher templateMatcher = templateFileNamePattern.matcher(path); 380 assert templateMatcher != null; 381 if (templateMatcher.find()) { 382 returnValue = templateMatcher.group(1); 383 } 384 } 385 return returnValue; 386 } 387 388 /** 389 * Given a semantic solidus-separated {@code chartPath} representing 390 * a file or logical directory within a chart, returns the proper 391 * {@link Chart.Builder} corresponding to that path. 392 * 393 * <p>This method never returns {@code null}.</p> 394 * 395 * <p>Any intermediate {@link Chart.Builder}s will also be created 396 * and properly parented.</p> 397 * 398 * @param chartBuilders a {@link Map} of {@link Chart.Builder} 399 * instances indexed by paths; must not be {@code null}; may be 400 * updated by this method 401 * 402 * @param chartPath a solidus-separated {@link String} representing 403 * a file or directory within a chart; must not be {@code null} 404 * 405 * @return a {@link Chart.Builder}; never {@code null} 406 * 407 * @exception NullPointerException if either {@code chartBuilders} 408 * or {@code chartPath} is {@code null} 409 */ 410 private static final Chart.Builder getChartBuilder(final Map<String, Chart.Builder> chartBuilders, final String chartPath) { 411 Objects.requireNonNull(chartBuilders); 412 Objects.requireNonNull(chartPath); 413 Chart.Builder rootBuilder = chartBuilders.get(null); 414 if (rootBuilder == null) { 415 rootBuilder = Chart.newBuilder(); 416 chartBuilders.put(null, rootBuilder); 417 } 418 assert rootBuilder != null; 419 Chart.Builder returnValue = rootBuilder; 420 final Collection<? extends String> chartPaths = toSubcharts(chartPath); 421 if (chartPaths != null && !chartPaths.isEmpty()) { 422 for (final String path : chartPaths) { 423 // By contract, shallowest path comes first, so 424 // foobar/charts/wordpress comes before, say, 425 // foobar/charts/wordpress/charts/mysql 426 Chart.Builder builder = chartBuilders.get(path); 427 if (builder == null) { 428 builder = createSubchartBuilder(returnValue, path); 429 assert builder != null; 430 chartBuilders.put(path, builder); 431 } 432 assert builder != null; 433 returnValue = builder; 434 } 435 } 436 assert returnValue != null; 437 return returnValue; 438 } 439 440 /** 441 * Given, e.g., {@code wordpress/charts/argle/charts/frob/foo.txt}, 442 * yield {@code [ wordpress/charts/argle, 443 * wordpress/charts/argle/charts/frob ]}. 444 * 445 * <p>This method never returns {@code null}.</p> 446 * 447 * @param chartPath the "relative" solidus-separated path 448 * identifying some chart resource; must not be {@code null} 449 * 450 * @return a {@link NavigableSet} of chart paths in ascending 451 * subchart hierarchy order; never {@code null} 452 */ 453 static final NavigableSet<String> toSubcharts(final String chartPath) { 454 Objects.requireNonNull(chartPath); 455 final NavigableSet<String> returnValue = new TreeSet<>(new ChartPathComparator()); 456 final Matcher matcher = nonGreedySubchartsPattern.matcher(chartPath); 457 if (matcher != null) { 458 while (matcher.find()) { 459 returnValue.add(chartPath.substring(0, matcher.end())); 460 } 461 } 462 return returnValue; 463 } 464 465 private static final Chart.Builder createSubchartBuilder(final Chart.Builder parentBuilder, final String chartPath) { 466 Objects.requireNonNull(parentBuilder); 467 Chart.Builder returnValue = null; 468 final String chartName = getChartName(chartPath); 469 if (chartName != null) { 470 returnValue = parentBuilder.addDependenciesBuilder(); 471 assert returnValue != null; 472 final Metadata.Builder builder = returnValue.getMetadataBuilder(); 473 assert builder != null; 474 builder.setName(chartName); 475 } 476 return returnValue; 477 } 478 479 private static final String getChartName(final String chartPath) { 480 String returnValue = null; 481 if (chartPath != null) { 482 final Matcher matcher = chartNamePattern.matcher(chartPath); 483 assert matcher != null; 484 if (matcher.find()) { 485 returnValue = matcher.group(1); 486 } 487 } 488 return returnValue; 489 } 490 491 private static final String basename(final String path) { 492 String returnValue = null; 493 if (path != null) { 494 final Matcher matcher = basenamePattern.matcher(path); 495 assert matcher != null; 496 if (matcher.find()) { 497 returnValue = matcher.group(1); 498 } 499 } 500 return returnValue; 501 } 502 503 504 /* 505 * Utility methods. 506 */ 507 508 509 /** 510 * Installs a {@link Config} object, represented by the supplied 511 * {@link InputStream}, into the supplied {@link 512 * hapi.chart.ChartOuterClass.Chart.Builder Chart.Builder}. 513 * 514 * @param chartBuilder the {@link 515 * hapi.chart.ChartOuterClass.Chart.Builder Chart.Builder} to 516 * affect; must not be {@code null} 517 * 518 * @param stream an {@link InputStream} representing <a 519 * href="https://docs.helm.sh/developing_charts/#values-files">valid 520 * values file contents</a> as defined by <a 521 * href="https://docs.helm.sh/developing_charts/#values-files">the 522 * chart specification</a>; must not be {@code null} 523 * 524 * @exception NullPointerException if {@code chartBuilder} or {@code 525 * stream} is {@code null} 526 * 527 * @exception IOException if there was a problem reading from the 528 * supplied {@link InputStream} 529 * 530 * @see hapi.chart.ChartOuterClass.Chart.Builder#getValuesBuilder() 531 * 532 * @see hapi.chart.ConfigOuterClass.Config.Builder#setRawBytes(ByteString) 533 */ 534 protected void installConfig(final Chart.Builder chartBuilder, final InputStream stream) throws IOException { 535 Objects.requireNonNull(chartBuilder); 536 Objects.requireNonNull(stream); 537 Config returnValue = null; 538 final Config.Builder builder = chartBuilder.getValuesBuilder(); 539 assert builder != null; 540 final ByteString rawBytes = ByteString.readFrom(stream); 541 assert rawBytes != null; 542 builder.setRawBytes(rawBytes); 543 } 544 545 /** 546 * Installs a {@link Metadata} object, represented by the supplied 547 * {@link InputStream}, into the supplied {@link 548 * hapi.chart.ChartOuterClass.Chart.Builder Chart.Builder}. 549 * 550 * @param chartBuilder the {@link 551 * hapi.chart.ChartOuterClass.Chart.Builder Chart.Builder} to 552 * affect; must not be {@code null} 553 * 554 * @param stream an {@link InputStream} representing <a 555 * href="https://docs.helm.sh/developing_charts/#the-chart-yaml-file">valid 556 * {@code Chart.yaml} contents</a> as defined by <a 557 * href="https://docs.helm.sh/developing_charts/#the-chart-yaml-file">the 558 * chart specification</a>; must not be {@code null} 559 * 560 * @exception NullPointerException if {@code chartBuilder} or {@code 561 * stream} is {@code null} 562 * 563 * @exception IOException if there was a problem reading from the 564 * supplied {@link InputStream} 565 * 566 * @see hapi.chart.ChartOuterClass.Chart.Builder#getMetadataBuilder() 567 * 568 * @see hapi.chart.MetadataOuterClass.Metadata.Builder 569 */ 570 protected void installMetadata(final Chart.Builder chartBuilder, final InputStream stream) throws IOException { 571 Objects.requireNonNull(chartBuilder); 572 Objects.requireNonNull(stream); 573 Metadata returnValue = null; 574 final Map<?, ?> map = new Yaml().loadAs(stream, Map.class); 575 assert map != null; 576 final Metadata.Builder metadataBuilder = chartBuilder.getMetadataBuilder(); 577 assert metadataBuilder != null; 578 @SuppressWarnings("unchecked") 579 final Iterable<String> keywords = (Iterable<String>)map.get("keywords"); 580 if (keywords != null) { 581 metadataBuilder.addAllKeywords(keywords); 582 } 583 @SuppressWarnings("unchecked") 584 final Iterable<? extends Map<?, ?>> maintainers = (Iterable<? extends Map<?, ?>>)map.get("maintainers"); 585 if (maintainers != null) { 586 for (final Map<?, ?> maintainer : maintainers) { 587 if (maintainer != null) { 588 final Maintainer.Builder maintainerBuilder = metadataBuilder.addMaintainersBuilder(); 589 assert maintainerBuilder != null; 590 maintainerBuilder.setName((String)maintainer.get("name")); 591 maintainerBuilder.setEmail((String)maintainer.get("email")); 592 } 593 } 594 } 595 @SuppressWarnings("unchecked") 596 final Iterable<String> sources = (Iterable<String>)map.get("sources"); 597 if (sources != null) { 598 metadataBuilder.addAllSources(sources); 599 } 600 final String name = (String)map.get("name"); 601 if (name != null) { 602 metadataBuilder.setName(name); 603 } 604 final String version = (String)map.get("version"); 605 if (version != null) { 606 metadataBuilder.setVersion(version); 607 } 608 final String description = (String)map.get("description"); 609 if (description != null) { 610 metadataBuilder.setDescription(description); 611 } 612 final String engine = (String)map.get("engine"); 613 if (engine != null) { 614 metadataBuilder.setEngine(engine); 615 } 616 final String icon = (String)map.get("icon"); 617 if (icon != null) { 618 metadataBuilder.setIcon(icon); 619 } 620 final String appVersion = (String)map.get("appVersion"); 621 if (appVersion != null) { 622 metadataBuilder.setAppVersion(appVersion); 623 } 624 final String tillerVersion = (String)map.get("tillerVersion"); 625 if (tillerVersion != null) { 626 metadataBuilder.setTillerVersion(tillerVersion); 627 } 628 metadataBuilder.setDeprecated("true".equals(String.valueOf(map.get("deprecated")))); 629 } 630 631 /** 632 * {@linkplain 633 * hapi.chart.ChartOuterClass.Chart.Builder#addTemplatesBuilder() 634 * Creates a new} {@link 635 * hapi.chart.TemplateOuterClass.Template.Builder} {@linkplain 636 * hapi.chart.TemplateOuterClass.Template.Builder#setData(ByteString) 637 * from the contents of the supplied <code>InputStream</code>}, 638 * {@linkplain 639 * hapi.chart.TemplateOuterClass.Template.Builder#setName(String) 640 * with the supplied <code>name</code>}, and returns it. 641 * 642 * <p>This method never returns {@code null}.</p> 643 * 644 * @param chartBuilder a {@link 645 * hapi.chart.ChartOuterClass.Chart.Builder} whose {@link 646 * hapi.chart.ChartOuterClass.Chart.Builder#addTemplatesBuilder()} 647 * method will be called to create the new {@link 648 * hapi.chart.TemplateOuterClass.Template.Builder} instance; must 649 * not be {@code null} 650 * 651 * @param stream an {@link InputStream} containing <a 652 * href="https://docs.helm.sh/developing_charts/#template-files">valid 653 * template contents</a> as defined by the <a 654 * href="https://docs.helm.sh/developing_charts/#template-files">chart 655 * specification</a>; must not be {@code null} 656 * 657 * @param name the name for the new {@link Template} that will 658 * ultimately reside within the chart; must not be {@code null} 659 * 660 * @return a new {@link 661 * hapi.chart.TemplateOuterClass.Template.Builder}; never {@code 662 * null} 663 * 664 * @exception NullPointerException if {@code chartBuilder}, {@code 665 * stream} or {@code name} is {@code null} 666 * 667 * @exception IOException if there was a problem reading from the 668 * supplied {@link InputStream} 669 * 670 * @see hapi.chart.TemplateOuterClass.Template.Builder 671 */ 672 protected Template.Builder createTemplateBuilder(final Chart.Builder chartBuilder, final InputStream stream, final String name) throws IOException { 673 Objects.requireNonNull(chartBuilder); 674 Objects.requireNonNull(stream); 675 Objects.requireNonNull(name); 676 final Template.Builder builder = chartBuilder.addTemplatesBuilder(); 677 assert builder != null; 678 builder.setName(name); 679 final ByteString data = ByteString.readFrom(stream); 680 assert data != null; 681 assert data.isValidUtf8(); 682 builder.setData(data); 683 return builder; 684 } 685 686 /** 687 * Installs an {@link Any} object, representing an arbitrary chart 688 * file with the supplied {@code name} and represented by the 689 * supplied {@link InputStream}, into the supplied {@link 690 * hapi.chart.ChartOuterClass.Chart.Builder Chart.Builder}. 691 * 692 * @param chartBuilder the {@link 693 * hapi.chart.ChartOuterClass.Chart.Builder Chart.Builder} to 694 * affect; must not be {@code null} 695 * 696 * @param stream an {@link InputStream} representing <a 697 * href="https://docs.helm.sh/developing_charts/">valid chart file 698 * contents</a> as defined by <a 699 * href="https://docs.helm.sh/developing_charts/">the chart 700 * specification</a>; must not be {@code null} 701 * 702 * @param name the name of the file within the chart; must not be 703 * {@code null} 704 * 705 * @exception NullPointerException if {@code chartBuilder} or {@code 706 * stream} or {@code name} is {@code null} 707 * 708 * @exception IOException if there was a problem reading from the 709 * supplied {@link InputStream} 710 * 711 * @see hapi.chart.ChartOuterClass.Chart.Builder#addFilesBuilder() 712 */ 713 protected void installAny(final Chart.Builder chartBuilder, final InputStream stream, final String name) throws IOException { 714 Objects.requireNonNull(chartBuilder); 715 Objects.requireNonNull(stream); 716 Objects.requireNonNull(name); 717 Any returnValue = null; 718 final Any.Builder builder = chartBuilder.addFilesBuilder(); 719 assert builder != null; 720 builder.setTypeUrl(name); 721 final ByteString fileContents = ByteString.readFrom(stream); 722 assert fileContents != null; 723 assert fileContents.isValidUtf8(); 724 builder.setValue(fileContents); 725 } 726 727 728 /* 729 * Inner and nested classes. 730 */ 731 732 733 /** 734 * An {@link Iterable} implementation that {@linkplain #iterator() 735 * returns an empty <code>Iterator</code>}. 736 * 737 * @author <a href="https://about.me/lairdnelson" 738 * target="_parent">Laird Nelson</a> 739 */ 740 static final class EmptyIterable implements Iterable<Entry<String, InputStream>> { 741 742 743 /* 744 * Constructors. 745 */ 746 747 748 /** 749 * Creates a new {@link EmptyIterable}. 750 */ 751 EmptyIterable() { 752 super(); 753 } 754 755 756 /* 757 * Instance methods. 758 */ 759 760 761 /** 762 * Returns the return value of the {@link 763 * Collections#emptyIterator()} method when invoked. 764 * 765 * <p>This method never returns {@code null}.</p> 766 * 767 * @return an empty {@link Iterator}; never {@code null} 768 */ 769 @Override 770 public final Iterator<Entry<String, InputStream>> iterator() { 771 return Collections.emptyIterator(); 772 } 773 774 } 775 776 777 778 private static final class ChartPathComparator implements Comparator<String> { 779 780 private ChartPathComparator() { 781 super(); 782 } 783 784 @Override 785 public final int compare(final String chartPath1, final String chartPath2) { 786 if (chartPath1 == null) { 787 if (chartPath2 == null) { 788 return 0; 789 } else { 790 return -1; // nulls go to the left 791 } 792 } else if (chartPath1.equals(chartPath2)) { 793 return 0; 794 } else if (chartPath2 == null) { 795 return 1; 796 } else { 797 final int chartPath1Length = chartPath1.length(); 798 final int chartPath2Length = chartPath2.length(); 799 if (chartPath1Length == chartPath2Length) { 800 return chartPath1.compareTo(chartPath2); 801 } else if (chartPath1Length > chartPath2Length) { 802 return 1; 803 } else { 804 return -1; 805 } 806 } 807 } 808 809 } 810 811}