Skip to content

Commit

Permalink
Make DockerSupportService configuration cache compatible
Browse files Browse the repository at this point in the history
Allow filtering docker command output for better cc compatibility
  • Loading branch information
breskeby committed Oct 23, 2023
1 parent 84afa5c commit e13a767
Show file tree
Hide file tree
Showing 3 changed files with 171 additions and 55 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

package org.elasticsearch.gradle.internal.docker;

import java.util.Objects;

/**
* This class models the result of running a command. It captures the exit code, standard output and standard error and allows
* applying String filter for stdout as this is intended to create configuration cache compatible output which
* aims to be agnostic.
*/
public class DockerResult {

private int exitCode;
private String stdout;
private String stderr;

public DockerResult(int exitCode, String stdout, String stderr) {
this.exitCode = exitCode;
this.stdout = stdout;
this.stderr = stderr;
}

public int getExitCode() {
return exitCode;
}

public String getStdout() {
return stdout;
}

public String getStderr() {
return stderr;
}

public void setExitCode(int exitCode) {
this.exitCode = exitCode;
}

public void setStdout(String stdout) {
this.stdout = stdout;
}

public void setStderr(String stderr) {
this.stderr = stderr;
}

public boolean isSuccess() {
return exitCode == 0;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DockerResult that = (DockerResult) o;
return exitCode == that.exitCode && Objects.equals(stdout, that.stdout) && Objects.equals(stderr, that.stderr);
}

@Override
public int hashCode() {
return Objects.hash(exitCode, stdout, stderr);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,10 @@
import org.gradle.api.GradleException;
import org.gradle.api.logging.Logger;
import org.gradle.api.logging.Logging;
import org.gradle.api.provider.ProviderFactory;
import org.gradle.api.services.BuildService;
import org.gradle.api.services.BuildServiceParameters;
import org.gradle.process.ExecOperations;
import org.gradle.process.ExecResult;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
Expand Down Expand Up @@ -56,12 +54,12 @@ public abstract class DockerSupportService implements BuildService<DockerSupport
"/usr/libexec/docker/cli-plugins/docker-compose" };
private static final Version MINIMUM_DOCKER_VERSION = Version.fromString("17.05.0");

private final ExecOperations execOperations;
private final ProviderFactory providerFactory;
private DockerAvailability dockerAvailability;

@Inject
public DockerSupportService(ExecOperations execOperations) {
this.execOperations = execOperations;
public DockerSupportService(ProviderFactory providerFactory) {
this.providerFactory = providerFactory;
}

/**
Expand All @@ -71,9 +69,9 @@ public DockerSupportService(ExecOperations execOperations) {
*/
public DockerAvailability getDockerAvailability() {
if (this.dockerAvailability == null) {
String dockerPath = null;
String dockerPath;
String dockerComposePath = null;
Result lastResult = null;
DockerResult lastResult = null;
Version version = null;
boolean isVersionHighEnough = false;
boolean isComposeAvailable = false;
Expand All @@ -86,21 +84,17 @@ public DockerAvailability getDockerAvailability() {

// Since we use a multi-stage Docker build, check the Docker version meets minimum requirement
lastResult = runCommand(dockerPath, "version", "--format", "{{.Server.Version}}");

var lastResultOutput = lastResult.stdout.trim();
var lastResultOutput = lastResult.getStdout().trim();
// docker returns 0/success if the daemon is not running, so we need to check the
// output before continuing
if (lastResult.isSuccess() && dockerDaemonIsRunning(lastResultOutput)) {

version = Version.fromString(lastResultOutput, Version.Mode.RELAXED);

isVersionHighEnough = version.onOrAfter(MINIMUM_DOCKER_VERSION);

if (isVersionHighEnough) {
// Check that we can execute a privileged command
lastResult = runCommand(dockerPath, "images");
lastResult = runCommand(Arrays.asList(dockerPath, "images"), input -> "");

// If docker all checks out, see if docker-compose is available and working
Optional<String> composePath = getDockerComposePath();
if (lastResult.isSuccess() && composePath.isPresent()) {
isComposeAvailable = runCommand(composePath.get(), "version").isSuccess();
Expand All @@ -109,9 +103,12 @@ public DockerAvailability getDockerAvailability() {

// Now let's check if buildx is available and what supported platforms exist
if (lastResult.isSuccess()) {
Result buildxResult = runCommand(dockerPath, "buildx", "inspect", "--bootstrap");
DockerResult buildxResult = runCommand(
Arrays.asList(dockerPath, "buildx", "inspect", "--bootstrap"),
input -> input.lines().filter(l -> l.startsWith("Platforms:")).collect(Collectors.joining("\n"))
);
if (buildxResult.isSuccess()) {
supportedArchitectures = buildxResult.stdout()
supportedArchitectures = buildxResult.getStdout()
.lines()
.filter(l -> l.startsWith("Platforms:"))
.map(l -> l.substring(10))
Expand All @@ -127,6 +124,8 @@ public DockerAvailability getDockerAvailability() {
}
}
}
} else {
dockerPath = null;
}

boolean isAvailable = isVersionHighEnough && lastResult != null && lastResult.isSuccess();
Expand All @@ -146,6 +145,17 @@ public DockerAvailability getDockerAvailability() {
return this.dockerAvailability;
}

private DockerResult runCommand(List args, DockerValueSource.OutputFilter outputFilter) {
return providerFactory.of(DockerValueSource.class, params -> {
params.getParameters().getArgs().addAll(args);
params.getParameters().getOutputFilter().set(outputFilter);
}).get();
}

private DockerResult runCommand(String... args) {
return runCommand(Arrays.asList(args), input -> input);
}

private boolean dockerDaemonIsRunning(String lastResultOutput) {
return lastResultOutput.contains("Cannot connect to the Docker daemon") == false;
}
Expand Down Expand Up @@ -198,8 +208,8 @@ void failIfDockerUnavailable(List<String> tasks) {
availability.version == null ? "" : " v" + availability.version,
tasks.size() > 1 ? "s" : "",
String.join("\n", tasks),
availability.lastCommand.exitCode,
availability.lastCommand.stderr.trim()
availability.lastCommand.getExitCode(),
availability.lastCommand.getStderr().trim()
);
throwDockerRequiredException(message);
}
Expand Down Expand Up @@ -319,32 +329,6 @@ private void throwDockerRequiredException(final String message, Exception e) {
);
}

/**
* Runs a command and captures the exit code, standard output and standard error.
*
* @param args the command and any arguments to execute
* @return a object that captures the result of running the command. If an exception occurring
* while running the command, or the process was killed after reaching the 10s timeout,
* then the exit code will be -1.
*/
private Result runCommand(String... args) {
if (args.length == 0) {
throw new IllegalArgumentException("Cannot execute with no command");
}

ByteArrayOutputStream stdout = new ByteArrayOutputStream();
ByteArrayOutputStream stderr = new ByteArrayOutputStream();

final ExecResult execResult = execOperations.exec(spec -> {
// The redundant cast is to silence a compiler warning.
spec.setCommandLine((Object[]) args);
spec.setStandardOutput(stdout);
spec.setErrorOutput(stderr);
spec.setIgnoreExitValue(true);
});
return new Result(execResult.getExitValue(), stdout.toString(), stderr.toString());
}

/**
* An immutable class that represents the results of a Docker search from {@link #getDockerAvailability()}}.
*/
Expand Down Expand Up @@ -377,22 +361,12 @@ public record DockerAvailability(
Version version,

// Information about the last command executes while probing Docker, or null.
Result lastCommand,
DockerResult lastCommand,

// Supported build architectures
Set<Architecture> supportedArchitectures
) {}

/**
* This class models the result of running a command. It captures the exit code, standard output and standard error.
*/
private record Result(int exitCode, String stdout, String stderr) {

boolean isSuccess() {
return exitCode == 0;
}
}

interface Parameters extends BuildServiceParameters {
File getExclusionsFile();

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

package org.elasticsearch.gradle.internal.docker;

import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;
import org.gradle.api.provider.ValueSource;
import org.gradle.api.provider.ValueSourceParameters;
import org.gradle.process.ExecOperations;
import org.gradle.process.ExecResult;

import java.io.ByteArrayOutputStream;
import java.util.List;

import javax.inject.Inject;

public abstract class DockerValueSource implements ValueSource<DockerResult, DockerValueSource.Parameters> {
public interface OutputFilter {
String filter(String input);
}

interface Parameters extends ValueSourceParameters {
ListProperty<String> getArgs();

Property<OutputFilter> getOutputFilter();
}

@Inject
abstract protected ExecOperations getExecOperations();

@Override
public DockerResult obtain() {
return runCommand(getParameters().getArgs().get());
}

/**
* Runs a command and captures the exit code, standard output and standard error.
*
* @param args the command and any arguments to execute
* @return a object that captures the result of running the command. If an exception occurring
* while running the command, or the process was killed after reaching the 10s timeout,
* then the exit code will be -1.
*/
private DockerResult runCommand(List args) {
if (args.size() == 0) {
throw new IllegalArgumentException("Cannot execute with no command");
}

ByteArrayOutputStream stdout = new ByteArrayOutputStream();
ByteArrayOutputStream stderr = new ByteArrayOutputStream();

final ExecResult execResult = getExecOperations().exec(spec -> {
// The redundant cast is to silence a compiler warning.
spec.setCommandLine(args);
spec.setStandardOutput(stdout);
spec.setErrorOutput(stderr);
spec.setIgnoreExitValue(true);
});
return new DockerResult(execResult.getExitValue(), filtered(stdout.toString()), stderr.toString());
}

private String filtered(String input) {
return getParameters().getOutputFilter().get().filter(input);
}

}

0 comments on commit e13a767

Please sign in to comment.