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 public static final ChartRepositoryRepository fromYaml(final InputStream stream, Path archiveCacheDirectory, Path indexCacheDirectory) throws IOException, URISyntaxException { 330 Objects.requireNonNull(stream); 331 Path helmHome = null; 332 if (archiveCacheDirectory == null) { 333 helmHome = ChartRepository.getHelmHome(); 334 assert helmHome != null; 335 archiveCacheDirectory = helmHome.resolve("cache/archive"); 336 assert archiveCacheDirectory != null; 337 } 338 if (!Files.isDirectory(archiveCacheDirectory)) { 339 throw new IllegalArgumentException("!Files.isDirectory(archiveCacheDirectory): " + archiveCacheDirectory); 340 } 341 if (indexCacheDirectory == null) { 342 if (helmHome == null) { 343 helmHome = ChartRepository.getHelmHome(); 344 assert helmHome != null; 345 } 346 indexCacheDirectory = helmHome.resolve("repository/cache"); 347 assert indexCacheDirectory != null; 348 } 349 if (!Files.isDirectory(indexCacheDirectory)) { 350 throw new IllegalArgumentException("!Files.isDirectory(indexCacheDirectory): " + indexCacheDirectory); 351 } 352 final Map<?, ?> map = new Yaml().loadAs(stream, Map.class); 353 if (map == null || map.isEmpty()) { 354 throw new IllegalArgumentException("No data readable from stream: " + stream); 355 } 356 final Set<ChartRepository> chartRepositories; 357 @SuppressWarnings("unchecked") 358 final Collection<? extends Map<?, ?>> repositories = (Collection<? extends Map<?, ?>>)map.get("repositories"); 359 if (repositories == null || repositories.isEmpty()) { 360 chartRepositories = Collections.emptySet(); 361 } else { 362 chartRepositories = new LinkedHashSet<>(); 363 for (final Map<?, ?> repositoryMap : repositories) { 364 if (repositoryMap != null && !repositoryMap.isEmpty()) { 365 final String name = Objects.requireNonNull((String)repositoryMap.get("name")); 366 final URI uri = new URI((String)repositoryMap.get("url")); 367 Path cache = Objects.requireNonNull(Paths.get((String)repositoryMap.get("cache"))); 368 if (!cache.isAbsolute()) { 369 cache = indexCacheDirectory.resolve(cache); 370 assert cache.isAbsolute(); 371 } 372 373 final ChartRepository chartRepository = new ChartRepository(name, uri, archiveCacheDirectory, indexCacheDirectory, cache); 374 chartRepositories.add(chartRepository); 375 } 376 } 377 } 378 return new ChartRepositoryRepository(chartRepositories); 379 } 380 381}