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.repository; 018 019import java.io.BufferedInputStream; 020import java.io.IOException; 021import java.io.InputStream; 022 023import java.net.URI; 024import java.net.URISyntaxException; 025 026import java.nio.file.Files; 027import java.nio.file.Path; 028import java.nio.file.Paths; 029 030import java.time.Instant; 031 032import java.util.Collection; 033import java.util.Collections; 034import java.util.Date; 035import java.util.LinkedHashSet; 036import java.util.Map; 037import java.util.Objects; 038import java.util.Set; 039 040import java.util.regex.Pattern; 041 042import hapi.chart.ChartOuterClass.Chart; 043 044import org.microbean.development.annotation.Experimental; 045 046import org.yaml.snakeyaml.Yaml; 047 048import org.microbean.helm.chart.resolver.AbstractChartResolver; 049import org.microbean.helm.chart.resolver.ChartResolverException; 050 051/** 052 * A repository of {@link ChartRepository} instances, normally built 053 * from a Helm {@code repositories.yaml} file. 054 * 055 * @author <a href="https://about.me/lairdnelson" 056 * target="_parent">Laird Nelson</a> 057 */ 058@Experimental 059public class ChartRepositoryRepository extends AbstractChartResolver { 060 061 062 /* 063 * Static fields. 064 */ 065 066 067 /** 068 * A {@link Pattern} that matches a single solidus ("{@code /}"). 069 * 070 * <p>This field is never {@code null}.</p> 071 */ 072 private static final Pattern slashPattern = Pattern.compile("/"); 073 074 075 /* 076 * Instance fields. 077 */ 078 079 080 /** 081 * The ({@linkplain Collections#unmodifiableSet(Set) immutable}) 082 * {@link Set} of {@link ChartRepository} instances managed by this 083 * {@link ChartRepositoryRepository}. 084 * 085 * <p>This field is never {@code null}.</p> 086 * 087 * @see #ChartRepositoryRepository(Set) 088 * 089 * @see #getChartRepositories() 090 */ 091 private final Set<ChartRepository> chartRepositories; 092 093 094 /* 095 * Constructors. 096 */ 097 098 099 /** 100 * Creates a new {@link ChartRepositoryRepository}. 101 * 102 * @param chartRepositories the {@link Set} of {@link 103 * ChartRepository} instances to be managed by this {@link 104 * ChartRepositoryRepository}; may be {@code null}; copied by value 105 * 106 * @see #getChartRepositories() 107 */ 108 public ChartRepositoryRepository(final Set<? extends ChartRepository> chartRepositories) { 109 super(); 110 if (chartRepositories == null || chartRepositories.isEmpty()) { 111 this.chartRepositories = Collections.emptySet(); 112 } else { 113 this.chartRepositories = Collections.unmodifiableSet(new LinkedHashSet<>(chartRepositories)); 114 } 115 } 116 117 118 /* 119 * Instance methods. 120 */ 121 122 123 /** 124 * Returns the non-{@code null} {@linkplain 125 * Collections#unmodifiableSet(Set) immutable} {@link Set} of {@link 126 * ChartRepository} instances managed by this {@link 127 * ChartRepositoryRepository}. 128 * 129 * @return the non-{@code null} {@linkplain 130 * Collections#unmodifiableSet(Set) immutable} {@link Set} of {@link 131 * ChartRepository} instances managed by this {@link 132 * ChartRepositoryRepository} 133 */ 134 public final Set<ChartRepository> getChartRepositories() { 135 return this.chartRepositories; 136 } 137 138 /** 139 * Returns the {@link ChartRepository} managed by this {@link 140 * ChartRepositoryRepository} with the supplied {@code name}, or 141 * {@code null} if there is no such {@link ChartRepository}. 142 * 143 * @param name the {@linkplain ChartRepository#getName() name} of 144 * the {@link ChartRepository} to return; must not be {@code null} 145 * 146 * @return the {@link ChartRepository} managed by this {@link 147 * ChartRepositoryRepository} with the supplied {@code name}, or 148 * {@code null} 149 * 150 * @exception NullPointerException if {@code name} is {@code null} 151 */ 152 public ChartRepository getChartRepository(final String name) { 153 Objects.requireNonNull(name); 154 ChartRepository returnValue = null; 155 final Collection<? extends ChartRepository> repos = this.getChartRepositories(); 156 if (repos != null && !repos.isEmpty()) { 157 for (final ChartRepository repo : repos) { 158 if (repo != null && name.equals(repo.getName())) { 159 returnValue = repo; 160 } 161 } 162 } 163 return returnValue; 164 } 165 166 /** 167 * {@inheritDoc} 168 * 169 * <p>This implementation splits the supplied slash-delimited {@code 170 * chartName} into a <em>chart repository name</em> and a <em>chart 171 * name</em>, uses the chart repository name to {@linkplain 172 * #getChartRepository(String) locate a suitable 173 * <code>ChartRepository</code>}, and then calls {@link 174 * ChartRepository#resolve(String, String)} with the chart name and 175 * the supplied {@code chartVersion}, and returns the result.</p> 176 * 177 * @param chartName a slash-separated {@link String} whose first 178 * component is a {@linkplain ChartRepository#getName() chart 179 * repository name} and whose second component is a Helm chart name; 180 * must not be {@code null} 181 * 182 * @param chartVersion the version of the chart to resolve; may be 183 * {@code null} in which case "latest" semantics are implied 184 * 185 * @return a {@link Chart.Builder}, or {@code null} 186 * 187 * @see #resolve(String, String, String) 188 */ 189 @Override 190 public Chart.Builder resolve(final String chartName, final String chartVersion) throws ChartResolverException { 191 Objects.requireNonNull(chartName); 192 Chart.Builder returnValue = null; 193 final String[] parts = slashPattern.split(chartName, 2); 194 if (parts != null && parts.length == 2) { 195 returnValue = this.resolve(parts[0], parts[1], chartVersion); 196 } 197 return returnValue; 198 } 199 200 /** 201 * Uses the supplied {@code repositoryName}, {@code chartName} and 202 * {@code chartVersion} parameters to find an appropriate Helm chart 203 * and returns it in the form of a {@link Chart.Builder} object. 204 * 205 * <p>This implementation uses the supplied {@code repositoryName} 206 * to {@linkplain #getChartRepository(String) locate a suitable 207 * <code>ChartRepository</code>}, and then calls {@link 208 * ChartRepository#resolve(String, String)} with the chart name and 209 * the supplied {@code chartVersion}, and returns the result.</p> 210 * 211 * @param repositoryName a {@linkplain ChartRepository#getName() 212 * chart repository name}; must not be {@code null} 213 * 214 * @param chartName a Helm chart name; must not be {@code null} 215 * 216 * @param chartVersion the version of the Helm chart to select; may 217 * be {@code null} in which case "latest" semantics are implied 218 * 219 * @return a {@link Chart.Builder}, or {@code null} 220 * 221 * @exception ChartResolverException if there was a problem with 222 * resolution 223 * 224 * @exception NullPointerException if {@code repositoryName} or 225 * {@code chartName} is {@code null} 226 * 227 * @see #getChartRepository(String) 228 * 229 * @see ChartRepository#getName() 230 * 231 * @see ChartRepository#resolve(String, String) 232 */ 233 public Chart.Builder resolve(final String repositoryName, final String chartName, final String chartVersion) throws ChartResolverException { 234 Objects.requireNonNull(repositoryName); 235 Objects.requireNonNull(chartName); 236 Chart.Builder returnValue = null; 237 final ChartRepository repo = this.getChartRepository(repositoryName); 238 if (repo != null) { 239 final Chart.Builder candidate = repo.resolve(chartName, chartVersion); 240 if (candidate != null) { 241 returnValue = candidate; 242 } 243 } 244 return returnValue; 245 } 246 247 248 /* 249 * Static methods. 250 */ 251 252 253 /** 254 * Creates and returns a new {@link ChartRepositoryRepository} from 255 * the contents of a {@code repositories.yaml} file typically 256 * located in the {@code ~/.helm/repository} directory. 257 * 258 * <p>This method never returns {@code null}.</p> 259 * 260 * @return a new {@link ChartRepositoryRepository}; never {@code 261 * null} 262 * 263 * @exception IOException if there was a problem reading the file 264 * 265 * @exception URISyntaxException if there was an invalid URI in the 266 * file 267 * 268 * @see #fromYaml(InputStream, Path, Path) 269 */ 270 public static final ChartRepositoryRepository fromHelmRepositoriesYaml() throws IOException, URISyntaxException { 271 try (final InputStream stream = new BufferedInputStream(Files.newInputStream(ChartRepository.getHelmHome().resolve("repository/repositories.yaml")))) { 272 return fromYaml(stream); 273 } 274 } 275 276 /** 277 * Creates and returns a new {@link ChartRepositoryRepository} from 278 * the contents of a {@code repositories.yaml} file represented by 279 * the supplied {@link InputStream}. 280 * 281 * @param stream the {@link InputStream} to read from; must not be 282 * {@code null} 283 * 284 * @return a new {@link ChartRepositoryRepository}; never {@code 285 * null} 286 * 287 * @exception IOException if there was a problem reading the file 288 * 289 * @exception URISyntaxException if there was an invalid URI in the 290 * file 291 * 292 * @see #fromYaml(InputStream, Path, Path) 293 */ 294 public static final ChartRepositoryRepository fromYaml(final InputStream stream) throws IOException, URISyntaxException { 295 return fromYaml(stream, null, null); 296 } 297 298 /** 299 * Creates and returns a new {@link ChartRepositoryRepository} from 300 * the contents of a {@code repositories.yaml} file represented by 301 * the supplied {@link InputStream}. 302 * 303 * @param stream the {@link InputStream} to read from; must not be 304 * {@code null} 305 * 306 * @param archiveCacheDirectory an {@linkplain Path#isAbsolute() 307 * absolute} {@link Path} representing a directory where Helm chart 308 * archives may be stored; if {@code null} then a {@link Path} 309 * beginning with the absolute directory represented by the value of 310 * the {@code helm.home} system property, or the value of the {@code 311 * HELM_HOME} environment variable, appended with {@code 312 * cache/archive} will be used instead 313 * 314 * @param indexCacheDirectory an {@linkplain Path#isAbsolute() 315 * absolute} {@link Path} representing a directory that the supplied 316 * {@code cachedIndexPath} parameter value will be considered to be 317 * relative to; will be ignored and hence may be {@code null} if the 318 * supplied {@code cachedIndexPath} parameter value {@linkplain 319 * Path#isAbsolute()} 320 * 321 * @return a new {@link ChartRepositoryRepository}; never {@code 322 * null} 323 * 324 * @exception IOException if there was a problem reading the file 325 * 326 * @exception URISyntaxException if there was an invalid URI in the 327 * file 328 * 329 * @see #fromYaml(InputStream, Path, Path, ChartRepositoryFactory) 330 */ 331 public static final ChartRepositoryRepository fromYaml(final InputStream stream, Path archiveCacheDirectory, Path indexCacheDirectory) throws IOException, URISyntaxException { 332 return fromYaml(stream, archiveCacheDirectory, indexCacheDirectory, null); 333 } 334 335 /** 336 * Creates and returns a new {@link ChartRepositoryRepository} from 337 * the contents of a {@code repositories.yaml} file represented by 338 * the supplied {@link InputStream}. 339 * 340 * @param stream the {@link InputStream} to read from; must not be 341 * {@code null} 342 * 343 * @param archiveCacheDirectory an {@linkplain Path#isAbsolute() 344 * absolute} {@link Path} representing a directory where Helm chart 345 * archives may be stored; if {@code null} then a {@link Path} 346 * beginning with the absolute directory represented by the value of 347 * the {@code helm.home} system property, or the value of the {@code 348 * HELM_HOME} environment variable, appended with {@code 349 * cache/archive} will be used instead 350 * 351 * @param indexCacheDirectory an {@linkplain Path#isAbsolute() 352 * absolute} {@link Path} representing a directory that the supplied 353 * {@code cachedIndexPath} parameter value will be considered to be 354 * relative to; will be ignored and hence may be {@code null} if the 355 * supplied {@code cachedIndexPath} parameter value {@linkplain 356 * Path#isAbsolute()} 357 * 358 * @param factory a {@link ChartRepositoryFactory} that can create 359 * {@link ChartRepository} instances; may be {@code null} in which 360 * case the {@link ChartRepository#ChartRepository(String, URI, 361 * Path, Path, Path)} constructor will be used instead 362 * 363 * @return a new {@link ChartRepositoryRepository}; never {@code 364 * null} 365 * 366 * @exception IOException if there was a problem reading the file 367 * 368 * @exception URISyntaxException if there was an invalid URI in the 369 * file 370 */ 371 public static final ChartRepositoryRepository fromYaml(final InputStream stream, 372 Path archiveCacheDirectory, 373 Path indexCacheDirectory, 374 ChartRepositoryFactory factory) 375 throws IOException, URISyntaxException { 376 Objects.requireNonNull(stream); 377 if (factory == null) { 378 factory = ChartRepository::new; 379 } 380 Path helmHome = null; 381 if (archiveCacheDirectory == null) { 382 helmHome = ChartRepository.getHelmHome(); 383 assert helmHome != null; 384 archiveCacheDirectory = helmHome.resolve("cache/archive"); 385 assert archiveCacheDirectory != null; 386 } 387 if (!Files.isDirectory(archiveCacheDirectory)) { 388 throw new IllegalArgumentException("!Files.isDirectory(archiveCacheDirectory): " + archiveCacheDirectory); 389 } 390 if (indexCacheDirectory == null) { 391 if (helmHome == null) { 392 helmHome = ChartRepository.getHelmHome(); 393 assert helmHome != null; 394 } 395 indexCacheDirectory = helmHome.resolve("repository/cache"); 396 assert indexCacheDirectory != null; 397 } 398 if (!Files.isDirectory(indexCacheDirectory)) { 399 throw new IllegalArgumentException("!Files.isDirectory(indexCacheDirectory): " + indexCacheDirectory); 400 } 401 final Map<?, ?> map = new Yaml().loadAs(stream, Map.class); 402 if (map == null || map.isEmpty()) { 403 throw new IllegalArgumentException("No data readable from stream: " + stream); 404 } 405 final Set<ChartRepository> chartRepositories; 406 @SuppressWarnings("unchecked") 407 final Collection<? extends Map<?, ?>> repositories = (Collection<? extends Map<?, ?>>)map.get("repositories"); 408 if (repositories == null || repositories.isEmpty()) { 409 chartRepositories = Collections.emptySet(); 410 } else { 411 chartRepositories = new LinkedHashSet<>(); 412 for (final Map<?, ?> repositoryMap : repositories) { 413 if (repositoryMap != null && !repositoryMap.isEmpty()) { 414 final String name = Objects.requireNonNull((String)repositoryMap.get("name")); 415 final URI uri = new URI((String)repositoryMap.get("url")); 416 Path cachedIndexPath = Objects.requireNonNull(Paths.get((String)repositoryMap.get("cache"))); 417 if (!cachedIndexPath.isAbsolute()) { 418 cachedIndexPath = indexCacheDirectory.resolve(cachedIndexPath); 419 assert cachedIndexPath.isAbsolute(); 420 } 421 422 // final ChartRepository chartRepository = new ChartRepository(name, uri, archiveCacheDirectory, indexCacheDirectory, cachedIndexPath); 423 final ChartRepository chartRepository = factory.createChartRepository(name, uri, archiveCacheDirectory, indexCacheDirectory, cachedIndexPath); 424 if (chartRepository == null) { 425 throw new IllegalStateException("factory.createChartRepository() == null"); 426 } 427 chartRepositories.add(chartRepository); 428 } 429 } 430 } 431 return new ChartRepositoryRepository(chartRepositories); 432 } 433 434 435 /* 436 * Inner and nested classes. 437 */ 438 439 440 /** 441 * A factory for {@link ChartRepository} instances. 442 * 443 * @author <a href="https://about.me/lairdnelson" 444 * target="_parent">Laird Nelson</a> 445 * 446 * @see ChartRepository 447 */ 448 @FunctionalInterface 449 public static interface ChartRepositoryFactory { 450 451 /** 452 * Creates a new {@link ChartRepository} and returns it. 453 * 454 * @param name the name of the chart repository; must not be 455 * {@code null} 456 * 457 * @param uri the {@link URI} to the root of the chart repository; 458 * must not be {@code null} 459 * 460 * @param archiveCacheDirectory an {@linkplain Path#isAbsolute() 461 * absolute} {@link Path} representing a directory where Helm chart 462 * archives may be stored; if {@code null} then often a {@link Path} 463 * beginning with the absolute directory represented by the value of 464 * the {@code helm.home} system property, or the value of the {@code 465 * HELM_HOME} environment variable, appended with {@code 466 * cache/archive} will be used instead 467 * 468 * @param indexCacheDirectory an {@linkplain Path#isAbsolute() 469 * absolute} {@link Path} representing a directory that the supplied 470 * {@code cachedIndexPath} parameter value will be considered to be 471 * relative to; <strong>will be ignored and hence may be {@code 472 * null}</strong> if the supplied {@code cachedIndexPath} parameter 473 * value {@linkplain Path#isAbsolute() is absolute} 474 * 475 * @param cachedIndexPath a {@link Path} naming the file that will 476 * store a copy of the chart repository's {@code index.yaml} file; 477 * if {@code null} then a {@link Path} relative to the absolute 478 * directory represented by the value of the {@code helm.home} 479 * system property, or the value of the {@code HELM_HOME} 480 * environment variable, and bearing a name consisting of the 481 * supplied {@code name} suffixed with {@code -index.yaml} will 482 * often be used instead 483 * 484 * @exception NullPointerException if either {@code name} or {@code 485 * uri} is {@code null} 486 * 487 * @exception IllegalArgumentException if {@code uri} is {@linkplain 488 * URI#isAbsolute() not absolute}, or if there is no existing "Helm 489 * home" directory, or if {@code archiveCacheDirectory} is 490 * non-{@code null} and either empty or not {@linkplain 491 * Path#isAbsolute()} 492 * 493 * @return a new, non-{@code null} {@link ChartRepository} 494 * 495 * @see ChartRepository#ChartRepository(String, URI, Path, Path, 496 * Path) 497 */ 498 public ChartRepository createChartRepository(final String name, final URI uri, final Path archiveCacheDirectory, final Path indexCacheDirectory, final Path cachedIndexPath); 499 500 } 501 502}