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 /* top level, i.e. non-recursive call */); 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 topLevel {@code true} if this is a non-recursive call, and 382 * 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 range of acceptable semantic versions of the subchart being 616 * represented by this {@link 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 versionRange; 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 range of acceptable semantic versions of the 757 * subchart being represented by this {@link 758 * Requirements.Dependency}. 759 * 760 * <p>This method may return {@code null}.</p> 761 * 762 * @return the range of acceptable semantic versions of the 763 * subchart being represented by this {@link 764 * Requirements.Dependency}, or {@code null} 765 * 766 * @see #setVersion(String) 767 */ 768 public final String getVersion() { 769 return this.versionRange; 770 } 771 772 /** 773 * Sets the range of acceptable semantic versions of the subchart 774 * being represented by this {@link Requirements.Dependency}. 775 * 776 * @param versionRange the new semantic version range; may be {@code 777 * null} 778 * 779 * @see #getVersion() 780 */ 781 public final void setVersion(final String versionRange) { 782 this.versionRange = versionRange; 783 } 784 785 /** 786 * Returns the {@link String} representation of a URI which, when 787 * {@code index.yaml} is appended to it, results in a URI 788 * designating a Helm chart repository index. 789 * 790 * <p>This method may return {@code null}.</p> 791 * 792 * @return the {@link String} representation of a URI which, when 793 * {@code index.yaml} is appended to it, results in a URI 794 * designating a Helm chart repository index, or {@code null} 795 * 796 * @see #setRepository(String) 797 */ 798 public final String getRepository() { 799 return this.repository; 800 } 801 802 /** 803 * Sets the {@link String} representation of a URI which, when 804 * {@code index.yaml} is appended to it, results in a URI 805 * designating a Helm chart repository index. 806 * 807 * @param repository the {@link String} representation of a URI 808 * which, when {@code index.yaml} is appended to it, results in a 809 * URI designating a Helm chart repository index, or {@code null} 810 * 811 * @see #getRepository() 812 */ 813 public final void setRepository(final String repository) { 814 this.repository = repository; 815 } 816 817 /** 818 * Returns a period-separated path that, when evaluated against a 819 * {@link Map} of {@link Map}s representing user-supplied or 820 * default values, will hopefully result in a value that can, in 821 * turn, be evaluated as a truth-value to aid in the enabling and 822 * disabling of subcharts. 823 * 824 * <p>This method may return {@code null}.</p> 825 * 826 * <p>This method may return a value that actually holds several 827 * such paths separated by commas. This is an artifact of the 828 * design of Helm's {@code requirements.yaml} file.</p> 829 * 830 * @return a period-separated path that, when evaluated against a 831 * {@link Map} of {@link Map}s representing user-supplied or 832 * default values, will hopefully result in a value that can, in 833 * turn, be evaluated as a truth-value to aid in the enabling and 834 * disabling of subcharts, or {@code null} 835 * 836 * @see #setCondition(String) 837 */ 838 public final String getCondition() { 839 return this.condition; 840 } 841 842 /** 843 * Sets the period-separated path that, when evaluated against a 844 * {@link Map} of {@link Map}s representing user-supplied or 845 * default values, will hopefully result in a value that can, in 846 * turn, be evaluated as a truth-value to aid in the enabling and 847 * disabling of subcharts. 848 * 849 * @param condition a period-separated path that, when evaluated 850 * against a {@link Map} of {@link Map}s representing 851 * user-supplied or default values, will hopefully result in a 852 * value that can, in turn, be evaluated as a truth-value to aid 853 * in the enabling and disabling of subcharts, or {@code null} 854 * 855 * @see #getCondition() 856 */ 857 public final void setCondition(final String condition) { 858 this.condition = condition; 859 } 860 861 /** 862 * Returns the {@link Collection} of tags that can be used to enable or 863 * disable subcharts. 864 * 865 * <p>This method may return {@code null}.</p> 866 * 867 * @return the {@link Collection} of tags that can be used to 868 * enable or disable subcharts, or {@code null} 869 * 870 * @see #setTags(Collection) 871 */ 872 public final Collection<String> getTags() { 873 return this.tags; 874 } 875 876 /** 877 * Sets the {@link Collection} of tags that can be used to enable 878 * or disable subcharts. 879 * 880 * @param tags the {@link Collection} of tags that can be used to 881 * enable or disable subcharts; may be {@code null} 882 * 883 * @see #getTags() 884 */ 885 public final void setTags(final Collection<String> tags) { 886 this.tags = tags; 887 } 888 889 /** 890 * Returns {@code true} if the subchart this {@link 891 * Requirements.Dependency} identifies is to be considered 892 * enabled. 893 * 894 * @return {@code true} if the subchart this {@link 895 * Requirements.Dependency} identifies is to be considered 896 * enabled; {@code false} otherwise 897 * 898 * @see #setEnabled(boolean) 899 */ 900 public final boolean isEnabled() { 901 return this.enabled; 902 } 903 904 /** 905 * Sets whether the subchart this {@link 906 * Requirements.Dependency} identifies is to be considered 907 * enabled. 908 * 909 * @param enabled whether the subchart this {@link 910 * Requirements.Dependency} identifies is to be considered 911 * enabled 912 * 913 * @see #isEnabled() 914 */ 915 public final void setEnabled(final boolean enabled) { 916 this.enabled = enabled; 917 } 918 919 /** 920 * Returns a {@link Collection} representing the contents of a {@code 921 * requirements.yaml}'s <a 922 * href="https://docs.helm.sh/developing_charts/#using-the-exports-format">{@code 923 * import-values} section</a>. 924 * 925 * <p>This method may return {@code null}.</p> 926 * 927 * @return a {@link Collection} representing the contents of a {@code 928 * requirements.yaml}'s <a 929 * href="https://docs.helm.sh/developing_charts/#using-the-exports-format">{@code 930 * import-values} section</a>, or {@code null} 931 * 932 * @see #setImportValues(Collection) 933 */ 934 public final Collection<Object> getImportValues() { 935 return this.importValues; 936 } 937 938 /** 939 * Sets the {@link Collection} representing the contents of a {@code 940 * requirements.yaml}'s <a 941 * href="https://docs.helm.sh/developing_charts/#using-the-exports-format">{@code 942 * import-values} section</a>. 943 * 944 * @param importValues the {@link Collection} representing the contents of a {@code 945 * requirements.yaml}'s <a 946 * href="https://docs.helm.sh/developing_charts/#using-the-exports-format">{@code 947 * import-values} section</a>; may be {@code null} 948 * 949 * @see #getImportValues() 950 */ 951 public final void setImportValues(final Collection<Object> importValues) { 952 this.importValues = importValues; 953 } 954 955 /** 956 * Returns the alias to use for the subchart identified by this {@link 957 * Requirements.Dependency}. 958 * 959 * <p>This method may return {@code null}.</p> 960 * 961 * @return the alias to use for the subchart identified by this {@link 962 * Requirements.Dependency}, or {@code null} 963 * 964 * @see #setAlias(String) 965 */ 966 public final String getAlias() { 967 return this.alias; 968 } 969 970 /** 971 * Sets the alias to use for the subchart identified by this {@link 972 * Requirements.Dependency}. 973 * 974 * @param alias the alias to use for the subchart identified by this {@link 975 * Requirements.Dependency}; may be {@code null} 976 * 977 * @see #getAlias() 978 */ 979 public final void setAlias(final String alias) { 980 this.alias = alias; 981 } 982 983 /** 984 * Returns {@code true} if this {@link Requirements.Dependency} 985 * identifies the given {@link ChartOrBuilder}. 986 * 987 * @param chart the {@link ChartOrBuilder} to check; may be {@code 988 * null} in which case {@code false} will be returned 989 * 990 * @return {@code true} if this {@link Requirements.Dependency} 991 * identifies the given {@link ChartOrBuilder}; {@code false} 992 * otherwise 993 */ 994 public final boolean selects(final ChartOrBuilder chart) { 995 if (chart == null) { 996 return false; 997 } 998 return this.selects(chart.getMetadata()); 999 } 1000 1001 private final boolean selects(final MetadataOrBuilder metadata) { 1002 final boolean returnValue; 1003 if (metadata == null) { 1004 returnValue = false; 1005 } else { 1006 returnValue = this.selects(metadata.getName(), metadata.getVersion()); 1007 } 1008 return returnValue; 1009 } 1010 1011 private final boolean selects(final String name, final String versionString) { 1012 if (versionString == null) { 1013 return false; 1014 } 1015 1016 final Object myName = this.getName(); 1017 if (myName == null) { 1018 if (name != null) { 1019 return false; 1020 } 1021 } else if (!myName.equals(name)) { 1022 return false; 1023 } 1024 1025 final String myVersionRange = this.getVersion(); 1026 if (myVersionRange == null) { 1027 return false; 1028 } 1029 1030 final Version version = Version.valueOf(versionString); 1031 assert version != null; 1032 return version.satisfies(ExpressionParser.newInstance().parse(myVersionRange)); 1033 } 1034 1035 final boolean adjustName(final Chart.Builder subchart) { 1036 boolean returnValue = false; 1037 if (subchart != null && this.selects(subchart)) { 1038 final String alias = this.getAlias(); 1039 if (alias != null && !alias.isEmpty() && subchart.hasMetadata()) { 1040 final Metadata.Builder subchartMetadataBuilder = subchart.getMetadataBuilder(); 1041 assert subchartMetadataBuilder != null; 1042 if (!alias.equals(subchartMetadataBuilder.getName())) { 1043 // Rename the chart to have our alias as its new name. 1044 subchartMetadataBuilder.setName(alias); 1045 returnValue = true; 1046 } 1047 } 1048 } 1049 return returnValue; 1050 } 1051 1052 final boolean setNameToAlias() { 1053 boolean returnValue = false; 1054 final String alias = this.getAlias(); 1055 if (alias != null && !alias.isEmpty() && !alias.equals(this.getName())) { 1056 this.setName(alias); 1057 returnValue = true; 1058 } 1059 return returnValue; 1060 } 1061 1062 final void processTags(final Map<String, Object> values) { 1063 if (values != null) { 1064 final Object tagsObject = values.get("tags"); 1065 if (tagsObject instanceof Map) { 1066 final Map<?, ?> tags = (Map<?, ?>)tagsObject; 1067 final Collection<? extends String> myTags = this.getTags(); 1068 if (myTags != null && !myTags.isEmpty()) { 1069 boolean explicitlyTrue = false; 1070 boolean explicitlyFalse = false; 1071 for (final String myTag : myTags) { 1072 final Object tagValue = tags.get(myTag); 1073 if (Boolean.TRUE.equals(tagValue)) { 1074 explicitlyTrue = true; 1075 } else if (Boolean.FALSE.equals(tagValue)) { 1076 explicitlyFalse = true; 1077 } else { 1078 // Not a Boolean at all; just skip it 1079 } 1080 } 1081 1082 // Note that this block looks different from the analogous 1083 // block in processConditions() below. It is this way in the 1084 // Go code as well. 1085 if (explicitlyFalse) { 1086 if (!explicitlyTrue) { 1087 this.setEnabled(false); 1088 } 1089 } else { 1090 this.setEnabled(explicitlyTrue); 1091 } 1092 } 1093 } 1094 } 1095 } 1096 1097 final void processConditions(final Map<String, Object> values) { 1098 if (values != null && !values.isEmpty()) { 1099 final MapTree mapTree = new MapTree(values); 1100 boolean explicitlyTrue = false; 1101 boolean explicitlyFalse = false; 1102 String conditionString = this.getCondition(); 1103 if (conditionString != null) { 1104 conditionString = conditionString.trim(); 1105 final String[] conditions = commaSplitPattern.split(conditionString); 1106 if (conditions != null && conditions.length > 0) { 1107 for (final String condition : conditions) { 1108 if (condition != null && !condition.isEmpty()) { 1109 final Object conditionValue = mapTree.get(condition, Object.class); 1110 if (Boolean.TRUE.equals(conditionValue)) { 1111 explicitlyTrue = true; 1112 } else if (Boolean.FALSE.equals(conditionValue)) { 1113 explicitlyFalse = true; 1114 } else if (conditionValue != null) { 1115 break; 1116 } 1117 } 1118 } 1119 } 1120 } 1121 1122 // Note that this block looks different from the analogous block 1123 // in processTags() above. It is this way in the Go code as 1124 // well. 1125 if (explicitlyFalse) { 1126 if (!explicitlyTrue) { 1127 this.setEnabled(false); 1128 } 1129 } else if (explicitlyTrue) { 1130 this.setEnabled(true); 1131 } 1132 } 1133 } 1134 1135 /** 1136 * Returns a {@link String} representation of this {@link 1137 * Requirements.Dependency}. 1138 * 1139 * <p>This method never returns {@code null}.</p> 1140 * 1141 * @return a non-{@code null} {@link String} representation of 1142 * this {@link Requirements.Dependency} 1143 */ 1144 @Override 1145 public final String toString() { 1146 final StringBuilder sb = new StringBuilder(); 1147 final Object name = this.getName(); 1148 if (name == null) { 1149 sb.append("Unnamed"); 1150 } else { 1151 sb.append(name); 1152 } 1153 final String alias = this.getAlias(); 1154 if (alias != null && !alias.isEmpty()) { 1155 sb.append(" (").append(alias).append(")"); 1156 } 1157 sb.append(" "); 1158 sb.append(this.getVersion()); 1159 return sb.toString(); 1160 } 1161 1162 } 1163 1164}