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}