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.beans.IntrospectionException; 020import java.beans.PropertyDescriptor; 021import java.beans.SimpleBeanInfo; 022 023import java.util.ArrayList; 024import java.util.Collection; 025import java.util.HashMap; 026import java.util.List; 027import java.util.ListIterator; 028import java.util.Map; 029import java.util.Objects; 030 031import java.util.regex.Pattern; 032 033import com.github.zafarkhaja.semver.Parser; 034import com.github.zafarkhaja.semver.Version; 035 036import com.github.zafarkhaja.semver.expr.Expression; 037import com.github.zafarkhaja.semver.expr.ExpressionParser; 038 039import com.google.protobuf.Any; 040import com.google.protobuf.ByteString; 041 042import hapi.chart.ChartOuterClass.Chart; 043import hapi.chart.ChartOuterClass.ChartOrBuilder; 044import hapi.chart.ConfigOuterClass.Config; 045import hapi.chart.ConfigOuterClass.ConfigOrBuilder; 046import hapi.chart.MetadataOuterClass.Metadata; 047import hapi.chart.MetadataOuterClass.MetadataOrBuilder; 048 049import org.yaml.snakeyaml.Yaml; 050 051/** 052 * A specification of a <a 053 * href="https://docs.helm.sh/developing_charts/#chart-dependencies">Helm 054 * chart's dependencies</a>; not normally used directly by end users. 055 * 056 * <p>Helm charts support a {@code requirements.yaml} resource, in 057 * YAML format, whose sole member is a {@code dependencies} list. 058 * This class represents that resource.</p> 059 * 060 * <h2>Thread Safety</h2> 061 * 062 * <p>Instances of this class are <strong>not</strong> suitable for 063 * concurrent access by multiple threads.</p> 064 * 065 * @author <a href="https://about.me/lairdnelson/" 066 * target="_parent">Laird Nelson</a> 067 */ 068public final class Requirements { 069 070 071 /* 072 * Instance fields. 073 */ 074 075 076 /** 077 * The {@link Collection} of {@link Dependency} instances that 078 * comprises this {@link Requirements}. 079 * 080 * <p>This field may be {@code null}.</p> 081 */ 082 private Collection<Dependency> dependencies; 083 084 085 /* 086 * Constructors. 087 */ 088 089 090 /** 091 * Creates a new {@link Requirements}. 092 */ 093 public Requirements() { 094 super(); 095 } 096 097 098 /* 099 * Instance methods. 100 */ 101 102 103 /** 104 * Returns {@code true} if this {@link Requirements} has no {@link 105 * Dependency} instances. 106 * 107 * @return {@code true} if this {@link Requirements} is empty; 108 * {@code false} otherwise 109 */ 110 public final boolean isEmpty() { 111 return this.dependencies == null || this.dependencies.isEmpty(); 112 } 113 114 /** 115 * Returns the {@link Collection} of {@link Dependency} instances 116 * comprising this {@link Requirements}. 117 * 118 * <p>This method may return {@code null}.</p> 119 * 120 * @see #setDependencies(Collection) 121 */ 122 public final Collection<Dependency> getDependencies() { 123 return this.dependencies; 124 } 125 126 /** 127 * Installs the {@link Collection} of {@link Dependency} instances 128 * comprising this {@link Requirements}. 129 * 130 * @param dependencies the {@link Collection} of {@link Dependency} 131 * instances that will comprise this {@link Requirements}; may be 132 * {@code null}; not copied or cloned 133 * 134 * @see #getDependencies() 135 */ 136 public final void setDependencies(final Collection<Dependency> dependencies) { 137 this.dependencies = dependencies; 138 } 139 140 private final void applyEnablementRules(final Map<String, Object> values) { 141 this.processTags(values); 142 this.processConditions(values); 143 } 144 145 private final void processTags(final Map<String, Object> values) { 146 final Collection<Dependency> dependencies = this.getDependencies(); 147 if (dependencies != null && !dependencies.isEmpty()) { 148 for (final Dependency dependency : dependencies) { 149 if (dependency != null) { 150 dependency.processTags(values); 151 } 152 } 153 } 154 155 } 156 157 private final void processConditions(final Map<String, Object> values) { 158 final Collection<Dependency> dependencies = this.getDependencies(); 159 if (dependencies != null && !dependencies.isEmpty()) { 160 for (final Dependency dependency : dependencies) { 161 if (dependency != null) { 162 dependency.processConditions(values); 163 } 164 } 165 } 166 } 167 168 169 /* 170 * Static methods. 171 */ 172 173 174 /** 175 * Applies rules around <a 176 * href="https://docs.helm.sh/developing_charts/#importing-child-values-via-requirements-yaml">importing 177 * subchart values into the parent chart's values</a>. 178 * 179 * @param chartBuilder the {@link Chart.Builder} to work on; must 180 * not be {@code null} 181 * 182 * @exception NullPointerException if {@code chartBuilder} is {@code 183 * null} 184 */ 185 static final Chart.Builder processImportValues(final Chart.Builder chartBuilder) { 186 Objects.requireNonNull(chartBuilder); 187 final List<? extends Chart.Builder> flattenedCharts = Charts.flatten(chartBuilder); 188 if (flattenedCharts != null) { 189 assert !flattenedCharts.isEmpty(); 190 final ListIterator<? extends Chart.Builder> listIterator = flattenedCharts.listIterator(flattenedCharts.size()); 191 assert listIterator != null; 192 while (listIterator.hasPrevious()) { 193 final Chart.Builder chart = listIterator.previous(); 194 assert chart != null; 195 processSingleChartImportValues(chart); 196 } 197 } 198 return chartBuilder; 199 } 200 201 // Ported from requirements.go processImportValues(). 202 private static final Chart.Builder processSingleChartImportValues(final Chart.Builder chartBuilder) { 203 Objects.requireNonNull(chartBuilder); 204 205 Chart.Builder returnValue = null; 206 207 final Map<String, Object> canonicalValues = Configs.toDefaultValuesMap(chartBuilder); 208 209 Map<String, Object> combinedValues = new HashMap<>(); 210 final Requirements requirements = fromChartOrBuilder(chartBuilder); 211 if (requirements != null) { 212 final Collection<Dependency> dependencies = requirements.getDependencies(); 213 if (dependencies != null && !dependencies.isEmpty()) { 214 for (final Dependency dependency : dependencies) { 215 if (dependency != null) { 216 217 final String dependencyName = dependency.getName(); 218 if (dependencyName == null) { 219 throw new IllegalStateException(); 220 } 221 222 final Collection<?> importValues = dependency.getImportValues(); 223 if (importValues != null && !importValues.isEmpty()) { 224 225 // Not clear why we build this and install it later; it 226 // is never used. See requirements.go's 227 // processImportValues(). 228 final Collection<Object> newImportValues = new ArrayList<>(importValues.size()); 229 230 for (final Object importValue : importValues) { 231 final String s; 232 233 if (importValue instanceof Map) { 234 @SuppressWarnings("unchecked") 235 final Map<String, String> importValueMap = (Map<String, String>)importValue; 236 237 final String importValueChild = importValueMap.get("child"); 238 final String importValueParent = importValueMap.get("parent"); 239 240 // Not clear to me why we build this and then 241 // install it; it's never used in the .go code. 242 final Map<String, String> newMap = new HashMap<>(); 243 newMap.put("child", importValueChild); 244 newMap.put("parent", importValueParent); 245 246 newImportValues.add(newMap); 247 248 final Map<String, Object> vv = 249 MapTree.newMapChain(importValueParent, 250 getMap(canonicalValues, 251 dependencyName + "." + importValueChild)); 252 combinedValues = Values.coalesceMaps(vv, canonicalValues); 253 // OK 254 255 } else if (importValue instanceof String) { 256 final String importValueString = (String)importValue; 257 258 final String importValueChild = "exports." + importValueString; 259 260 // Not clear to me why we build this and then 261 // install it; it's never used in the .go code. 262 final Map<String, String> newMap = new HashMap<>(); 263 newMap.put("child", importValueChild); 264 newMap.put("parent", "."); 265 266 newImportValues.add(newMap); 267 268 combinedValues = Values.coalesceMaps(getMap(canonicalValues, dependencyName + "." + importValueChild), combinedValues); 269 // OK 270 271 } 272 } 273 // The .go code alters the dependency's importValues; 274 // I'm not sure why, but we follow suit. 275 dependency.setImportValues(newImportValues); 276 } 277 } 278 } 279 } 280 } 281 combinedValues = Values.coalesceMaps(canonicalValues, combinedValues); 282 assert combinedValues != null; 283 final String yaml = new Yaml().dump(combinedValues); 284 assert yaml != null; 285 final Config.Builder configBuilder = chartBuilder.getValuesBuilder(); 286 assert configBuilder != null; 287 configBuilder.setRaw(yaml); 288 returnValue = chartBuilder; 289 assert returnValue != null; 290 return returnValue; 291 } 292 293 // ported from Table() in chartutil/values.go 294 private static final Map<String, Object> getMap(Map<String, Object> map, final String dotSeparatedPath) { 295 final Map<String, Object> returnValue; 296 if (map == null || dotSeparatedPath == null || dotSeparatedPath.isEmpty() || map.isEmpty()) { 297 returnValue = null; 298 } else { 299 returnValue = new MapTree(map).getMap(dotSeparatedPath); 300 } 301 return returnValue; 302 } 303 304 // Ported from LoadRequirements() in chartutil/requirements.go 305 /** 306 * Creates a new {@link Requirements} from a top-level {@code 307 * requirements.yaml} {@linkplain Any resource} present in the 308 * supplied {@link ChartOrBuilder} and returns it. 309 * 310 * <p>This method may return {@code null} if the supplied {@link 311 * ChartOrBuilder} is itself {@code null} or doesn't have a {@code 312 * requirements.yaml} {@linkplain Any resource}.</p> 313 * 314 * @param chart the {@link ChartOrBuilder} housing a {@code 315 * requirement.yaml} {@linkplain Any resource}; may be {@code null} 316 * in which case {@code null} will be returned 317 * 318 * @return a new {@link Requirements} or {@code null} 319 */ 320 public static final Requirements fromChartOrBuilder(final ChartOrBuilder chart) { 321 Requirements returnValue = null; 322 if (chart != null) { 323 final Collection<? extends Any> files = chart.getFilesList(); 324 if (files != null && !files.isEmpty()) { 325 final Yaml yaml = new Yaml(); 326 for (final Any file : files) { 327 if (file != null && "requirements.yaml".equals(file.getTypeUrl())) { 328 final ByteString fileContents = file.getValue(); 329 if (fileContents != null) { 330 final String yamlString = fileContents.toStringUtf8(); 331 if (yamlString != null) { 332 returnValue = yaml.loadAs(yamlString, Requirements.class); 333 assert returnValue != null; 334 } 335 } 336 } 337 } 338 } 339 } 340 return returnValue; 341 } 342 343 /** 344 * Applies a <a 345 * href="https://docs.helm.sh/developing_charts/#alias-field-in-requirements-yaml">variety 346 * of rules concerning subchart aliasing and enablement</a> to the 347 * contents of the supplied {@code Chart.Builder}. 348 * 349 * <p>This method never returns {@code null} 350 * 351 * @param chartBuilder the {@link Chart.Builder} whose subcharts may 352 * be affected; must not be {@code null} 353 * 354 * @param userSuppliedValues a {@link ConfigOrBuilder} representing 355 * overriding values; may be {@code null} 356 * 357 * @return the supplied {@code chartBuilder} for convenience; never 358 * {@code null} 359 * 360 * @exception NullPointerException if {@code chartBuilder} is {@code 361 * null} 362 */ 363 public static final Chart.Builder apply(final Chart.Builder chartBuilder, ConfigOrBuilder userSuppliedValues) { 364 return apply(chartBuilder, userSuppliedValues, true); 365 } 366 367 /** 368 * Applies a <a 369 * href="https://docs.helm.sh/developing_charts/#alias-field-in-requirements-yaml">variety 370 * of rules concerning subchart aliasing and enablement</a> to the 371 * contents of the supplied {@code Chart.Builder}. 372 * 373 * <p>This method never returns {@code null} 374 * 375 * @param chartBuilder the {@link Chart.Builder} whose subcharts may 376 * be affected; must not be {@code null} 377 * 378 * @param userSuppliedValues a {@link ConfigOrBuilder} representing 379 * overriding values; may be {@code null} 380 * 381 * @param firstInvocation {@code true} if this is a non-recursive 382 * call, and hence certain "top-level" processing should take place 383 * 384 * @return the supplied {@code chartBuilder} for convenience; never 385 * {@code null} 386 * 387 * @exception NullPointerException if {@code chartBuilder} is {@code 388 * null} 389 */ 390 static final Chart.Builder apply(final Chart.Builder chartBuilder, final ConfigOrBuilder userSuppliedValues, final boolean topLevel) { 391 Objects.requireNonNull(chartBuilder); 392 393 final Requirements requirements = fromChartOrBuilder(chartBuilder); 394 if (requirements != null && !requirements.isEmpty()) { 395 396 final Collection<? extends Dependency> requirementsDependencies = requirements.getDependencies(); 397 if (requirementsDependencies != null && !requirementsDependencies.isEmpty()) { 398 399 final List<? extends Chart.Builder> existingSubcharts = chartBuilder.getDependenciesBuilderList(); 400 if (existingSubcharts != null && !existingSubcharts.isEmpty()) { 401 402 Collection<Dependency> missingSubcharts = null; 403 404 for (final Dependency dependency : requirementsDependencies) { 405 if (dependency != null) { 406 boolean dependencySelectsAtLeastOneSubchart = false; 407 for (final Chart.Builder subchart : existingSubcharts) { 408 if (subchart != null) { 409 dependencySelectsAtLeastOneSubchart = dependencySelectsAtLeastOneSubchart || dependency.selects(subchart); 410 dependency.adjustName(subchart); 411 } 412 } 413 if (topLevel && !dependencySelectsAtLeastOneSubchart) { 414 if (missingSubcharts == null) { 415 missingSubcharts = new ArrayList<>(); 416 } 417 missingSubcharts.add(dependency); 418 } else { 419 dependency.setNameToAlias(); 420 } 421 assert dependency.isEnabled(); 422 } 423 } 424 425 if (missingSubcharts != null && !missingSubcharts.isEmpty()) { 426 throw new MissingDependenciesException(missingSubcharts); 427 } 428 429 // Combine the supplied values with the chart's default 430 // values in the form of a Map. 431 final Map<String, Object> chartValuesMap = Configs.toValuesMap(chartBuilder, userSuppliedValues); 432 assert chartValuesMap != null; 433 434 // Now disable certain Dependencies. This might be because 435 // the canonical value set contains tags or conditions 436 // designating them for disablement. We couldn't disable 437 // them earlier because we didn't have values. 438 requirements.applyEnablementRules(chartValuesMap); 439 440 // Turn the values into YAML, because YAML is the only format 441 // we have for setting the contents of a new Config.Builder object (see 442 // Config.Builder#setRaw(String)). 443 final String userSuppliedValuesYaml; 444 if (chartValuesMap.isEmpty()) { 445 userSuppliedValuesYaml = ""; 446 } else { 447 userSuppliedValuesYaml = new Yaml().dump(chartValuesMap); 448 } 449 assert userSuppliedValuesYaml != null; 450 451 final Config.Builder configBuilder = Config.newBuilder(); 452 assert configBuilder != null; 453 configBuilder.setRaw(userSuppliedValuesYaml); 454 455 // Very carefully remove subcharts that have been disabled. 456 // Note the recursive call contained below. 457 ITERATION: 458 for (int i = 0; i < chartBuilder.getDependenciesCount(); i++) { 459 final Chart.Builder subchart = chartBuilder.getDependenciesBuilder(i); 460 for (final Dependency dependency : requirementsDependencies) { 461 if (dependency != null && !dependency.isEnabled() && dependency.selects(subchart)) { 462 chartBuilder.removeDependencies(i--); 463 continue ITERATION; 464 } 465 } 466 467 // If we get here, this is an enabled subchart. 468 Requirements.apply(subchart, configBuilder, false /* not topLevel, i.e. this is recursive */); // <-- RECURSIVE CALL 469 } 470 471 } 472 } 473 } 474 final Chart.Builder returnValue; 475 if (topLevel) { 476 returnValue = processImportValues(chartBuilder); 477 } else { 478 returnValue = chartBuilder; 479 } 480 return returnValue; 481 } 482 483 484 /* 485 * Inner and nested classes. 486 */ 487 488 489 /** 490 * A {@link SimpleBeanInfo} describing the Java Bean properties for 491 * the {@link Dependency} class; not normally used directly by end 492 * users. 493 * 494 * @author <a href="https://about.me/lairdnelson/" 495 * target="_parent">Laird Nelson</a> 496 * 497 * @see SimpleBeanInfo 498 */ 499 public static final class DependencyBeanInfo extends SimpleBeanInfo { 500 501 502 /* 503 * Instance methods. 504 */ 505 506 507 /** 508 * The {@link Collection} of {@link PropertyDescriptor}s whose 509 * contents will be returned by the {@link 510 * #getPropertyDescriptors()} method. 511 * 512 * <p>This field is never {@code null}.</p> 513 * 514 * @see #getPropertyDescriptors() 515 */ 516 private final Collection<? extends PropertyDescriptor> propertyDescriptors; 517 518 519 /* 520 * Constructors. 521 */ 522 523 524 /** 525 * Creates a new {@link DependencyBeanInfo}. 526 * 527 * @exception IntrospectionException if there was a problem 528 * creating a {@link PropertyDescriptor} 529 * 530 * @see #getPropertyDescriptors() 531 */ 532 public DependencyBeanInfo() throws IntrospectionException { 533 super(); 534 final Collection<PropertyDescriptor> propertyDescriptors = new ArrayList<>(); 535 propertyDescriptors.add(new PropertyDescriptor("name", Dependency.class)); 536 propertyDescriptors.add(new PropertyDescriptor("version", Dependency.class)); 537 propertyDescriptors.add(new PropertyDescriptor("repository", Dependency.class)); 538 propertyDescriptors.add(new PropertyDescriptor("condition", Dependency.class)); 539 propertyDescriptors.add(new PropertyDescriptor("tags", Dependency.class)); 540 propertyDescriptors.add(new PropertyDescriptor("import-values", Dependency.class, "getImportValues", "setImportValues")); 541 propertyDescriptors.add(new PropertyDescriptor("alias", Dependency.class)); 542 this.propertyDescriptors = propertyDescriptors; 543 } 544 545 546 /* 547 * Instance methods. 548 */ 549 550 551 /** 552 * Returns an array of {@link PropertyDescriptor}s describing the 553 * {@link Dependency} class. 554 * 555 * <p>This method never returns {@code null}.</p> 556 * 557 * @return a non-{@code null}, non-empty array of {@link 558 * PropertyDescriptor}s 559 */ 560 @Override 561 public final PropertyDescriptor[] getPropertyDescriptors() { 562 return this.propertyDescriptors.toArray(new PropertyDescriptor[this.propertyDescriptors.size()]); 563 } 564 565 } 566 567 /** 568 * A description of a subchart that should be present in a parent 569 * Helm chart; not normally used directly by end users. 570 * 571 * @author <a href="https://about.me/lairdnelson" 572 * target="_parent">Laird Nelson</a> 573 * 574 * @see Requirements 575 */ 576 public static final class Dependency { 577 578 579 /* 580 * Static fields. 581 */ 582 583 584 /** 585 * An unanchored {@link Pattern} matching a sequence of zero or more 586 * whitespace characters, followed by a comma, followed by zero or 587 * more whitespace characters. 588 * 589 * <p>This field is never {@code null}.</p> 590 * 591 * <p>This field is used during {@link #processConditions(Map)} 592 * method execution.</p> 593 */ 594 private static final Pattern commaSplitPattern = Pattern.compile("\\s*,\\s*"); 595 596 597 /* 598 * Instance fields. 599 */ 600 601 602 /** 603 * The name of the subchart being represented by this {@link 604 * Requirements.Dependency}. 605 * 606 * <p>This field may be {@code null}.</p> 607 * 608 * @see #getName() 609 * 610 * @see #setName(String) 611 */ 612 private String name; 613 614 /** 615 * The version of the subchart being represented by this {@link 616 * Requirements.Dependency}. 617 * 618 * <p>This field may be {@code null}.</p> 619 * 620 * @see #getVersion() 621 * 622 * @see #setVersion(String) 623 */ 624 private String version; 625 626 /** 627 * A {@link String} representation of a URI which, when {@code 628 * index.yaml} is appended to it, results in a URI designating a 629 * Helm chart repository index. 630 * 631 * <p>This field may be {@code null}.</p> 632 * 633 * @see #getRepository() 634 * 635 * @see #setRepository(String) 636 */ 637 private String repository; 638 639 /** 640 * A period-separated path that, when evaluated against a {@link 641 * Map} of {@link Map}s representing user-supplied or default 642 * values, will hopefully result in a value that can, in turn, be 643 * evaluated as a truth-value to aid in the enabling and disabling 644 * of subcharts. 645 * 646 * <p>This field may be {@code null}.</p> 647 * 648 * <p>This field may actually hold several such paths separated by 649 * commas. This is an artifact of the design of Helm's {@code 650 * requirements.yaml} file.</p> 651 * 652 * @see #getCondition() 653 * 654 * @see #setCondition(String) 655 */ 656 private String condition; 657 658 /** 659 * A {@link Collection} of tags that can be used to enable or 660 * disable subcharts. 661 * 662 * <p>This field may be {@code null}. 663 * 664 * @see #getTags() 665 * 666 * @see #setTags(Collection) 667 */ 668 private Collection<String> tags; 669 670 /** 671 * Whether the subchart that this {@link Requirements.Dependency} 672 * identifies is to be considered enabled. 673 * 674 * <p>This field is set to {@code true} by default.</p> 675 * 676 * @see #isEnabled() 677 * 678 * @see #setEnabled(boolean) 679 */ 680 private boolean enabled; 681 682 /** 683 * A {@link Collection} representing the contents of a {@code 684 * requirements.yaml}'s <a 685 * href="https://docs.helm.sh/developing_charts/#using-the-exports-format">{@code 686 * import-values} section</a>. 687 * 688 * <p>This field may be {@code null}.</p> 689 * 690 * @see #getImportValues() 691 * 692 * @see #setImportValues(Collection) 693 */ 694 private Collection<Object> importValues; 695 696 /** 697 * The alias to use for the subchart identified by this {@link 698 * Requirements.Dependency}. 699 * 700 * <p>This field may be {@code null}.</p> 701 * 702 * @see #getAlias() 703 * 704 * @see #setAlias(String) 705 */ 706 private String alias; 707 708 709 /* 710 * Constructors. 711 */ 712 713 714 /** 715 * Creates a new {@link Dependency}. 716 */ 717 public Dependency() { 718 super(); 719 this.setEnabled(true); 720 } 721 722 723 /* 724 * Instance methods. 725 */ 726 727 728 /** 729 * Returns the name of the subchart being represented by this {@link 730 * Requirements.Dependency}. 731 * 732 * <p>This method may return {@code null}.</p> 733 * 734 * @return the name of the subchart being represented by this {@link 735 * Requirements.Dependency}, or {@code null} 736 * 737 * @see #setName(String) 738 */ 739 public final String getName() { 740 return this.name; 741 } 742 743 /** 744 * Sets the name of the subchart being represented by this {@link 745 * Requirements.Dependency}. 746 * 747 * @param name the new name; may be {@code null} 748 * 749 * @see #getName() 750 */ 751 public final void setName(final String name) { 752 this.name = name; 753 } 754 755 /** 756 * Returns the version of the subchart being represented by this {@link 757 * Requirements.Dependency}. 758 * 759 * <p>This method may return {@code null}.</p> 760 * 761 * @return the version of the subchart being represented by this {@link 762 * Requirements.Dependency}, or {@code null} 763 * 764 * @see #setVersion(String) 765 */ 766 public final String getVersion() { 767 return this.version; 768 } 769 770 /** 771 * Sets the version of the subchart being represented by this {@link 772 * Requirements.Dependency}. 773 * 774 * @param version the new version; may be {@code null} 775 * 776 * @see #getVersion() 777 */ 778 public final void setVersion(final String version) { 779 this.version = version; 780 } 781 782 /** 783 * Returns the {@link String} representation of a URI which, when 784 * {@code index.yaml} is appended to it, results in a URI 785 * designating a Helm chart repository index. 786 * 787 * <p>This method may return {@code null}.</p> 788 * 789 * @return the {@link String} representation of a URI which, when 790 * {@code index.yaml} is appended to it, results in a URI 791 * designating a Helm chart repository index, or {@code null} 792 * 793 * @see #setRepository(String) 794 */ 795 public final String getRepository() { 796 return this.repository; 797 } 798 799 /** 800 * Sets the {@link String} representation of a URI which, when 801 * {@code index.yaml} is appended to it, results in a URI 802 * designating a Helm chart repository index. 803 * 804 * @param repository the {@link String} representation of a URI 805 * which, when {@code index.yaml} is appended to it, results in a 806 * URI designating a Helm chart repository index, or {@code null} 807 * 808 * @see #getRepository() 809 */ 810 public final void setRepository(final String repository) { 811 this.repository = repository; 812 } 813 814 /** 815 * Returns a period-separated path that, when evaluated against a 816 * {@link Map} of {@link Map}s representing user-supplied or 817 * default values, will hopefully result in a value that can, in 818 * turn, be evaluated as a truth-value to aid in the enabling and 819 * disabling of subcharts. 820 * 821 * <p>This method may return {@code null}.</p> 822 * 823 * <p>This method may return a value that actually holds several 824 * such paths separated by commas. This is an artifact of the 825 * design of Helm's {@code requirements.yaml} file.</p> 826 * 827 * @return a period-separated path that, when evaluated against a 828 * {@link Map} of {@link Map}s representing user-supplied or 829 * default values, will hopefully result in a value that can, in 830 * turn, be evaluated as a truth-value to aid in the enabling and 831 * disabling of subcharts, or {@code null} 832 * 833 * @see #setCondition(String) 834 */ 835 public final String getCondition() { 836 return this.condition; 837 } 838 839 /** 840 * Sets the period-separated path that, when evaluated against a 841 * {@link Map} of {@link Map}s representing user-supplied or 842 * default values, will hopefully result in a value that can, in 843 * turn, be evaluated as a truth-value to aid in the enabling and 844 * disabling of subcharts. 845 * 846 * @param condition a period-separated path that, when evaluated 847 * against a {@link Map} of {@link Map}s representing 848 * user-supplied or default values, will hopefully result in a 849 * value that can, in turn, be evaluated as a truth-value to aid 850 * in the enabling and disabling of subcharts, or {@code null} 851 * 852 * @see #getCondition() 853 */ 854 public final void setCondition(final String condition) { 855 this.condition = condition; 856 } 857 858 /** 859 * Returns the {@link Collection} of tags that can be used to enable or 860 * disable subcharts. 861 * 862 * <p>This method may return {@code null}.</p> 863 * 864 * @return the {@link Collection} of tags that can be used to 865 * enable or disable subcharts, or {@code null} 866 * 867 * @see #setTags(Collection) 868 */ 869 public final Collection<String> getTags() { 870 return this.tags; 871 } 872 873 /** 874 * Sets the {@link Collection} of tags that can be used to enable 875 * or disable subcharts. 876 * 877 * @param tags the {@link Collection} of tags that can be used to 878 * enable or disable subcharts; may be {@code null} 879 * 880 * @see #getTags() 881 */ 882 public final void setTags(final Collection<String> tags) { 883 this.tags = tags; 884 } 885 886 /** 887 * Returns {@code true} if the subchart this {@link 888 * Requirements.Dependency} identifies is to be considered 889 * enabled. 890 * 891 * @return {@code true} if the subchart this {@link 892 * Requirements.Dependency} identifies is to be considered 893 * enabled; {@code false} otherwise 894 * 895 * @see #setEnabled(boolean) 896 */ 897 public final boolean isEnabled() { 898 return this.enabled; 899 } 900 901 /** 902 * Sets whether the subchart this {@link 903 * Requirements.Dependency} identifies is to be considered 904 * enabled. 905 * 906 * @param enabled whether the subchart this {@link 907 * Requirements.Dependency} identifies is to be considered 908 * enabled 909 * 910 * @see #isEnabled() 911 */ 912 public final void setEnabled(final boolean enabled) { 913 this.enabled = enabled; 914 } 915 916 /** 917 * Returns a {@link Collection} representing the contents of a {@code 918 * requirements.yaml}'s <a 919 * href="https://docs.helm.sh/developing_charts/#using-the-exports-format">{@code 920 * import-values} section</a>. 921 * 922 * <p>This method may return {@code null}.</p> 923 * 924 * @return a {@link Collection} representing the contents of a {@code 925 * requirements.yaml}'s <a 926 * href="https://docs.helm.sh/developing_charts/#using-the-exports-format">{@code 927 * import-values} section</a>, or {@code null} 928 * 929 * @see #setImportValues(Collection) 930 */ 931 public final Collection<Object> getImportValues() { 932 return this.importValues; 933 } 934 935 /** 936 * Sets the {@link Collection} representing the contents of a {@code 937 * requirements.yaml}'s <a 938 * href="https://docs.helm.sh/developing_charts/#using-the-exports-format">{@code 939 * import-values} section</a>. 940 * 941 * @param importValues the {@link Collection} representing the contents of a {@code 942 * requirements.yaml}'s <a 943 * href="https://docs.helm.sh/developing_charts/#using-the-exports-format">{@code 944 * import-values} section</a>; may be {@code null} 945 * 946 * @see #getImportValues() 947 */ 948 public final void setImportValues(final Collection<Object> importValues) { 949 this.importValues = importValues; 950 } 951 952 /** 953 * Returns the alias to use for the subchart identified by this {@link 954 * Requirements.Dependency}. 955 * 956 * <p>This method may return {@code null}.</p> 957 * 958 * @return the alias to use for the subchart identified by this {@link 959 * Requirements.Dependency}, or {@code null} 960 * 961 * @see #setAlias(String) 962 */ 963 public final String getAlias() { 964 return this.alias; 965 } 966 967 /** 968 * Sets the alias to use for the subchart identified by this {@link 969 * Requirements.Dependency}. 970 * 971 * @param alias the alias to use for the subchart identified by this {@link 972 * Requirements.Dependency}; may be {@code null} 973 * 974 * @see #getAlias() 975 */ 976 public final void setAlias(final String alias) { 977 this.alias = alias; 978 } 979 980 /** 981 * Returns {@code true} if this {@link Requirements.Dependency} 982 * identifies the given {@link ChartOrBuilder}. 983 * 984 * @param chart the {@link ChartOrBuilder} to check; may be {@code 985 * null} in which case {@code false} will be returned 986 * 987 * @return {@code true} if this {@link Requirements.Dependency} 988 * identifies the given {@link ChartOrBuilder}; {@code false} 989 * otherwise 990 */ 991 public final boolean selects(final ChartOrBuilder chart) { 992 if (chart == null) { 993 return false; 994 } 995 return this.selects(chart.getMetadata()); 996 } 997 998 private final boolean selects(final MetadataOrBuilder metadata) { 999 final boolean returnValue; 1000 if (metadata == null) { 1001 returnValue = this.selects(null, null); 1002 } else { 1003 returnValue = this.selects(metadata.getName(), metadata.getVersion()); 1004 } 1005 return returnValue; 1006 } 1007 1008 private final boolean selects(final String name, final String versionString) { 1009 final Object myName = this.getName(); 1010 if (myName == null) { 1011 if (name != null) { 1012 return false; 1013 } 1014 } else if (!myName.equals(name)) { 1015 return false; 1016 } 1017 1018 final String myVersion = this.getVersion(); 1019 if (myVersion == null) { 1020 if (versionString != null) { 1021 return false; 1022 } 1023 } else { 1024 final Version version = Version.valueOf(myVersion); 1025 assert version != null; 1026 final Parser<Expression> parser = ExpressionParser.newInstance(); 1027 assert parser != null; 1028 final Expression semVerConstraint = parser.parse(versionString); 1029 assert semVerConstraint != null; 1030 if (!version.satisfies(semVerConstraint)) { 1031 return false; 1032 } 1033 } 1034 return true; 1035 } 1036 1037 final boolean adjustName(final Chart.Builder subchart) { 1038 boolean returnValue = false; 1039 if (subchart != null && this.selects(subchart)) { 1040 final String alias = this.getAlias(); 1041 if (alias != null && !alias.isEmpty() && subchart.hasMetadata()) { 1042 final Metadata.Builder subchartMetadataBuilder = subchart.getMetadataBuilder(); 1043 assert subchartMetadataBuilder != null; 1044 if (!alias.equals(subchartMetadataBuilder.getName())) { 1045 // Rename the chart to have our alias as its new name. 1046 subchartMetadataBuilder.setName(alias); 1047 returnValue = true; 1048 } 1049 } 1050 } 1051 return returnValue; 1052 } 1053 1054 final boolean setNameToAlias() { 1055 boolean returnValue = false; 1056 final String alias = this.getAlias(); 1057 if (alias != null && !alias.isEmpty() && !alias.equals(this.getName())) { 1058 this.setName(alias); 1059 returnValue = true; 1060 } 1061 return returnValue; 1062 } 1063 1064 final void processTags(final Map<String, Object> values) { 1065 if (values != null) { 1066 final Object tagsObject = values.get("tags"); 1067 if (tagsObject instanceof Map) { 1068 final Map<?, ?> tags = (Map<?, ?>)tagsObject; 1069 final Collection<? extends String> myTags = this.getTags(); 1070 if (myTags != null && !myTags.isEmpty()) { 1071 boolean explicitlyTrue = false; 1072 boolean explicitlyFalse = false; 1073 for (final String myTag : myTags) { 1074 final Object tagValue = tags.get(myTag); 1075 if (Boolean.TRUE.equals(tagValue)) { 1076 explicitlyTrue = true; 1077 } else if (Boolean.FALSE.equals(tagValue)) { 1078 explicitlyFalse = true; 1079 } else { 1080 // Not a Boolean at all; just skip it 1081 } 1082 } 1083 1084 // Note that this block looks different from the analogous 1085 // block in processConditions() below. It is this way in the 1086 // Go code as well. 1087 if (explicitlyFalse) { 1088 if (!explicitlyTrue) { 1089 this.setEnabled(false); 1090 } 1091 } else { 1092 this.setEnabled(explicitlyTrue); 1093 } 1094 } 1095 } 1096 } 1097 } 1098 1099 final void processConditions(final Map<String, Object> values) { 1100 if (values != null && !values.isEmpty()) { 1101 final MapTree mapTree = new MapTree(values); 1102 boolean explicitlyTrue = false; 1103 boolean explicitlyFalse = false; 1104 String conditionString = this.getCondition(); 1105 if (conditionString != null) { 1106 conditionString = conditionString.trim(); 1107 final String[] conditions = commaSplitPattern.split(conditionString); 1108 if (conditions != null && conditions.length > 0) { 1109 for (final String condition : conditions) { 1110 if (condition != null && !condition.isEmpty()) { 1111 final Object conditionValue = mapTree.get(condition, Object.class); 1112 if (Boolean.TRUE.equals(conditionValue)) { 1113 explicitlyTrue = true; 1114 } else if (Boolean.FALSE.equals(conditionValue)) { 1115 explicitlyFalse = true; 1116 } else if (conditionValue != null) { 1117 break; 1118 } 1119 } 1120 } 1121 } 1122 } 1123 1124 // Note that this block looks different from the analogous block 1125 // in processTags() above. It is this way in the Go code as 1126 // well. 1127 if (explicitlyFalse) { 1128 if (!explicitlyTrue) { 1129 this.setEnabled(false); 1130 } 1131 } else if (explicitlyTrue) { 1132 this.setEnabled(true); 1133 } 1134 } 1135 } 1136 1137 /** 1138 * Returns a {@link String} representation of this {@link 1139 * Requirements.Dependency}. 1140 * 1141 * <p>This method never returns {@code null}.</p> 1142 * 1143 * @return a non-{@code null} {@link String} representation of 1144 * this {@link Requirements.Dependency} 1145 */ 1146 @Override 1147 public final String toString() { 1148 final StringBuilder sb = new StringBuilder(); 1149 final Object name = this.getName(); 1150 if (name == null) { 1151 sb.append("Unnamed"); 1152 } else { 1153 sb.append(name); 1154 } 1155 final String alias = this.getAlias(); 1156 if (alias != null && !alias.isEmpty()) { 1157 sb.append(" (").append(alias).append(")"); 1158 } 1159 sb.append(" "); 1160 sb.append(this.getVersion()); 1161 return sb.toString(); 1162 } 1163 1164 } 1165 1166}