Skip to content

Commit

Permalink
Helm client support
Browse files Browse the repository at this point in the history
  • Loading branch information
spriadka authored and simkam committed Jul 27, 2022
1 parent b9e3f2f commit d54220d
Show file tree
Hide file tree
Showing 21 changed files with 532 additions and 59 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Maven
target
tmp

# Eclipse
.settings
Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,17 @@ JUnit5 module provides a number of extensions and listeners designed to easy up
See [JUnit5](https://github.com/xtf-cz/xtf/blob/master/core/src/main/java/cz/xtf/core/waiting/SimpleWaiter.java) for
more information.

### Helm

You can use `HelmBinary.execute()` method to run Helm against your cluster. Following Helm properties are introduced:

| Property name | Type | Description | Default value |
----------------|------|-------------|---------------|
| `xtf.helm.clients.url` | `String` | URL from which version specified by `xtf.helm.client.version` | `https://mirror.openshift.com/pub/openshift-v4/clients/helm` |
| `xtf.helm.client.version` | `String` | Version of the Helm client to be downloaded (from `http://[xtf.clients.url]/[xtf.client.version`) | `latest` |
| `xtf.helm.binary.path` | `String` | Path to existing Helm client binary. If absent, it will be downloaded using combination of `xtf.helm.clients.url` and `xtf.helm.client.version` parameters | |

## Releasing XTF
Have a look to the [release documentation](RELEASE.md) to learn about the process that defines how to release XTF to
the community.

30 changes: 30 additions & 0 deletions core/src/main/java/cz/xtf/core/config/HelmConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package cz.xtf.core.config;

import java.nio.file.Paths;

public class HelmConfig {

public static final String HELM_BINARY_CACHE_PATH = "xtf.helm.binary.cache.path";
public static final String HELM_BINARY_CACHE_ENABLED = "xtf.helm.binary.cache.enabled";
public static final String HELM_BINARY_PATH = "xtf.helm.binary.path";
public static final String HELM_CLIENT_VERSION = "xtf.helm.client.version";
private static final String HELM_BINARY_CACHE_DEFAULT_FOLDER = "xtf-helm-cache";

public static boolean isHelmBinaryCacheEnabled() {
return Boolean.parseBoolean(XTFConfig.get(HELM_BINARY_CACHE_ENABLED, "true"));
}

public static String binaryCachePath() {
return XTFConfig.get(HELM_BINARY_CACHE_PATH, Paths.get(System.getProperty("java.io.tmpdir"),
HELM_BINARY_CACHE_DEFAULT_FOLDER).toAbsolutePath().normalize().toString());
}

public static String binaryPath() {
return XTFConfig.get(HELM_BINARY_PATH);
}

public static String helmClientVersion() {
return XTFConfig.get(HELM_CLIENT_VERSION, "latest");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package cz.xtf.core.helm;

import cz.xtf.core.config.HelmConfig;

class ConfiguredPathHelmBinaryResolver implements HelmBinaryPathResolver {
@Override
public String resolve() {
return HelmConfig.binaryPath();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package cz.xtf.core.helm;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.SystemUtils;

import cz.xtf.core.config.HelmConfig;
import cz.xtf.core.http.Https;
import cz.xtf.core.openshift.ClusterVersionInfo;
import cz.xtf.core.openshift.ClusterVersionInfoFactory;
import lombok.extern.slf4j.Slf4j;

@Slf4j
class ConfiguredVersionHelmBinaryPathResolver implements HelmBinaryPathResolver {

private static final String OCP_4_HELM_BINARY_DOWNLOAD_URL = "https://mirror.openshift.com/pub/openshift-v4/clients/helm";

@Override
public String resolve() {
final ClusterVersionInfo clusterVersionInfo = ClusterVersionInfoFactory.INSTANCE.getClusterVersionInfo();
if (!clusterVersionInfo.getOpenshiftVersion().startsWith("4")) {
log.warn(
"Unsupported Openshift version for Helm client, OCP cluster is of version {}, while currently only OCP 4 is supported",
clusterVersionInfo.getOpenshiftVersion());
return null;
}
final boolean cacheEnabled = HelmConfig.isHelmBinaryCacheEnabled();
final String helmClientVersion = HelmConfig.helmClientVersion();
final String clientUrl = getHelmClientUrlBasedOnConfiguredHelmVersion(helmClientVersion);
final Path archivePath = getCachedOrDownloadClientArchive(clientUrl, helmClientVersion, cacheEnabled);
return unpackHelmClientArchive(archivePath, !cacheEnabled);
}

private String unpackHelmClientArchive(final Path archivePath, final boolean deleteArchiveWhenDone) {
Objects.requireNonNull(archivePath);

try {
List<String> args = Stream
.of("tar", "-xf", archivePath.toAbsolutePath().toString(), "-C",
getProjectHelmDir().toAbsolutePath().toString())
.collect(Collectors.toList());
ProcessBuilder pb = new ProcessBuilder(args);

pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
pb.redirectError(ProcessBuilder.Redirect.INHERIT);

int result = pb.start().waitFor();

if (result != 0) {
throw new IOException("Failed to execute: " + args);
}

} catch (IOException | InterruptedException e) {
throw new IllegalStateException("Failed to extract helm binary " + archivePath.toAbsolutePath(), e);
}

try {
if (deleteArchiveWhenDone) {
Files.delete(archivePath);
}
} catch (IOException ioe) {
log.warn("It wasn't possible to delete Helm client archive {}", archivePath.toAbsolutePath(), ioe);
}
try (Stream<Path> helmDirContents = Files.list(getProjectHelmDir())) {
Path helmBinaryFile = helmDirContents.collect(Collectors.toList()).get(0);
if (!helmBinaryFile.getFileName().toString().equals("helm")) {
Files.move(helmBinaryFile, helmBinaryFile.resolveSibling("helm"), StandardCopyOption.REPLACE_EXISTING);
}
} catch (IOException e) {
throw new IllegalStateException("Error when extracting Helm client binary", e);
}
return getProjectHelmDir().resolve("helm").toAbsolutePath().toString();
}

private Path getCachedOrDownloadClientArchive(final String url, final String version, final boolean cacheEnabled) {
Objects.requireNonNull(url);

log.debug("Trying to load Helm client archive from cache (enabled: {}) or download it from {}.", cacheEnabled,
url);
Path archivePath;
if (cacheEnabled) {
Path cachePath = Paths.get(HelmConfig.binaryCachePath(), version, DigestUtils.md5Hex(url));
archivePath = cachePath.resolve("helm.tar.gz");
if (Files.exists(archivePath)) {
// it is cached, removed immediately
log.debug("Helm client archive is already in cache: {}.", archivePath.toAbsolutePath());
return archivePath;
}
log.debug("Helm client archive not found in cache, downloading it.");
} else {
archivePath = getProjectHelmDir().resolve("helm.tar.gz");
log.debug("Cache is disabled, downloading Helm client archive to {}.", archivePath.toAbsolutePath());
}

try {
Https.copyHttpsURLToFile(url, archivePath.toFile(), 20_000, 300_000);
} catch (IOException ioe) {
throw new IllegalStateException("Failed to download and extract helm binary from " + url, ioe);
}
return archivePath;
}

private String getHelmClientUrlBasedOnConfiguredHelmVersion(final String helmClientVersion) {
String systemType = "linux";
if (SystemUtils.IS_OS_MAC) {
systemType = "darwin";
}
return String.format("%s/%s/helm-%s-amd64.tar.gz", OCP_4_HELM_BINARY_DOWNLOAD_URL, helmClientVersion,
systemType);
}

private Path getProjectHelmDir() {
Path dir = Paths.get("tmp/helm/");

try {
Files.createDirectories(dir);
} catch (IOException ioe) {
throw new IllegalStateException("Failed to create directory " + dir.toAbsolutePath(), ioe);
}

return dir;
}
}
57 changes: 57 additions & 0 deletions core/src/main/java/cz/xtf/core/helm/HelmBinary.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package cz.xtf.core.helm;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang3.ArrayUtils;

import cz.xtf.core.config.OpenShiftConfig;
import cz.xtf.core.openshift.CLIUtils;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class HelmBinary {

private final String path;
private final String helmConfigPath;
private final String kubeUsername;
private final String kubeToken;
private final String namespace;

public HelmBinary(String path, String kubeUsername, String kubeToken, String namespace) {
this.path = path;
this.kubeUsername = kubeUsername;
this.kubeToken = kubeToken;
Path helmConfigFile = Paths.get(path).getParent().resolve(".config");
try {
helmConfigFile = Files.createDirectories(helmConfigFile);
} catch (IOException e) {
throw new RuntimeException(e);
}
this.helmConfigPath = helmConfigFile.toAbsolutePath().toString();
this.namespace = namespace;
}

public HelmBinary(String path, String helmConfigPath, String kubeUsername, String kubeToken, String namespace) {
this.path = path;
this.helmConfigPath = helmConfigPath;
this.kubeUsername = kubeUsername;
this.kubeToken = kubeToken;
this.namespace = namespace;
}

public String execute(String... args) {
Map<String, String> environmentVariables = new HashMap<>();
environmentVariables.put("HELM_CONFIG_HOME", helmConfigPath);
environmentVariables.put("HELM_KUBEAPISERVER", OpenShiftConfig.url());
environmentVariables.put("HELM_KUBEASUSER", kubeUsername);
environmentVariables.put("HELM_KUBETOKEN", kubeToken);
environmentVariables.put("HELM_NAMESPACE", namespace);
return CLIUtils.executeCommand(environmentVariables, ArrayUtils.addAll(new String[] { path }, args));
}

}
31 changes: 31 additions & 0 deletions core/src/main/java/cz/xtf/core/helm/HelmBinaryManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package cz.xtf.core.helm;

import cz.xtf.core.config.OpenShiftConfig;
import lombok.extern.slf4j.Slf4j;

@Slf4j
class HelmBinaryManager {
private final String helmBinaryPath;

HelmBinaryManager(String helmBinaryPath) {
this.helmBinaryPath = helmBinaryPath;
}

String getHelmBinaryPath() {
return helmBinaryPath;
}

public HelmBinary adminBinary() {
return getBinary(OpenShiftConfig.adminToken(), OpenShiftConfig.adminUsername(), OpenShiftConfig.namespace());
}

public HelmBinary masterBinary() {
return getBinary(OpenShiftConfig.masterToken(), OpenShiftConfig.masterUsername(), OpenShiftConfig.namespace());
}

private static HelmBinary getBinary(String token, String username, String namespace) {
String helmBinaryPath = HelmBinaryManagerFactory.INSTANCE.getHelmBinaryManager().getHelmBinaryPath();
return new HelmBinary(helmBinaryPath, username, token, namespace);

}
}
33 changes: 33 additions & 0 deletions core/src/main/java/cz/xtf/core/helm/HelmBinaryManagerFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package cz.xtf.core.helm;

import java.util.Arrays;

public enum HelmBinaryManagerFactory {

INSTANCE;

private volatile HelmBinaryManager helmBinaryManager;

HelmBinaryManager getHelmBinaryManager() {
HelmBinaryManager localHelmBinaryManagerRef = helmBinaryManager;
if (localHelmBinaryManagerRef == null) {
synchronized (HelmBinaryManagerFactory.class) {
localHelmBinaryManagerRef = helmBinaryManager;
if (localHelmBinaryManagerRef == null) {
for (HelmBinaryPathResolver resolver : Arrays.asList(new ConfiguredPathHelmBinaryResolver(),
new ConfiguredVersionHelmBinaryPathResolver())) {
String resolvedPath = resolver.resolve();
if (resolvedPath != null) {
helmBinaryManager = localHelmBinaryManagerRef = new HelmBinaryManager(resolvedPath);
break;
}
}
}

}
}
return localHelmBinaryManagerRef;

}

}
13 changes: 13 additions & 0 deletions core/src/main/java/cz/xtf/core/helm/HelmBinaryPathResolver.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package cz.xtf.core.helm;

/**
* Interface for resolving Helm client binary
*/
interface HelmBinaryPathResolver {
/**
* Resolves Helm client binary path
*
* @return Helm client binary path
*/
String resolve();
}
12 changes: 12 additions & 0 deletions core/src/main/java/cz/xtf/core/helm/HelmClients.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package cz.xtf.core.helm;

public class HelmClients {

public static HelmBinary adminBinary() {
return HelmBinaryManagerFactory.INSTANCE.getHelmBinaryManager().adminBinary();
}

public static HelmBinary masterBinary() {
return HelmBinaryManagerFactory.INSTANCE.getHelmBinaryManager().masterBinary();
}
}
16 changes: 16 additions & 0 deletions core/src/main/java/cz/xtf/core/image/Product.java
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ public String templatesBranch() {
return resolveDefaultingProperty("templates.branch");
}

public String helmChartsRepo() {
return resolveDefaultingProperty("helm.charts.repo");
}

public String helmChartsBranch() {
return resolveDefaultingProperty("helm.charts.branch");
}

public String helmChartsLocation() {
return resolveDefaultingProperty("helm.charts.location");
}

public String helmChartExamplesLocation() {
return resolveDefaultingProperty("helm.charts.examples.location");
}

private String resolveDefaultingProperty(String propertyId) {
String value = XTFConfig.get("xtf." + id + "." + propertyId);
String defaultingValue = id.contains(".") ? XTFConfig.get("xtf." + id.replaceAll("\\..*", "") + "." + propertyId)
Expand Down
Loading

0 comments on commit d54220d

Please sign in to comment.