Browse Source

Make DockerSupportService configuration cache compatible (#101140)

Allow filtering docker command output for better cc compatibility
Rene Groeschke 1 year ago
parent
commit
f02bbc356d

+ 70 - 0
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/docker/DockerResult.java

@@ -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);
+    }
+}

+ 29 - 55
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/docker/DockerSupportService.java

@@ -14,12 +14,10 @@ import org.elasticsearch.gradle.internal.info.BuildParams;
 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;
@@ -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;
     }
 
     /**
@@ -71,9 +69,9 @@ public abstract class DockerSupportService implements BuildService<DockerSupport
      */
     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;
@@ -86,21 +84,17 @@ public abstract class DockerSupportService implements BuildService<DockerSupport
 
                 // 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();
@@ -109,9 +103,12 @@ public abstract class DockerSupportService implements BuildService<DockerSupport
 
                         // 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))
@@ -127,6 +124,8 @@ public abstract class DockerSupportService implements BuildService<DockerSupport
                         }
                     }
                 }
+            } else {
+                dockerPath = null;
             }
 
             boolean isAvailable = isVersionHighEnough && lastResult != null && lastResult.isSuccess();
@@ -146,6 +145,17 @@ public abstract class DockerSupportService implements BuildService<DockerSupport
         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;
     }
@@ -198,8 +208,8 @@ public abstract class DockerSupportService implements BuildService<DockerSupport
             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);
     }
@@ -319,32 +329,6 @@ public abstract class DockerSupportService implements BuildService<DockerSupport
         );
     }
 
-    /**
-     * 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()}}.
      */
@@ -377,22 +361,12 @@ public abstract class DockerSupportService implements BuildService<DockerSupport
         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();
 

+ 72 - 0
build-tools-internal/src/main/java/org/elasticsearch/gradle/internal/docker/DockerValueSource.java

@@ -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);
+    }
+
+}