package com.kiwigrid.helm.maven.plugin; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStreamReader; import java.net.PasswordAuthentication; import java.nio.file.Files; import java.nio.file.FileVisitOption; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Optional; import java.util.function.Predicate; import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; import com.kiwigrid.helm.maven.plugin.pojo.HelmRepository; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.settings.Server; import org.apache.maven.settings.Settings; import org.codehaus.plexus.util.FileUtils; import org.codehaus.plexus.util.MatchPatterns; import org.sonatype.plexus.components.sec.dispatcher.DefaultSecDispatcher; import org.sonatype.plexus.components.sec.dispatcher.SecDispatcher; import org.sonatype.plexus.components.sec.dispatcher.SecDispatcherException; /** * Base class for mojos * * @author Fabian Schlegel * @since 06.11.17 */ public abstract class AbstractHelmMojo extends AbstractMojo { @Component(role = org.sonatype.plexus.components.sec.dispatcher.SecDispatcher.class, hint = "default") private SecDispatcher securityDispatcher; @Parameter(property = "helm.useLocalHelmBinary", defaultValue = "false") private boolean useLocalHelmBinary; @Parameter(property = "helm.autoDetectLocalHelmBinary", defaultValue = "true") private boolean autoDetectLocalHelmBinary; @Parameter(property = "helm.executableDirectory", defaultValue = "${project.build.directory}/helm") private String helmExecutableDirectory; @Parameter(property = "helm.outputDirectory", defaultValue = "${project.build.directory}/helm/repo") private String outputDirectory; @Parameter(property = "helm.excludes") private String[] excludes; @Parameter(property = "helm.chartDirectory", required = true) private String chartDirectory; @Parameter(property = "helm.chartVersion", required = true) private String chartVersion; @Parameter(property = "helm.appVersion") private String appVersion; @Parameter(property = "helm.uploadRepo.stable") private HelmRepository uploadRepoStable; @Parameter(property = "helm.uploadRepo.snapshot") private HelmRepository uploadRepoSnapshot; @Parameter(property = "helm.downloadUrl") private String helmDownloadUrl; @Parameter(property = "helm.version", defaultValue = "3.2.0") private String helmVersion; @Parameter(property = "helm.registryConfig") private String registryConfig; @Parameter(property = "helm.repositoryCache") private String repositoryCache; @Parameter(property = "helm.repositoryConfig") private String repositoryConfig; @Parameter(property = "helm.extraRepos") private HelmRepository[] helmExtraRepos; @Parameter(property = "helm.security", defaultValue = "~/.m2/settings-security.xml") private String helmSecurity; @Parameter(property = "helm.skip", defaultValue = "false") protected boolean skip; /** * The current user system settings for use in Maven. */ @Parameter(defaultValue = "${settings}", readonly = true) private Settings settings; Path getHelmExecuteablePath() throws MojoExecutionException { String helmExecutable = SystemUtils.IS_OS_WINDOWS ? "helm.exe" : "helm"; Optional<Path> path; if (isUseLocalHelmBinary() && isAutoDetectLocalHelmBinary()) { path = findInPath(helmExecutable); } else { path = Optional.of(Paths.get(helmExecutableDirectory, helmExecutable)) .map(Path::toAbsolutePath) .filter(Files::exists); } return path.orElseThrow(() -> new MojoExecutionException("Helm executable is not found.")); } /** * Finds the absolute path to a given {@code executable} in {@code PATH} environment variable. * * @param executable the name of the executable to search for * @return the absolute path to the executable if found, otherwise an empty optional. */ private Optional<Path> findInPath(final String executable) { final String[] paths = getPathsFromEnvironmentVariables(); return Stream.of(paths) .map(Paths::get) .map(path -> path.resolve(executable)) .filter(Files::exists) .map(Path::toAbsolutePath) .findFirst(); } String[] getPathsFromEnvironmentVariables() { return System.getenv("PATH").split(Pattern.quote(File.pathSeparator)); } /** * Calls cli with specified command * * @param command the command to be executed * @param errorMessage a readable error message that will be shown in case of exceptions * @param verbose logs STDOUT to Maven info log * @throws MojoExecutionException on error */ void callCli(String command, String errorMessage, final boolean verbose) throws MojoExecutionException { int exitValue; getLog().debug(command); try { final Process p = Runtime.getRuntime().exec(command); new Thread(() -> { BufferedReader input = new BufferedReader(new InputStreamReader(p.getInputStream())); BufferedReader error = new BufferedReader(new InputStreamReader(p.getErrorStream())); String inputLine; String errorLine; try { while ((inputLine = input.readLine()) != null) { if (verbose) { getLog().info(inputLine); } else { getLog().debug(inputLine); } } while ((errorLine = error.readLine()) != null) { getLog().error(errorLine); } } catch (IOException e) { getLog().error(e); } }).start(); p.waitFor(); exitValue = p.exitValue(); } catch (Exception e) { throw new MojoExecutionException("Error processing command [" + command + "]", e); } if (exitValue != 0) { throw new MojoExecutionException(errorMessage); } } List<String> getChartDirectories(String path) throws MojoExecutionException { List<String> exclusions = new ArrayList<>(); if (getExcludes() != null) { exclusions.addAll(Arrays.asList(getExcludes())); } exclusions.addAll(FileUtils.getDefaultExcludesAsList()); MatchPatterns exclusionPatterns = MatchPatterns.from(exclusions); try (Stream<Path> files = Files.walk(Paths.get(path), FileVisitOption.FOLLOW_LINKS)) { List<String> chartDirs = files.filter(p -> p.getFileName().toString().equalsIgnoreCase("chart.yaml")) .map(p -> p.getParent().toString()) .filter(shouldIncludeDirectory(exclusionPatterns)) .collect(Collectors.toList()); if (chartDirs.isEmpty()) { getLog().warn("No Charts detected - no Chart.yaml files found below " + path); } return chartDirs; } catch (IOException e) { throw new MojoExecutionException("Unable to scan chart directory at " + path, e); } } private Predicate<String> shouldIncludeDirectory(MatchPatterns exclusionPatterns) { return inputDirectory -> { final boolean isCaseSensitive = Boolean.FALSE; boolean matches = exclusionPatterns.matches(inputDirectory, isCaseSensitive); if (matches) { getLog().debug("Skip excluded directory " + inputDirectory); return Boolean.FALSE; } return Boolean.TRUE; }; } List<String> getChartTgzs(String path) throws MojoExecutionException { try (Stream<Path> files = Files.walk(Paths.get(path))) { return files.filter(p -> p.getFileName().toString().endsWith("tgz")) .map(Path::toString) .collect(Collectors.toList()); } catch (IOException e) { throw new MojoExecutionException("Unable to scan repo directory at " + path, e); } } /** * Returns the proper upload URL based on the provided chart version. * Charts w/ an SNAPSHOT suffix will be uploaded to SNAPSHOT repo. * * @return Upload URL based on chart version */ String getHelmUploadUrl() { String uploadUrl = uploadRepoStable.getUrl(); if (chartVersion.endsWith("-SNAPSHOT") && uploadRepoSnapshot != null && StringUtils.isNotEmpty(uploadRepoSnapshot.getUrl())) { uploadUrl = uploadRepoSnapshot.getUrl(); } return uploadUrl; } HelmRepository getHelmUploadRepo() { if (chartVersion.endsWith("-SNAPSHOT") && uploadRepoSnapshot != null && StringUtils.isNotEmpty(uploadRepoSnapshot.getUrl())) { return uploadRepoSnapshot; } return uploadRepoStable; } /** * Get credentials for given helm repo. If username is not provided the repo * name will be used to search for credentials in <code>settings.xml</code>. * * @param repository Helm repo with id and optional credentials. * @return Authentication object or <code>null</code> if no credentials are present. * @throws IllegalArgumentException Unable to get authentication because of misconfiguration. * @throws MojoExecutionException Unable to get password from settings.xml */ PasswordAuthentication getAuthentication(HelmRepository repository) throws IllegalArgumentException, MojoExecutionException { String id = repository.getName(); if (repository.getUsername() != null) { if (repository.getPassword() == null) { throw new IllegalArgumentException("Repo " + id + " has a username but no password defined."); } getLog().debug("Repo " + id + " has credentials definded, skip searching in server list."); return new PasswordAuthentication(repository.getUsername(), repository.getPassword().toCharArray()); } Server server = settings.getServer(id); if (server == null) { getLog().info("No credentials found for " + id + " in configuration or settings.xml server list."); return null; } getLog().debug("Use credentials from server list for " + id + "."); if (server.getUsername() == null || server.getPassword() == null) { throw new IllegalArgumentException("Repo " + id + " was found in server list but has no username/password."); } try { return new PasswordAuthentication(server.getUsername(), getSecDispatcher().decrypt(server.getPassword()).toCharArray()); } catch (SecDispatcherException e) { throw new MojoExecutionException(e.getMessage()); } } protected SecDispatcher getSecDispatcher() { if (securityDispatcher instanceof DefaultSecDispatcher) { ((DefaultSecDispatcher) securityDispatcher).setConfigurationFile(getHelmSecurity()); } return securityDispatcher; } public String getOutputDirectory() { return outputDirectory; } public void setOutputDirectory(String outputDirectory) { this.outputDirectory = outputDirectory; } public String getHelmExecutableDirectory() { return helmExecutableDirectory; } public void setHelmExecutableDirectory(String helmExecuteableDirectory) { this.helmExecutableDirectory = helmExecuteableDirectory; } public String getHelmDownloadUrl() { return helmDownloadUrl; } public void setHelmDownloadUrl(String helmDownloadUrl) { this.helmDownloadUrl = helmDownloadUrl; } public String getHelmVersion() { return helmVersion; } public void setHelmVersion(String helmVersion) { this.helmVersion = helmVersion; } public String[] getExcludes() { return excludes; } public void setExcludes(String[] excludes) { this.excludes = excludes; } public String getChartDirectory() { return chartDirectory; } public void setChartDirectory(String chartDirectory) { this.chartDirectory = chartDirectory; } public String getRegistryConfig() { return registryConfig; } public void setRegistryConfig(String registryConfig) { this.registryConfig = registryConfig; } public String getRepositoryCache() { return repositoryCache; } public void setRepositoryCache(String repositoryCache) { this.repositoryCache = repositoryCache; } public String getRepositoryConfig() { return repositoryConfig; } public void setRepositoryConfig(String repositoryConfig) { this.repositoryConfig = repositoryConfig; } public String getChartVersion() { return chartVersion; } public void setChartVersion(String chartVersion) { this.chartVersion = chartVersion; } public String getAppVersion() { return appVersion; } public void setAppVersion(String appVersion) { this.appVersion = appVersion; } public HelmRepository[] getHelmExtraRepos() { return helmExtraRepos; } public void setHelmExtraRepos(HelmRepository[] helmExtraRepos) { this.helmExtraRepos = helmExtraRepos; } public HelmRepository getUploadRepoStable() { return uploadRepoStable; } public void setUploadRepoStable(HelmRepository uploadRepoStable) { this.uploadRepoStable = uploadRepoStable; } public HelmRepository getUploadRepoSnapshot() { return uploadRepoSnapshot; } public void setUploadRepoSnapshot(HelmRepository uploadRepoSnapshot) { this.uploadRepoSnapshot = uploadRepoSnapshot; } public String getHelmSecurity() { return helmSecurity; } public void setHelmSecurity(String helmSecurity) { this.helmSecurity = helmSecurity; } public Settings getSettings() { return settings; } public boolean isUseLocalHelmBinary() { return useLocalHelmBinary; } public void setUseLocalHelmBinary(boolean useLocalHelmBinary) { this.useLocalHelmBinary = useLocalHelmBinary; } public boolean isAutoDetectLocalHelmBinary() { return autoDetectLocalHelmBinary; } public void setAutoDetectLocalHelmBinary(final boolean autoDetectLocalHelmBinary) { this.autoDetectLocalHelmBinary = autoDetectLocalHelmBinary; } }