diff --git a/plugin/src/main/java/hudson/plugins/gradle/BuildToolType.java b/plugin/src/main/java/hudson/plugins/gradle/BuildToolType.java index 5cbf8cf5..0a5db89c 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/BuildToolType.java +++ b/plugin/src/main/java/hudson/plugins/gradle/BuildToolType.java @@ -9,7 +9,9 @@ public enum BuildToolType { @JsonProperty("gradle") GRADLE, @JsonProperty("maven") - MAVEN; + MAVEN, + @JsonProperty("npm") + NPM; public String getAttributesUrlSuffix() { return String.format("/%s-attributes", name().toLowerCase(Locale.ROOT)); diff --git a/plugin/src/main/java/hudson/plugins/gradle/enriched/ScanDetail.java b/plugin/src/main/java/hudson/plugins/gradle/enriched/ScanDetail.java index 0608d33f..f47f50c6 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/enriched/ScanDetail.java +++ b/plugin/src/main/java/hudson/plugins/gradle/enriched/ScanDetail.java @@ -2,11 +2,13 @@ import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonSetter; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import hudson.plugins.gradle.BuildToolType; import org.kohsuke.stapler.export.ExportedBean; import java.util.List; +import java.util.Map; import java.util.Objects; @ExportedBean @@ -15,7 +17,7 @@ public class ScanDetail { private final String url; - @JsonAlias({"rootProjectName", "topLevelProjectName"}) + @JsonAlias({"rootProjectName", "topLevelProjectName", "packageName"}) private String projectName; @SuppressFBWarnings(value = "UWF_UNWRITTEN_FIELD") private BuildToolType buildToolType; @@ -26,6 +28,18 @@ public class ScanDetail { @SuppressFBWarnings(value = "UWF_UNWRITTEN_FIELD") private boolean hasFailed; + // Converts NpmCommand object into a list of tasks + @JsonSetter("command") + private void setCommand(Map npmCommand) { + if (npmCommand == null) { + return; + } + Object command = npmCommand.get("command"); + if (command != null && (tasks == null || tasks.isEmpty())) { + tasks = List.of(command.toString()); + } + } + ScanDetail(String url) { this.url = url; } @@ -59,7 +73,12 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ScanDetail that = (ScanDetail) o; - return hasFailed == that.hasFailed && Objects.equals(url, that.url) && Objects.equals(projectName, that.projectName) && buildToolType == that.buildToolType && Objects.equals(buildToolVersion, that.buildToolVersion) && Objects.equals(tasks, that.tasks); + return hasFailed == that.hasFailed + && Objects.equals(url, that.url) + && Objects.equals(projectName, that.projectName) + && buildToolType == that.buildToolType + && Objects.equals(buildToolVersion, that.buildToolVersion) + && Objects.equals(tasks, that.tasks); } @Override diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/ArtifactDigest.java b/plugin/src/main/java/hudson/plugins/gradle/injection/ArtifactDigest.java new file mode 100644 index 00000000..8661dfd6 --- /dev/null +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/ArtifactDigest.java @@ -0,0 +1,8 @@ +package hudson.plugins.gradle.injection; + +public record ArtifactDigest(String digest) { + + public boolean matches(String otherDigest) { + return digest.equals(otherDigest); + } +} diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/ArtifactMetadata.java b/plugin/src/main/java/hudson/plugins/gradle/injection/ArtifactMetadata.java new file mode 100644 index 00000000..e821fcf0 --- /dev/null +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/ArtifactMetadata.java @@ -0,0 +1,38 @@ +package hudson.plugins.gradle.injection; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; + +public record ArtifactMetadata(String version, ArtifactDigest digest) { + + private static final String SEPARATOR = ","; + + // Adapter from the old interface + public ArtifactMetadata(String version, String digest) { + this(version, new ArtifactDigest(digest)); + } + + public boolean isForVersion(@Nullable String requiredVersion) { + return version.equals(requiredVersion); + } + + public void writeToFile(Path metadataFile) throws IOException { + String content = version + SEPARATOR + digest.digest(); + Files.writeString(metadataFile, content); + } + + public static Optional readFromFile(Path metadataFile) throws IOException { + if (Files.exists(metadataFile)) { + String[] metadata = Files.readString(metadataFile).split(SEPARATOR); + if (metadata.length == 2) { + String version = metadata[0]; + ArtifactDigest digest = new ArtifactDigest(metadata[1]); + return Optional.of(new ArtifactMetadata(version, digest)); + } + } + return Optional.empty(); + } +} diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/BuildScanEnvironmentContributor.java b/plugin/src/main/java/hudson/plugins/gradle/injection/BuildScanEnvironmentContributor.java index 0dec8a12..a896d26d 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/BuildScanEnvironmentContributor.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/BuildScanEnvironmentContributor.java @@ -2,6 +2,7 @@ import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; +import com.google.common.annotations.VisibleForTesting; import hudson.EnvVars; import hudson.Extension; import hudson.model.EnvironmentContributor; @@ -33,12 +34,13 @@ public class BuildScanEnvironmentContributor extends EnvironmentContributor { private final ShortLivedTokenClientFactory shortLivedTokenClientFactory; + @SuppressWarnings("unused") public BuildScanEnvironmentContributor() { - this.shortLivedTokenClientFactory = new ShortLivedTokenClientFactory(); + this(new ShortLivedTokenClientFactory()); } - // required for testing - public BuildScanEnvironmentContributor(ShortLivedTokenClientFactory shortLivedTokenClientFactory) { + @VisibleForTesting + BuildScanEnvironmentContributor(ShortLivedTokenClientFactory shortLivedTokenClientFactory) { this.shortLivedTokenClientFactory = shortLivedTokenClientFactory; } @@ -67,10 +69,11 @@ public void buildEnvironmentFor(@Nonnull Run run, @Nonnull EnvVars envs, @Nonnul Secret shortLivedToken = getShortLivedToken(secretKey, logger); - run.addAction(DevelocityParametersAction.of(logger, shortLivedToken, secretPassword)); + run.addAction(DevelocityParametersAction.of(shortLivedToken, secretPassword)); } - private @Nullable Secret getShortLivedToken(Secret secretKey, DevelocityLogger logger) { + @Nullable + private Secret getShortLivedToken(Secret secretKey, DevelocityLogger logger) { if (secretKey == null) { return null; } @@ -143,7 +146,7 @@ static DevelocityParametersAction empty() { return EMPTY; } - private static DevelocityParametersAction of(DevelocityLogger logger, @Nullable Secret shortLivedToken, @Nullable Secret repoPassword) { + private static DevelocityParametersAction of(@Nullable Secret shortLivedToken, @Nullable Secret repoPassword) { List values = new ArrayList<>(); if (shortLivedToken != null) { values.add(new PasswordParameterValue(GRADLE_ENTERPRISE_ACCESS_KEY, shortLivedToken.getPlainText())); diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/CopyUtil.java b/plugin/src/main/java/hudson/plugins/gradle/injection/CopyUtil.java index f772e907..f6ab9125 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/CopyUtil.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/CopyUtil.java @@ -2,14 +2,9 @@ import hudson.FilePath; import hudson.Util; -import jenkins.model.Jenkins; -import org.apache.commons.io.IOUtils; import java.io.IOException; import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; public final class CopyUtil { @@ -24,7 +19,7 @@ public static void copyResourceToNode(FilePath nodePath, String resourceName) th } public static void copyDownloadedResourceToNode(FilePath controllerRootPath, FilePath nodePath, String resourceName) throws IOException, InterruptedException { - nodePath.copyFrom(controllerRootPath.child(MavenExtensionDownloadHandler.DOWNLOAD_CACHE_DIR).child(resourceName)); + nodePath.copyFrom(controllerRootPath.child(InjectionUtil.DOWNLOAD_CACHE_DIR).child(resourceName)); } public static String unsafeResourceDigest(String resourceName) { diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/DevelocityComputerListener.java b/plugin/src/main/java/hudson/plugins/gradle/injection/DevelocityComputerListener.java index 35a57cbb..ced9f9e2 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/DevelocityComputerListener.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/DevelocityComputerListener.java @@ -6,9 +6,12 @@ import hudson.model.Computer; import hudson.model.Node; import hudson.model.TaskListener; +import hudson.plugins.gradle.injection.npm.NpmAgentDownloadHandler; +import hudson.plugins.gradle.injection.npm.NpmBuildScanInjection; import hudson.slaves.ComputerListener; import jenkins.model.Jenkins; +import java.io.File; import java.util.Map; import java.util.function.Supplier; import java.util.logging.Level; @@ -27,13 +30,18 @@ public class DevelocityComputerListener extends ComputerListener { private final GradleBuildScanInjection gradleBuildScanInjection; private final MavenBuildScanInjection mavenBuildScanInjection; private final MavenExtensionDownloadHandler mavenExtensionDownloadHandler; + private final NpmBuildScanInjection npmBuildScanInjection; + private final NpmAgentDownloadHandler npmAgentDownloadHandler; private final Supplier injectionConfigSupplier; + @SuppressWarnings("unused") public DevelocityComputerListener() { this( new GradleBuildScanInjection(), new MavenBuildScanInjection(), new MavenExtensionDownloadHandler(), + new NpmBuildScanInjection(), + new NpmAgentDownloadHandler(), new JenkinsInjectionConfig() ); } @@ -43,11 +51,15 @@ public DevelocityComputerListener() { GradleBuildScanInjection gradleBuildScanInjection, MavenBuildScanInjection mavenBuildScanInjection, MavenExtensionDownloadHandler mavenExtensionDownloadHandler, + NpmBuildScanInjection npmBuildScanInjection, + NpmAgentDownloadHandler npmAgentDownloadHandler, Supplier injectionConfigSupplier ) { this.gradleBuildScanInjection = gradleBuildScanInjection; this.mavenBuildScanInjection = mavenBuildScanInjection; this.mavenExtensionDownloadHandler = mavenExtensionDownloadHandler; + this.npmBuildScanInjection = npmBuildScanInjection; + this.npmAgentDownloadHandler = npmAgentDownloadHandler; this.injectionConfigSupplier = injectionConfigSupplier; } @@ -60,15 +72,20 @@ public void onOnline(Computer computer, TaskListener listener) { return; } - Map extensionsDigest = mavenExtensionDownloadHandler.getExtensionDigests( - () -> Jenkins.get().getRootDir(), injectionConfig - ); + Supplier root = () -> Jenkins.get().getRootDir(); + // When the agent becomes online, all artifacts must be already downloaded on the controller. + Map extensionsDigest = mavenExtensionDownloadHandler.getExtensionDigests(root, injectionConfig); + ArtifactDigest npmAgentDigest = + npmBuildScanInjection + .ifInjectionEnabledGlobally(injectionConfig, () -> npmAgentDownloadHandler.getDownloadedNpmAgentDigest(root)) + .orElse(null); Node node = computer.getNode(); EnvVars computerEnvVars = computer.getEnvironment(); gradleBuildScanInjection.inject(node, globalEnvVars, computerEnvVars); mavenBuildScanInjection.inject(node, extensionsDigest); + npmBuildScanInjection.inject(node, npmAgentDigest, computerEnvVars); } catch (Throwable t) { /* * We should catch everything because this is not handled by {@link hudson.slaves.SlaveComputer#setChannel(Channel, OutputStream, Channel.Listener)} diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/DevelocityErrorsAction.java b/plugin/src/main/java/hudson/plugins/gradle/injection/DevelocityErrorsAction.java index c9a0776f..e42a9780 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/DevelocityErrorsAction.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/DevelocityErrorsAction.java @@ -23,8 +23,10 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +@SuppressWarnings("unused") @Extension public class DevelocityErrorsAction implements RootAction, StaplerProxy { + @Override public String getIconFileName() { return isVisible() && Jenkins.get().hasPermission(Jenkins.ADMINISTER) ? "/plugin/gradle/images/svgs/gradle-build-scan.svg" : null; diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/EnvUtil.java b/plugin/src/main/java/hudson/plugins/gradle/injection/EnvUtil.java index 7074ec60..e0de13b4 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/EnvUtil.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/EnvUtil.java @@ -25,7 +25,7 @@ public static EnvVars globalEnvironment() { } EnvironmentVariablesNodeProperty nodeProperty = - jenkins.getGlobalNodeProperties().get(EnvironmentVariablesNodeProperty.class); + jenkins.getGlobalNodeProperties().get(EnvironmentVariablesNodeProperty.class); return nodeProperty != null ? nodeProperty.getEnvVars() : null; } @@ -33,7 +33,7 @@ public static EnvVars globalEnvironment() { @CheckForNull public static String getEnv(Node node, String key) { List all = - node.getNodeProperties().getAll(EnvironmentVariablesNodeProperty.class); + node.getNodeProperties().getAll(EnvironmentVariablesNodeProperty.class); if (all.isEmpty()) { return null; @@ -51,8 +51,9 @@ public static String getEnv(EnvVars env, String key) { public static void removeEnvVars(Node node, Collection keys) { keys.forEach(key -> removeEnvVar(node, key)); } - public static void removeEnvVars(Node node, InitScriptVariables[] keys) { - for (InitScriptVariables key : keys) { + + public static void removeEnvVars(Node node, EnvVar[] keys) { + for (EnvVar key : keys) { removeEnvVar(node, key); } } @@ -60,22 +61,24 @@ public static void removeEnvVars(Node node, InitScriptVariables[] keys) { public static void removeEnvVar(Node node, String key) { setEnvVar(node, key, null); } - public static void removeEnvVar(Node node, InitScriptVariables key) { + + public static void removeEnvVar(Node node, EnvVar key) { setEnvVar(node, key, null); } - public static void setEnvVar(Node node, InitScriptVariables key, @Nullable String value) { + public static void setEnvVar(Node node, EnvVar key, @Nullable String value) { setEnvVar(node, key.getEnvVar(), value); } + public static void setEnvVar(Node node, String key, @Nullable String value) { List all = - node.getNodeProperties().getAll(EnvironmentVariablesNodeProperty.class); + node.getNodeProperties().getAll(EnvironmentVariablesNodeProperty.class); if (all.isEmpty()) { if (value != null) { node.getNodeProperties().add( - new EnvironmentVariablesNodeProperty( - new EnvironmentVariablesNodeProperty.Entry(key, value))); + new EnvironmentVariablesNodeProperty( + new EnvironmentVariablesNodeProperty.Entry(key, value))); } // noop if null return; } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/EnvVar.java b/plugin/src/main/java/hudson/plugins/gradle/injection/EnvVar.java new file mode 100644 index 00000000..6b0c87d4 --- /dev/null +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/EnvVar.java @@ -0,0 +1,6 @@ +package hudson.plugins.gradle.injection; + +public interface EnvVar { + + String getEnvVar(); +} diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/GitScmListener.java b/plugin/src/main/java/hudson/plugins/gradle/injection/GitScmListener.java index 8d016494..1d48fce9 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/GitScmListener.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/GitScmListener.java @@ -4,7 +4,11 @@ import hudson.EnvVars; import hudson.Extension; import hudson.FilePath; -import hudson.model.*; +import hudson.model.Computer; +import hudson.model.EnvironmentContributingAction; +import hudson.model.InvisibleAction; +import hudson.model.Run; +import hudson.model.TaskListener; import hudson.model.listeners.SCMListener; import hudson.plugins.git.GitSCM; import hudson.plugins.git.UserRemoteConfig; @@ -25,7 +29,9 @@ import static hudson.plugins.gradle.injection.MavenInjectionAware.JENKINSGRADLEPLUGIN_MAVEN_PLUGIN_CONFIG_EXT_CLASSPATH; import static hudson.plugins.gradle.injection.MavenInjectionAware.MAVEN_OPTS_HANDLER; import static hudson.plugins.gradle.injection.MavenOptsHandler.MAVEN_OPTS; +import static hudson.plugins.gradle.injection.npm.NpmBuildScanInjection.DEVELOCITY_INTERNAL_DISABLE_AGENT; +@SuppressWarnings("unused") @Extension public class GitScmListener extends SCMListener { @@ -33,12 +39,12 @@ public class GitScmListener extends SCMListener { @Override public void onCheckout( - Run build, - SCM scm, - FilePath workspace, - TaskListener listener, - @CheckForNull File changelogFile, - @CheckForNull SCMRevisionState pollingBaseline + Run build, + SCM scm, + FilePath workspace, + TaskListener listener, + @CheckForNull File changelogFile, + @CheckForNull SCMRevisionState pollingBaseline ) { try { InjectionConfig config = InjectionConfig.get(); @@ -55,16 +61,20 @@ public void onCheckout( // Check .mvn/extensions.xml for already applied Develocity extension for maven injection only disableMavenAutoInjectionIfAlreadyApplied(build, workspace, config, listener); + + // TODO: Figure out how to detect if npm injection is already configured for the project. + // If the project already has npm Develocity agent configured, + // we should set DEVELOCITY_URL depending on the config.isEnforceUrl() flag. } catch (Exception e) { LOGGER.error("Error occurred when processing onCheckout notification", e); } } private static void disableAutoInjection( - Run build, - FilePath workspace, - InjectionConfig config, - TaskListener listener + Run build, + FilePath workspace, + InjectionConfig config, + TaskListener listener ) throws Exception { Computer computer = workspace.toComputer(); if (computer == null) { @@ -73,11 +83,11 @@ private static void disableAutoInjection( EnvVars envVars = computer.buildEnvironment(listener); - if (shouldDisableGradleInjection(config)) { + if (InjectionStatus.GRADLE.isEnabled(config)) { build.addAction(GradleInjectionDisabledAction.INSTANCE); } - if (shouldDisableMavenInjection(config)) { + if (InjectionStatus.MAVEN.isEnabled(config)) { String currentMavenOpts = envVars.get(MavenOptsHandler.MAVEN_OPTS); if (currentMavenOpts != null) { String mavenOpts = Strings.nullToEmpty(MAVEN_OPTS_HANDLER.removeIfNeeded(currentMavenOpts)); @@ -85,13 +95,17 @@ private static void disableAutoInjection( build.addAction(new MavenInjectionDisabledAction(mavenOpts)); } } + + if (InjectionStatus.NPM.isEnabled(config)) { + build.addAction(NpmInjectionDisabledAction.INSTANCE); + } } private static void disableMavenAutoInjectionIfAlreadyApplied( - Run build, - FilePath workspace, - InjectionConfig config, - TaskListener listener + Run build, + FilePath workspace, + InjectionConfig config, + TaskListener listener ) throws Exception { Computer computer = workspace.toComputer(); if (computer == null) { @@ -104,10 +118,9 @@ private static void disableMavenAutoInjectionIfAlreadyApplied( if (currentMavenOpts != null) { Set knownExtensions = detect(config, workspace); if (!knownExtensions.isEmpty()) { - build.addAction( - new MavenInjectionDisabledAction( - new MavenOptsDevelocityFilter(knownExtensions, isUnix(computer)) - .filter(currentMavenOpts, config.isEnforceUrl()))); + MavenOptsDevelocityFilter mavenOptsFilter = new MavenOptsDevelocityFilter(knownExtensions, isUnix(computer)); + String filteredMavenOpts = mavenOptsFilter.filter(currentMavenOpts, config.isEnforceUrl()); + build.addAction(new MavenInjectionDisabledAction(filteredMavenOpts)); } } } @@ -130,8 +143,10 @@ private static boolean isInjectionEnabledForRepository(InjectionConfig config, S return true; } switch (config.matchesRepositoryFilter(url)) { - case EXCLUDED: return false; - case INCLUDED: return true; + case EXCLUDED: + return false; + case INCLUDED: + return true; } } } @@ -139,12 +154,31 @@ private static boolean isInjectionEnabledForRepository(InjectionConfig config, S return false; } - private static boolean shouldDisableGradleInjection(InjectionConfig config) { - return InjectionUtil.isValid(InjectionConfig.checkRequiredVersion(config.getGradlePluginVersion())); - } + private enum InjectionStatus { + GRADLE { + @Override + String getAgentVersion(InjectionConfig config) { + return config.getGradlePluginVersion(); + } + }, + MAVEN { + @Override + String getAgentVersion(InjectionConfig config) { + return config.getMavenExtensionVersion(); + } + }, + NPM { + @Override + String getAgentVersion(InjectionConfig config) { + return config.getNpmAgentVersion(); + } + }; - private static boolean shouldDisableMavenInjection(InjectionConfig config) { - return InjectionUtil.isValid(InjectionConfig.checkRequiredVersion(config.getMavenExtensionVersion())); + public boolean isEnabled(InjectionConfig config) { + return InjectionUtil.isValid(InjectionConfig.checkRequiredVersion(getAgentVersion(config))); + } + + abstract String getAgentVersion(InjectionConfig config); } /** @@ -161,7 +195,6 @@ private GradleInjectionDisabledAction() { public void buildEnvironment(@Nonnull Run run, @Nonnull EnvVars envVars) { envVars.put(DEVELOCITY_INJECTION_ENABLED.getEnvVar(), "false"); } - } /** @@ -180,7 +213,18 @@ public void buildEnvironment(@Nonnull Run run, @Nonnull EnvVars envVars) { envVars.put(MAVEN_OPTS, mavenOpts); envVars.put(JENKINSGRADLEPLUGIN_MAVEN_PLUGIN_CONFIG_EXT_CLASSPATH, ""); } - } + public static final class NpmInjectionDisabledAction extends InvisibleAction implements EnvironmentContributingAction { + + public static final NpmInjectionDisabledAction INSTANCE = new NpmInjectionDisabledAction(); + + private NpmInjectionDisabledAction() { + } + + @Override + public void buildEnvironment(@Nonnull Run run, @Nonnull EnvVars envVars) { + envVars.put(DEVELOCITY_INTERNAL_DISABLE_AGENT, "true"); + } + } } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/GradleBuildScanInjection.java b/plugin/src/main/java/hudson/plugins/gradle/injection/GradleBuildScanInjection.java index 88957f3c..9445f0e9 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/GradleBuildScanInjection.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/GradleBuildScanInjection.java @@ -12,13 +12,16 @@ import hudson.model.Node; import hudson.remoting.VirtualChannel; +import javax.annotation.Nullable; import java.io.IOException; import java.util.List; import java.util.Objects; import java.util.logging.Level; import java.util.logging.Logger; -import static hudson.plugins.gradle.injection.CopyUtil.*; +import static hudson.plugins.gradle.injection.CopyUtil.copyResourceToNode; +import static hudson.plugins.gradle.injection.CopyUtil.unsafeResourceDigest; +import static hudson.plugins.gradle.injection.InjectionUtil.HOME; public class GradleBuildScanInjection implements GradleInjectionAware { @@ -26,7 +29,6 @@ public class GradleBuildScanInjection implements GradleInjectionAware { private static final String JENKINSGRADLEPLUGIN_BUILD_SCAN_OVERRIDE_GRADLE_HOME = "JENKINSGRADLEPLUGIN_BUILD_SCAN_OVERRIDE_GRADLE_HOME"; private static final String JENKINSGRADLEPLUGIN_BUILD_SCAN_OVERRIDE_HOME = "JENKINSGRADLEPLUGIN_BUILD_SCAN_OVERRIDE_HOME"; - private static final String HOME = "HOME"; @VisibleForTesting static final String RESOURCE_INIT_SCRIPT_GRADLE = "init-script.gradle"; @@ -36,7 +38,7 @@ public class GradleBuildScanInjection implements GradleInjectionAware { private final Supplier initScriptDigest = Suppliers.memoize(() -> unsafeResourceDigest(RESOURCE_INIT_SCRIPT_GRADLE)); - public void inject(Node node, EnvVars envGlobal, EnvVars envComputer) { + public void inject(@Nullable Node node, EnvVars envGlobal, EnvVars envComputer) { if (node == null) { return; } @@ -64,14 +66,16 @@ private static String getInitScriptDirectory(EnvVars envGlobal, EnvVars envCompu if (gradleHomeOverride != null) { return filePath(gradleHomeOverride, INIT_DIR); - } else if (homeOverride != null) { - return filePath(homeOverride, GRADLE_DIR, INIT_DIR); - } else { - String home = EnvUtil.getEnv(envComputer, HOME); - Preconditions.checkState(home != null, "HOME is not set"); + } - return filePath(home, GRADLE_DIR, INIT_DIR); + if (homeOverride != null) { + return filePath(homeOverride, GRADLE_DIR, INIT_DIR); } + + String home = EnvUtil.getEnv(envComputer, HOME); + Preconditions.checkState(home != null, "HOME is not set"); + + return filePath(home, GRADLE_DIR, INIT_DIR); } private void inject(InjectionConfig config, Node node, String initScriptDirectory) { @@ -184,5 +188,4 @@ private static String getRepositoryUsername(String gradlePluginRepositoryCredent private static String filePath(String... parts) { return String.join("/", parts); } - } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/GradleInjectionAware.java b/plugin/src/main/java/hudson/plugins/gradle/injection/GradleInjectionAware.java index 2c8f1950..b0062eeb 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/GradleInjectionAware.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/GradleInjectionAware.java @@ -1,40 +1,25 @@ package hudson.plugins.gradle.injection; -import hudson.model.Node; -import hudson.plugins.gradle.util.CollectionUtil; +import javax.annotation.Nullable; +import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; +public interface GradleInjectionAware extends InjectionAware { -public interface GradleInjectionAware { - - default boolean isInjectionDisabledGlobally(InjectionConfig config) { - return config.isDisabled() || - InjectionUtil.isAnyInvalid( - InjectionConfig.checkRequiredUrl(config.getServer()), - InjectionConfig.checkRequiredVersion(config.getGradlePluginVersion()) - ); + @Nullable + @Override + default String getAgentVersion(InjectionConfig config) { + return config.getGradlePluginVersion(); } - default boolean isInjectionEnabledForNode(InjectionConfig config, Node node) { - if (isInjectionDisabledGlobally(config)) { - return false; - } - - Set disabledNodes = - CollectionUtil.safeStream(config.getGradleInjectionDisabledNodes()) - .map(NodeLabelItem::getLabel) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - - Set enabledNodes = - CollectionUtil.safeStream(config.getGradleInjectionEnabledNodes()) - .map(NodeLabelItem::getLabel) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - - return InjectionUtil.isInjectionEnabledForNode(node::getAssignedLabels, disabledNodes, enabledNodes); + @Nullable + @Override + default List getAgentInjectionDisabledNodes(InjectionConfig config) { + return config.getGradleInjectionDisabledNodes(); } + @Nullable + @Override + default List getAgentInjectionEnabledNodes(InjectionConfig config) { + return config.getGradleInjectionEnabledNodes(); + } } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/InitScriptVariables.java b/plugin/src/main/java/hudson/plugins/gradle/injection/InitScriptVariables.java index faa2b343..8bba649d 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/InitScriptVariables.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/InitScriptVariables.java @@ -1,6 +1,7 @@ package hudson.plugins.gradle.injection; -public enum InitScriptVariables { +public enum InitScriptVariables implements EnvVar { + DEVELOCITY_INJECTION_CUSTOM_VALUE("develocity-injection.custom-value"), DEVELOCITY_INIT_SCRIPT_NAME("develocity-injection.init-script-name"), DEVELOCITY_INJECTION_ENABLED("develocity-injection.enabled"), @@ -24,15 +25,8 @@ public enum InitScriptVariables { this.templateName = templateName; } - String getTemplateName() { - return templateName; - } - - String getEnvVar() { + @Override + public String getEnvVar() { return templateName.toUpperCase().replace('.', '_').replace('-', '_'); } - - String sysProp(String value) { - return "-D" + getTemplateName() + "=" + value; - } } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionAware.java b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionAware.java new file mode 100644 index 00000000..4cce2132 --- /dev/null +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionAware.java @@ -0,0 +1,59 @@ +package hudson.plugins.gradle.injection; + +import hudson.model.Node; +import hudson.plugins.gradle.util.CollectionUtil; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.stream.Collectors; + +public interface InjectionAware { + + @Nullable + String getAgentVersion(InjectionConfig config); + + @Nullable + List getAgentInjectionDisabledNodes(InjectionConfig config); + + @Nullable + List getAgentInjectionEnabledNodes(InjectionConfig config); + + default Optional ifInjectionEnabledGlobally(InjectionConfig config, Callable action) throws Exception { + if (isInjectionDisabledGlobally(config)) { + return Optional.empty(); + } + return Optional.ofNullable(action.call()); + } + + default boolean isInjectionDisabledGlobally(InjectionConfig config) { + return config.isDisabled() || + InjectionUtil.isAnyInvalid( + InjectionConfig.checkRequiredUrl(config.getServer()), + InjectionConfig.checkRequiredVersion(getAgentVersion(config)) + ); + } + + default boolean isInjectionEnabledForNode(InjectionConfig config, Node node) { + if (isInjectionDisabledGlobally(config)) { + return false; + } + + Set disabledNodes = + CollectionUtil.safeStream(getAgentInjectionDisabledNodes(config)) + .map(NodeLabelItem::getLabel) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + Set enabledNodes = + CollectionUtil.safeStream(getAgentInjectionEnabledNodes(config)) + .map(NodeLabelItem::getLabel) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + + return InjectionUtil.isInjectionEnabledForNode(node::getAssignedLabels, disabledNodes, enabledNodes); + } +} diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfig.java b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfig.java index f230f1b4..13f134e4 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfig.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfig.java @@ -36,7 +36,9 @@ import org.kohsuke.stapler.verb.POST; import javax.annotation.CheckForNull; +import javax.annotation.Nullable; import java.io.IOException; +import java.io.Serializable; import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -96,6 +98,11 @@ public class InjectionConfig extends GlobalConfiguration { private ImmutableList mavenInjectionDisabledNodes; private Boolean mavenCaptureGoalInputFiles; + private String npmAgentVersion; + private String npmAgentRegistryUrl; + private ImmutableList npmInjectionEnabledNodes; + private ImmutableList npmInjectionDisabledNodes; + private boolean enforceUrl; private boolean checkForBuildAgentErrors; @@ -234,8 +241,7 @@ public List getGradleInjectionEnabledNodes() { @DataBoundSetter public void setGradleInjectionEnabledNodes(List gradleInjectionEnabledNodes) { - this.gradleInjectionEnabledNodes = - gradleInjectionEnabledNodes == null ? null : ImmutableList.copyOf(gradleInjectionEnabledNodes); + this.gradleInjectionEnabledNodes = safeImmutableListCopy(gradleInjectionEnabledNodes); } @CheckForNull @@ -245,8 +251,7 @@ public List getGradleInjectionDisabledNodes() { @DataBoundSetter public void setGradleInjectionDisabledNodes(List gradleInjectionDisabledNodes) { - this.gradleInjectionDisabledNodes = - gradleInjectionDisabledNodes == null ? null : ImmutableList.copyOf(gradleInjectionDisabledNodes); + this.gradleInjectionDisabledNodes = safeImmutableListCopy(gradleInjectionDisabledNodes); } public Boolean isGradleCaptureTaskInputFiles() { @@ -323,8 +328,7 @@ public List getMavenInjectionEnabledNodes() { @DataBoundSetter public void setMavenInjectionEnabledNodes(List mavenInjectionEnabledNodes) { - this.mavenInjectionEnabledNodes = - mavenInjectionEnabledNodes == null ? null : ImmutableList.copyOf(mavenInjectionEnabledNodes); + this.mavenInjectionEnabledNodes = safeImmutableListCopy(mavenInjectionEnabledNodes); } @CheckForNull @@ -334,8 +338,7 @@ public List getMavenInjectionDisabledNodes() { @DataBoundSetter public void setMavenInjectionDisabledNodes(List mavenInjectionDisabledNodes) { - this.mavenInjectionDisabledNodes = - mavenInjectionDisabledNodes == null ? null : ImmutableList.copyOf(mavenInjectionDisabledNodes); + this.mavenInjectionDisabledNodes = safeImmutableListCopy(mavenInjectionDisabledNodes); } public Boolean isMavenCaptureGoalInputFiles() { @@ -347,6 +350,45 @@ public void setMavenCaptureGoalInputFiles(Boolean mavenCaptureGoalInputFiles) { this.mavenCaptureGoalInputFiles = mavenCaptureGoalInputFiles; } + public String getNpmAgentVersion() { + return npmAgentVersion; + } + + @DataBoundSetter + public void setNpmAgentVersion(String npmAgentVersion) { + this.npmAgentVersion = Util.fixEmptyAndTrim(npmAgentVersion); + } + + @CheckForNull + public String getNpmAgentRegistryUrl() { + return npmAgentRegistryUrl; + } + + @DataBoundSetter + public void setNpmAgentRegistryUrl(String npmAgentRegistryUrl) { + this.npmAgentRegistryUrl = Util.fixEmptyAndTrim(npmAgentRegistryUrl); + } + + @CheckForNull + public List getNpmInjectionEnabledNodes() { + return npmInjectionEnabledNodes; + } + + @DataBoundSetter + public void setNpmInjectionEnabledNodes(List npmInjectionEnabledNodes) { + this.npmInjectionEnabledNodes = safeImmutableListCopy(npmInjectionEnabledNodes); + } + + @CheckForNull + public List getNpmInjectionDisabledNodes() { + return npmInjectionDisabledNodes; + } + + @DataBoundSetter + public void setNpmInjectionDisabledNodes(List npmInjectionDisabledNodes) { + this.npmInjectionDisabledNodes = safeImmutableListCopy(npmInjectionDisabledNodes); + } + @DataBoundSetter public void setVcsRepositoryFilter(String vcsRepositoryFilter) { this.parsedVcsRepositoryFilter = VcsRepositoryFilter.of(vcsRepositoryFilter); @@ -419,6 +461,9 @@ private void clearRepeatableProperties() { setMavenInjectionEnabledNodes(null); setMavenInjectionDisabledNodes(null); + + setNpmInjectionEnabledNodes(null); + setNpmInjectionDisabledNodes(null); } @Restricted(NoExternalUse.class) @@ -558,6 +603,26 @@ public FormValidation doCheckCcudExtensionVersion(@QueryParameter String value) return checkVersion(value); } + @Restricted(NoExternalUse.class) + @POST + public FormValidation doCheckNpmAgentVersion(@QueryParameter String value) { + if (doesNotHaveAdministerPermission()) { + return FormValidation.error("Validating npm agent version requires 'Administer' permission"); + } + + return checkVersion(value); + } + + @Restricted(NoExternalUse.class) + @POST + public FormValidation doCheckNpmAgentRegistryUrl(@QueryParameter String value) { + if (doesNotHaveAdministerPermission()) { + return FormValidation.error("Validating npm agent registry URL requires 'Administer' permission"); + } + + return checkUrl(value); + } + @SuppressWarnings("called by Jelly") @POST public ListBoxModel doFillGradlePluginRepositoryCredentialIdItems(@AncestorInPath Item project) { @@ -602,7 +667,9 @@ private static ListBoxModel getAllCredentials(Item project) { private static FormValidation validateMavenCoordinates(String value) { String coord = Util.fixEmptyAndTrim(value); - return coord == null || MavenCoordinates.isValid(coord) ? FormValidation.ok() : FormValidation.error(Messages.InjectionConfig_InvalidMavenExtensionCustomCoordinates()); + return coord == null || MavenCoordinates.isValid(coord) + ? FormValidation.ok() + : FormValidation.error(Messages.InjectionConfig_InvalidMavenExtensionCustomCoordinates()); } public static FormValidation checkRequiredUrl(String value) { @@ -676,7 +743,7 @@ protected Object readResolve() throws IOException, FormException { StandardUsernamePasswordCredentials standardUsernameCredentials = new UsernamePasswordCredentialsImpl( CredentialsScope.GLOBAL, UUID.randomUUID().toString(), - "Migrated Gradle Plugin Respoitory credentials", + "Migrated Gradle Plugin Repository credentials", gradlePluginRepositoryUsername, gradlePluginRepositoryPassword.getPlainText() ); @@ -708,4 +775,7 @@ private static boolean doesNotHaveAdministerPermission() { return jenkins == null || !jenkins.hasPermission(Jenkins.ADMINISTER); } + private static ImmutableList safeImmutableListCopy(@Nullable List list) { + return list == null ? null : ImmutableList.copyOf(list); + } } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfigChangeListener.java b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfigChangeListener.java index 2860480d..514b88e1 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfigChangeListener.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionConfigChangeListener.java @@ -8,8 +8,11 @@ import hudson.model.Node; import hudson.model.Saveable; import hudson.model.listeners.SaveableListener; +import hudson.plugins.gradle.injection.npm.NpmAgentDownloadHandler; +import hudson.plugins.gradle.injection.npm.NpmBuildScanInjection; import jenkins.model.Jenkins; +import java.io.File; import java.util.Arrays; import java.util.Collection; import java.util.Map; @@ -28,14 +31,19 @@ public class InjectionConfigChangeListener extends SaveableListener { private final GradleBuildScanInjection gradleBuildScanInjection; private final MavenBuildScanInjection mavenBuildScanInjection; private final MavenExtensionDownloadHandler mavenExtensionDownloadHandler; + private final NpmBuildScanInjection npmBuildScanInjection; + private final NpmAgentDownloadHandler npmAgentDownloadHandler; private final Supplier globalEnvVarsSupplier; private final Supplier> computersSupplier; + @SuppressWarnings("unused") public InjectionConfigChangeListener() { this( new GradleBuildScanInjection(), new MavenBuildScanInjection(), new MavenExtensionDownloadHandler(), + new NpmBuildScanInjection(), + new NpmAgentDownloadHandler(), new JenkinsGlobalEnvVars(), new JenkinsComputers() ); @@ -46,12 +54,16 @@ public InjectionConfigChangeListener() { GradleBuildScanInjection gradleBuildScanInjection, MavenBuildScanInjection mavenBuildScanInjection, MavenExtensionDownloadHandler mavenExtensionDownloadHandler, + NpmBuildScanInjection npmBuildScanInjection, + NpmAgentDownloadHandler npmAgentDownloadHandler, Supplier globalEnvVarsSupplier, Supplier> computersSupplier ) { this.gradleBuildScanInjection = gradleBuildScanInjection; this.mavenBuildScanInjection = mavenBuildScanInjection; this.mavenExtensionDownloadHandler = mavenExtensionDownloadHandler; + this.npmBuildScanInjection = npmBuildScanInjection; + this.npmAgentDownloadHandler = npmAgentDownloadHandler; this.globalEnvVarsSupplier = globalEnvVarsSupplier; this.computersSupplier = computersSupplier; } @@ -67,9 +79,14 @@ public void onChange(Saveable saveable, XmlFile file) { } try { - Map extensionsDigest = mavenExtensionDownloadHandler.ensureExtensionsDownloaded( - () -> Jenkins.get().getRootDir(), injectionConfig - ); + Supplier root = () -> Jenkins.get().getRootDir(); + // This code runs on the controller, and we need to download artifacts + // before injecting them on agents. + Map extensionsDigest = mavenExtensionDownloadHandler.ensureExtensionsDownloaded(root, injectionConfig); + ArtifactDigest npmAgentDigest = + npmBuildScanInjection + .ifInjectionEnabledGlobally(injectionConfig, () -> npmAgentDownloadHandler.downloadNpmAgent(root, injectionConfig)) + .orElse(null); for (Computer computer : computersSupplier.get()) { if (computer.isOnline()) { @@ -78,6 +95,7 @@ public void onChange(Saveable saveable, XmlFile file) { gradleBuildScanInjection.inject(node, globalEnvVars, computerEnvVars); mavenBuildScanInjection.inject(node, extensionsDigest); + npmBuildScanInjection.inject(node, npmAgentDigest, computerEnvVars); } } } catch (Exception e) { diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionUtil.java b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionUtil.java index 3692cd66..7f4c889c 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionUtil.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/InjectionUtil.java @@ -2,6 +2,7 @@ import hudson.EnvVars; import hudson.PluginWrapper; +import hudson.Util; import hudson.model.labels.LabelAtom; import hudson.plugins.gradle.util.CollectionUtil; import hudson.util.FormValidation; @@ -9,6 +10,8 @@ import jenkins.model.Jenkins; import javax.annotation.Nullable; +import java.io.File; +import java.nio.file.Path; import java.util.Arrays; import java.util.Optional; import java.util.Set; @@ -19,13 +22,25 @@ public final class InjectionUtil { private static final String MAVEN_PLUGIN_SHORT_NAME = "maven-plugin"; + public static final String HOME = "HOME"; + public static final String DOWNLOAD_CACHE_DIR = "jenkins-gradle-plugin/cache"; public static final VersionNumber MINIMUM_SUPPORTED_MAVEN_PLUGIN_VERSION = new VersionNumber("3.20"); - public static final String JENKINSGRADLEPLUGIN_GLOBAL_AUTO_INJECTION_CHECK = "JENKINSGRADLEPLUGIN_GLOBAL_AUTO_INJECTION_CHECK"; private InjectionUtil() { } + public static String getNormalizedUrl(@Nullable String url, String defaultUrl) { + if (url == null || InjectionUtil.isInvalid(InjectionConfig.checkUrl(url))) { + return defaultUrl; + } + return Util.removeTrailingSlash(url); + } + + public static Path getDownloadCacheDir(Supplier rootDir) { + return rootDir.get().toPath().resolve(DOWNLOAD_CACHE_DIR); + } + public static boolean globalAutoInjectionCheckEnabled(EnvVars envVars) { return EnvUtil.getEnv(envVars, JENKINSGRADLEPLUGIN_GLOBAL_AUTO_INJECTION_CHECK) != null; } @@ -36,13 +51,13 @@ public static Optional mavenPluginVersionNumber() { public static Optional maybeGetPlugin(String pluginShortName) { return Optional.ofNullable(Jenkins.getInstanceOrNull()) - .map(Jenkins::getPluginManager) - .map(pm -> pm.getPlugin(pluginShortName)); + .map(Jenkins::getPluginManager) + .map(pm -> pm.getPlugin(pluginShortName)); } public static boolean isSupportedMavenPluginVersion(@Nullable VersionNumber mavenPluginVersion) { return mavenPluginVersion != null - && !mavenPluginVersion.isOlderThan(MINIMUM_SUPPORTED_MAVEN_PLUGIN_VERSION); + && !mavenPluginVersion.isOlderThan(MINIMUM_SUPPORTED_MAVEN_PLUGIN_VERSION); } public static boolean isInvalid(FormValidation validation) { @@ -61,9 +76,9 @@ public static boolean isInjectionEnabledForNode(Supplier> assigne Set disabledNodes, Set enabledNodes) { Set labels = - CollectionUtil.safeStream(assignedLabels.get()) - .map(LabelAtom::getName) - .collect(Collectors.toSet()); + CollectionUtil.safeStream(assignedLabels.get()) + .map(LabelAtom::getName) + .collect(Collectors.toSet()); return isNotDisabled(labels, disabledNodes) && isEnabled(labels, enabledNodes); } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenBuildScanInjection.java b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenBuildScanInjection.java index 98354129..1c77c76d 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenBuildScanInjection.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenBuildScanInjection.java @@ -4,7 +4,7 @@ import hudson.model.Node; import jenkins.model.Jenkins; -import java.io.File; +import javax.annotation.Nullable; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -34,7 +34,7 @@ public class MavenBuildScanInjection implements MavenInjectionAware { private final MavenExtensionsHandler extensionsHandler = new MavenExtensionsHandler(); - public void inject(Node node, Map extensionsDigest) { + public void inject(@Nullable Node node, Map extensionsDigest) { if (node == null) { return; } @@ -69,12 +69,12 @@ private void inject(InjectionConfig config, Node node, FilePath nodeRootPath, Ma String server = config.getServer(); - LOGGER.info("Injecting Maven extensions " + nodeRootPath); + LOGGER.log(Level.INFO, "Injecting Maven extensions {0}", nodeRootPath); List extensions = new ArrayList<>(); FilePath controllerRootPath = Jenkins.get().getRootPath(); - MavenExtension develocityMavenExtension = MavenExtension.getDevelocityMavenExtension(config.getMavenExtensionVersion()); + MavenExtension develocityMavenExtension = MavenExtension.forVersion(config.getMavenExtensionVersion()); extensions.add(extensionsHandler.copyExtensionToAgent(develocityMavenExtension, controllerRootPath, nodeRootPath, extensionsDigest.get(develocityMavenExtension))); if (InjectionUtil.isInvalid(InjectionConfig.checkRequiredVersion(config.getCcudExtensionVersion()))) { extensionsHandler.deleteExtensionFromAgent(MavenExtension.CCUD, nodeRootPath); @@ -127,5 +127,4 @@ private void cleanup(Node node, FilePath rootPath) { throw new IllegalStateException(e); } } - } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenCoordinates.java b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenCoordinates.java index 262ae308..f4119ac8 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenCoordinates.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenCoordinates.java @@ -62,10 +62,6 @@ String artifactId() { return artifactId; } - String version() { - return version; - } - @Override public String toString() { return String.format("%s:%s:%s", groupId, artifactId, version); diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtension.java b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtension.java index e357c836..429ee238 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtension.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtension.java @@ -1,7 +1,5 @@ package hudson.plugins.gradle.injection; -import hudson.Util; - import javax.annotation.Nullable; import java.net.URI; @@ -12,7 +10,6 @@ public enum MavenExtension { CCUD("common-custom-user-data-maven-extension", "ccud_metadata", new MavenCoordinates("com.gradle", "common-custom-user-data-maven-extension")), CONFIGURATION("configuration-maven-extension", "configuration_metadata", new MavenCoordinates("com.gradle", "configuration-maven-extension")); - private static final String EXTENSION_REPOSITORY_PATH = "/com/gradle/%s/%s/%s-%s.jar"; private static final String DEFAULT_REPOSITORY_URL = "https://repo1.maven.org/maven2"; private static final String JAR_EXTENSION = ".jar"; private static final String LAST_GRADLE_ENTERPRISE_VERSION = "1.20.1"; @@ -48,40 +45,18 @@ public String getDownloadMetadataFileName() { return downloadMetadataFileName; } - public static MavenExtension getDevelocityMavenExtension(String version) { + public static MavenExtension forVersion(String version) { return version.compareTo(LAST_GRADLE_ENTERPRISE_VERSION) > 0 ? DEVELOCITY : GRADLE_ENTERPRISE; } - public URI createDownloadUrl(String version, @Nullable String repositoryUrl) { - String extensionUrlTemplate = getNormalizedRepositoryUrl(repositoryUrl) + EXTENSION_REPOSITORY_PATH; - return URI.create(String.format(extensionUrlTemplate, this.getName(), version, this.getName(), version)); - } - - private String getNormalizedRepositoryUrl(@Nullable String repositoryUrl) { - if (repositoryUrl == null || InjectionUtil.isInvalid(InjectionConfig.checkUrl(repositoryUrl))) { - return DEFAULT_REPOSITORY_URL; - } - return Util.removeTrailingSlash(repositoryUrl); + public URI createDownloadUrl(String version) { + return createDownloadUrl(version, null); } - public static final class RepositoryCredentials { - - private final String username; - private final String password; - - public RepositoryCredentials(String username, String password) { - this.username = username; - this.password = password; - } - - public String username() { - return username; - } - - public String password() { - return password; - } - + public URI createDownloadUrl(String version, @Nullable String repositoryUrl) { + String normalizedRepositoryUrl = InjectionUtil.getNormalizedUrl(repositoryUrl, DEFAULT_REPOSITORY_URL); + return URI.create( + "%1$s/com/gradle/%2$s/%3$s/%2$s-%3$s.jar".formatted(normalizedRepositoryUrl, getName(), version) + ); } - } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java index 746aa006..303446b1 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionDownloadHandler.java @@ -3,8 +3,10 @@ import com.cloudbees.plugins.credentials.CredentialsProvider; import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import hudson.Util; -import hudson.plugins.gradle.injection.extension.ExtensionClient; +import hudson.plugins.gradle.injection.download.AgentDownloadClient; +import hudson.plugins.gradle.injection.download.RequestAuthenticator; +import javax.annotation.Nullable; import java.io.BufferedOutputStream; import java.io.File; import java.io.IOException; @@ -21,55 +23,49 @@ public class MavenExtensionDownloadHandler implements MavenInjectionAware { - public static final String DOWNLOAD_CACHE_DIR = "jenkins-gradle-plugin/cache"; - - private final ExtensionClient extensionClient = new ExtensionClient(); + private final AgentDownloadClient downloadClient = new AgentDownloadClient(); public Map ensureExtensionsDownloaded(Supplier root, InjectionConfig injectionConfig) throws IOException { - if (!isInjectionDisabledGlobally(injectionConfig)) { - Map extensionsDigest = new HashMap<>(); - Path cacheDir = root.get().toPath().resolve(DOWNLOAD_CACHE_DIR); + if (isInjectionDisabledGlobally(injectionConfig)) { + return Collections.emptyMap(); + } - MavenExtension develocityMavenExtension = MavenExtension.getDevelocityMavenExtension(injectionConfig.getMavenExtensionVersion()); + Map extensionsDigest = new HashMap<>(); + Path cacheDir = InjectionUtil.getDownloadCacheDir(root); - extensionsDigest.put(develocityMavenExtension, getOrDownloadExtensionDigest(injectionConfig, cacheDir, develocityMavenExtension)); - if (InjectionUtil.isValid(InjectionConfig.checkRequiredVersion(injectionConfig.getCcudExtensionVersion()))) { - extensionsDigest.put(MavenExtension.CCUD, getOrDownloadExtensionDigest(injectionConfig, cacheDir, MavenExtension.CCUD)); - } + MavenExtension develocityMavenExtension = MavenExtension.forVersion(injectionConfig.getMavenExtensionVersion()); - return extensionsDigest; + extensionsDigest.put(develocityMavenExtension, getOrDownloadExtensionDigest(injectionConfig, cacheDir, develocityMavenExtension)); + if (InjectionUtil.isValid(InjectionConfig.checkRequiredVersion(injectionConfig.getCcudExtensionVersion()))) { + extensionsDigest.put(MavenExtension.CCUD, getOrDownloadExtensionDigest(injectionConfig, cacheDir, MavenExtension.CCUD)); } - return Collections.emptyMap(); + return extensionsDigest; } - public Map getExtensionDigests(Supplier rootDir, InjectionConfig injectionConfig) throws IOException { - if (!isInjectionDisabledGlobally(injectionConfig)) { - Map extensionDigests = new HashMap<>(); - Path cacheDir = rootDir.get().toPath().resolve(DOWNLOAD_CACHE_DIR); + public Map getExtensionDigests(Supplier root, InjectionConfig injectionConfig) throws IOException { + if (isInjectionDisabledGlobally(injectionConfig)) { + return Collections.emptyMap(); + } - MavenExtension develocityMavenExtension = MavenExtension.getDevelocityMavenExtension(injectionConfig.getMavenExtensionVersion()); + Map extensionDigests = new HashMap<>(); + Path cacheDir = InjectionUtil.getDownloadCacheDir(root); - getExtensionDigest(cacheDir, develocityMavenExtension).ifPresent(it -> extensionDigests.put(develocityMavenExtension, it)); - if (InjectionUtil.isValid(InjectionConfig.checkRequiredVersion(injectionConfig.getCcudExtensionVersion()))) { - getExtensionDigest(cacheDir, MavenExtension.CCUD).ifPresent(it -> extensionDigests.put(MavenExtension.CCUD, it)); - } + MavenExtension develocityMavenExtension = MavenExtension.forVersion(injectionConfig.getMavenExtensionVersion()); - return extensionDigests; + getExtensionDigest(cacheDir, develocityMavenExtension).ifPresent(it -> extensionDigests.put(develocityMavenExtension, it)); + if (InjectionUtil.isValid(InjectionConfig.checkRequiredVersion(injectionConfig.getCcudExtensionVersion()))) { + getExtensionDigest(cacheDir, MavenExtension.CCUD).ifPresent(it -> extensionDigests.put(MavenExtension.CCUD, it)); } - return Collections.emptyMap(); + return extensionDigests; } private static Optional getExtensionDigest(Path parent, MavenExtension extension) throws IOException { Path metadataFile = parent.resolve(extension.getDownloadMetadataFileName()); - if (Files.exists(metadataFile)) { - String[] metadata = Files.readString(metadataFile).split(","); - - return Optional.of(metadata[1]); - } - - return Optional.empty(); + return ArtifactMetadata.readFromFile(metadataFile) + .map(ArtifactMetadata::digest) + .map(ArtifactDigest::digest); } private String getOrDownloadExtensionDigest(InjectionConfig injectionConfig, Path parent, MavenExtension extension) throws IOException { @@ -78,19 +74,14 @@ private String getOrDownloadExtensionDigest(InjectionConfig injectionConfig, Pat ? injectionConfig.getCcudExtensionVersion() : injectionConfig.getMavenExtensionVersion(); - if (Files.exists(metadataFile)) { - String[] metadata = Files.readString(metadataFile).split(","); - String extensionVersion = metadata[0]; - String extensionDigest = metadata[1]; - - if (!extensionVersion.equals(version)) { - return downloadExtension(injectionConfig, parent, extension, metadataFile, version); - } else { - return extensionDigest; - } - } else { - return downloadExtension(injectionConfig, parent, extension, metadataFile, version); - } + ArtifactDigest cachedDigest = ArtifactMetadata.readFromFile(metadataFile) + .filter(m -> m.isForVersion(version)) + .map(ArtifactMetadata::digest) + .orElse(null); + + return cachedDigest != null + ? cachedDigest.digest() + : downloadExtension(injectionConfig, parent, extension, metadataFile, version); } private String downloadExtension( @@ -105,32 +96,31 @@ private String downloadExtension( Path jarFile = parent.resolve(extension.getEmbeddedJarName()); URI downloadUrl = extension.createDownloadUrl(version, injectionConfig.getMavenExtensionRepositoryUrl()); - MavenExtension.RepositoryCredentials repositoryCredentials - = getRepositoryCredentials(injectionConfig.getMavenExtensionRepositoryCredentialId()); + RequestAuthenticator authenticator = createRequestAuthenticator(injectionConfig.getMavenExtensionRepositoryCredentialId()); try (OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(jarFile))) { - extensionClient.downloadExtension(downloadUrl, repositoryCredentials, outputStream); + downloadClient.download(downloadUrl, authenticator, outputStream); } + // TODO: Consider downloading the checksum file from the repository and verifying the download against it String digest = Util.getDigestOf(jarFile.toFile()); - - Files.writeString(metadataFile, version + "," + digest); + new ArtifactMetadata(version, digest).writeToFile(metadataFile); return digest; } - private static MavenExtension.RepositoryCredentials getRepositoryCredentials(String repositoryCredentialId) { + private static RequestAuthenticator createRequestAuthenticator(@Nullable String repositoryCredentialId) { if (repositoryCredentialId == null) { - return null; + return RequestAuthenticator.NONE; } - List allCredentials - = CredentialsProvider.lookupCredentialsInItem(StandardUsernamePasswordCredentials.class, null, null); + List allCredentials = + CredentialsProvider.lookupCredentialsInItem(StandardUsernamePasswordCredentials.class, null, null); return allCredentials.stream() - .filter(it -> it.getId().equals(repositoryCredentialId)) + .filter(c -> c.getId().equals(repositoryCredentialId)) .findFirst() - .map(it -> new MavenExtension.RepositoryCredentials(it.getUsername(), it.getPassword().getPlainText())) - .orElse(null); + .map(c -> RequestAuthenticator.basic(c.getUsername(), c.getPassword().getPlainText())) + .orElse(RequestAuthenticator.NONE); } } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionsHandler.java b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionsHandler.java index 876c44a6..923fc93e 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionsHandler.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenExtensionsHandler.java @@ -5,7 +5,6 @@ import hudson.FilePath; import java.io.IOException; -import java.nio.file.Files; import java.util.Arrays; import java.util.Map; import java.util.Objects; diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenInjectionAware.java b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenInjectionAware.java index ac2ab45b..c2961569 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/MavenInjectionAware.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/MavenInjectionAware.java @@ -1,13 +1,9 @@ package hudson.plugins.gradle.injection; -import hudson.model.Node; -import hudson.plugins.gradle.util.CollectionUtil; +import javax.annotation.Nullable; +import java.util.List; -import java.util.Objects; -import java.util.Set; -import java.util.stream.Collectors; - -public interface MavenInjectionAware { +public interface MavenInjectionAware extends InjectionAware { String JENKINSGRADLEPLUGIN_MAVEN_PLUGIN_CONFIG_EXT_CLASSPATH = "JENKINSGRADLEPLUGIN_MAVEN_PLUGIN_CONFIG_EXT_CLASSPATH"; // Use different variables so Gradle and Maven injections can work independently on the same node @@ -37,32 +33,21 @@ public interface MavenInjectionAware { DEVELOCITY_CAPTURE_FILE_FINGERPRINTS_PROPERTY_KEY ); - default boolean isInjectionDisabledGlobally(InjectionConfig config) { - return config.isDisabled() || - InjectionUtil.isAnyInvalid( - InjectionConfig.checkRequiredUrl(config.getServer()), - InjectionConfig.checkRequiredVersion(config.getMavenExtensionVersion()) - ); + @Nullable + @Override + default String getAgentVersion(InjectionConfig config) { + return config.getMavenExtensionVersion(); } - default boolean isInjectionEnabledForNode(InjectionConfig config, Node node) { - if (isInjectionDisabledGlobally(config)) { - return false; - } - - Set disabledNodes = - CollectionUtil.safeStream(config.getMavenInjectionDisabledNodes()) - .map(NodeLabelItem::getLabel) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - - Set enabledNodes = - CollectionUtil.safeStream(config.getMavenInjectionEnabledNodes()) - .map(NodeLabelItem::getLabel) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - - return InjectionUtil.isInjectionEnabledForNode(node::getAssignedLabels, disabledNodes, enabledNodes); + @Nullable + @Override + default List getAgentInjectionDisabledNodes(InjectionConfig config) { + return config.getMavenInjectionDisabledNodes(); } + @Nullable + @Override + default List getAgentInjectionEnabledNodes(InjectionConfig config) { + return config.getMavenInjectionEnabledNodes(); + } } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/extension/ExtensionClient.java b/plugin/src/main/java/hudson/plugins/gradle/injection/download/AgentDownloadClient.java similarity index 51% rename from plugin/src/main/java/hudson/plugins/gradle/injection/extension/ExtensionClient.java rename to plugin/src/main/java/hudson/plugins/gradle/injection/download/AgentDownloadClient.java index 2dd64c0c..ba28301e 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/injection/extension/ExtensionClient.java +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/download/AgentDownloadClient.java @@ -1,44 +1,38 @@ -package hudson.plugins.gradle.injection.extension; +package hudson.plugins.gradle.injection.download; -import hudson.plugins.gradle.injection.MavenExtension; -import okhttp3.Credentials; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; -import javax.annotation.Nullable; import java.io.BufferedInputStream; import java.io.IOException; import java.io.OutputStream; import java.net.URI; import java.util.concurrent.TimeUnit; -public final class ExtensionClient { +public final class AgentDownloadClient { private final OkHttpClient httpClient; - public ExtensionClient() { - this.httpClient = new OkHttpClient().newBuilder() + public AgentDownloadClient() { + httpClient = new OkHttpClient().newBuilder() .readTimeout(60, TimeUnit.SECONDS) .build(); } - public void downloadExtension( - URI downloadUrl, - @Nullable MavenExtension.RepositoryCredentials repositoryCredentials, - OutputStream outputStream - ) throws IOException { + public void download(URI downloadUrl, OutputStream outputStream) throws IOException { + download(downloadUrl, RequestAuthenticator.NONE, outputStream); + } + + public void download(URI downloadUrl, RequestAuthenticator authenticator, OutputStream outputStream) throws IOException { Request.Builder requestBuilder = new Request.Builder().url(downloadUrl.toURL()); - if (repositoryCredentials != null) { - String basicCredentials = Credentials.basic(repositoryCredentials.username(), repositoryCredentials.password()); - requestBuilder.addHeader("Authorization", basicCredentials); - } + authenticator.authenticate(requestBuilder); try (Response response = httpClient.newCall(requestBuilder.build()).execute()) { ResponseBody responseBody = response.body(); if (!response.isSuccessful() || responseBody == null) { - throw new IOException("Could not download the extension from " + downloadUrl); + throw new IOException("Could not download the agent from " + downloadUrl); } try (BufferedInputStream bufferedInputStream = new BufferedInputStream(responseBody.byteStream())) { @@ -46,5 +40,4 @@ public void downloadExtension( } } } - } diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/download/RequestAuthenticator.java b/plugin/src/main/java/hudson/plugins/gradle/injection/download/RequestAuthenticator.java new file mode 100644 index 00000000..912cfc95 --- /dev/null +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/download/RequestAuthenticator.java @@ -0,0 +1,16 @@ +package hudson.plugins.gradle.injection.download; + +import okhttp3.Credentials; +import okhttp3.Request; + +public interface RequestAuthenticator { + + RequestAuthenticator NONE = request -> { + }; + + void authenticate(Request.Builder request); + + static RequestAuthenticator basic(String username, String password) { + return request -> request.addHeader("Authorization", Credentials.basic(username, password)); + } +} diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmAgentDownloadHandler.java b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmAgentDownloadHandler.java new file mode 100644 index 00000000..b5d9ae31 --- /dev/null +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmAgentDownloadHandler.java @@ -0,0 +1,74 @@ +package hudson.plugins.gradle.injection.npm; + +import hudson.Util; +import hudson.plugins.gradle.injection.ArtifactDigest; +import hudson.plugins.gradle.injection.ArtifactMetadata; +import hudson.plugins.gradle.injection.InjectionConfig; +import hudson.plugins.gradle.injection.InjectionUtil; +import hudson.plugins.gradle.injection.download.AgentDownloadClient; + +import javax.annotation.Nullable; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.OutputStream; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Supplier; + +public class NpmAgentDownloadHandler { + + private static final String FILENAME = "develocity-npm-agent"; + private static final String METADATA_FILENAME = FILENAME + ".meta"; + private static final String DEFAULT_REGISTRY_URL = "https://registry.npmjs.org"; + + public static final String AGENT_FILENAME = FILENAME + ".tgz"; + + private final AgentDownloadClient downloadClient = new AgentDownloadClient(); + + public ArtifactDigest downloadNpmAgent(Supplier root, InjectionConfig injectionConfig) throws IOException { + String npmAgentVersion = injectionConfig.getNpmAgentVersion(); + + Path cacheDir = InjectionUtil.getDownloadCacheDir(root); + Path metadataFile = cacheDir.resolve(METADATA_FILENAME); + + ArtifactDigest cachedDigest = ArtifactMetadata.readFromFile(metadataFile) + .filter(m -> m.isForVersion(npmAgentVersion)) + .map(ArtifactMetadata::digest) + .orElse(null); + + if (cachedDigest != null) { + return cachedDigest; + } + + // Download the NPM agent + Files.createDirectories(cacheDir); + Path agentFile = cacheDir.resolve(AGENT_FILENAME); + + URI downloadUrl = createDownloadUrl(npmAgentVersion, injectionConfig.getNpmAgentRegistryUrl()); + try (OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(agentFile))) { + downloadClient.download(downloadUrl, outputStream); + } + + // TODO: Consider downloading the checksum file from the repository and verifying the download against it + ArtifactDigest digest = new ArtifactDigest(Util.getDigestOf(agentFile.toFile())); + new ArtifactMetadata(npmAgentVersion, digest).writeToFile(metadataFile); + + return digest; + } + + public ArtifactDigest getDownloadedNpmAgentDigest(Supplier root) throws IOException { + Path metadataFile = InjectionUtil.getDownloadCacheDir(root).resolve(METADATA_FILENAME); + return ArtifactMetadata.readFromFile(metadataFile) + .map(ArtifactMetadata::digest) + .orElse(null); + } + + private static URI createDownloadUrl(String npmAgentVersion, @Nullable String registryUrl) { + String normalizedRegistryUrl = InjectionUtil.getNormalizedUrl(registryUrl, DEFAULT_REGISTRY_URL); + return URI.create( + "%s/@gradle-tech/develocity-agent/-/develocity-agent-%s.tgz".formatted(normalizedRegistryUrl, npmAgentVersion) + ); + } +} diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmBuildScanInjection.java b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmBuildScanInjection.java new file mode 100644 index 00000000..0f87ccf5 --- /dev/null +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmBuildScanInjection.java @@ -0,0 +1,153 @@ +package hudson.plugins.gradle.injection.npm; + +import hudson.EnvVars; +import hudson.FilePath; +import hudson.model.Node; +import hudson.plugins.gradle.injection.ArtifactDigest; +import hudson.plugins.gradle.injection.EnvUtil; +import hudson.plugins.gradle.injection.EnvVar; +import hudson.plugins.gradle.injection.InjectionConfig; +import hudson.plugins.gradle.injection.InjectionUtil; +import hudson.remoting.RemoteInputStream; +import jenkins.model.Jenkins; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static hudson.plugins.gradle.injection.InjectionUtil.HOME; + +public class NpmBuildScanInjection implements NpmInjectionAware { + + private static final Logger LOGGER = Logger.getLogger(NpmBuildScanInjection.class.getName()); + + public static final String DEVELOCITY_INTERNAL_DISABLE_AGENT = "DEVELOCITY_INTERNAL_DISABLE_AGENT"; + + private static final String NODE_LIBRARIES_DIR = ".node_libraries"; + private static final String GRADLE_TECH_SCOPE = "@gradle-tech"; + + private enum NpmAgentConfig implements EnvVar { + DEVELOCITY_URL, + DEVELOCITY_ALLOW_UNTRUSTED_SERVER, + DEVELOCITY_INTERNAL_ENABLE_JEST_REPORTER_INJECTION, + DEVELOCITY_VALUE_CI_AUTO_INJECTION("DEVELOCITY_VALUE_CIAutoInjection"), + NODE_OPTIONS; + + @Nullable + private final String envVar; + + NpmAgentConfig() { + this(null); + } + + NpmAgentConfig(@Nullable String envVar) { + this.envVar = envVar; + } + + @Override + public String getEnvVar() { + return envVar != null ? envVar : name(); + } + } + + public void inject(@Nullable Node node, @Nullable ArtifactDigest npmAgentDigest, EnvVars envComputer) { + if (node == null) { + return; + } + + FilePath userHome = getUserHome(node, envComputer).orElse(null); + if (userHome == null) { + LOGGER.log(Level.WARNING, "Could not determine user home"); + return; + } + + InjectionConfig config = InjectionConfig.get(); + boolean enabled = isInjectionEnabledForNode(config, node); + try { + if (enabled) { + if (npmAgentDigest != null) { + inject(node, userHome, npmAgentDigest, config); + } else { + LOGGER.log(Level.WARNING, "npm agent digest is not present even though injection is enabled"); + } + } else { + cleanup(node, userHome); + } + } catch (IllegalStateException e) { + if (enabled) { + LOGGER.log(Level.WARNING, "Unexpected exception while injecting build scans for npm", e); + } + } + } + + private void inject(Node node, FilePath userHome, ArtifactDigest npmAgentDigest, InjectionConfig config) { + LOGGER.log(Level.INFO, "Injecting npm agent version {0}", config.getNpmAgentVersion()); + + try { + FilePath controllerRootPath = Jenkins.get().getRootPath(); + + installNpmAgent(controllerRootPath, userHome, npmAgentDigest, config); + injectEnvironmentVariables(node, config); + } catch (IOException | InterruptedException e) { + throw new IllegalStateException(e); + } + } + + private void installNpmAgent(FilePath controllerRootPath, FilePath userHome, ArtifactDigest npmAgentDigest, InjectionConfig config) throws IOException, InterruptedException { + FilePath scopePath = userHome.child(NODE_LIBRARIES_DIR).child(GRADLE_TECH_SCOPE); + FilePath agentPath = scopePath.child("develocity-agent"); + // ~/.node_libraries/@gradle-tech/develocity-agent/version.meta + FilePath versionMeta = agentPath.child("version.meta"); + if (!npmAgentChanged(versionMeta, npmAgentDigest)) { + return; + } + scopePath.deleteRecursive(); + // Unarchive the agent tarball from the controller to the agent node + FilePath agentFilePath = controllerRootPath.child(InjectionUtil.DOWNLOAD_CACHE_DIR).child(NpmAgentDownloadHandler.AGENT_FILENAME); + try (InputStream in = agentFilePath.read()) { + UnarchiveNpmAgent action = new UnarchiveNpmAgent(config.getNpmAgentVersion(), new RemoteInputStream(in, RemoteInputStream.Flag.GREEDY)); + agentPath.act(action); + } + versionMeta.write(npmAgentDigest.digest(), StandardCharsets.UTF_8.name()); + } + + private void injectEnvironmentVariables(Node node, InjectionConfig config) { + EnvUtil.setEnvVar(node, NpmAgentConfig.DEVELOCITY_URL, config.getServer()); + if (config.isAllowUntrusted()) { + EnvUtil.setEnvVar(node, NpmAgentConfig.DEVELOCITY_ALLOW_UNTRUSTED_SERVER, "true"); + } else { + EnvUtil.removeEnvVar(node, NpmAgentConfig.DEVELOCITY_ALLOW_UNTRUSTED_SERVER); + } + EnvUtil.setEnvVar(node, NpmAgentConfig.DEVELOCITY_INTERNAL_ENABLE_JEST_REPORTER_INJECTION, "true"); + EnvUtil.setEnvVar(node, NpmAgentConfig.DEVELOCITY_VALUE_CI_AUTO_INJECTION, "Jenkins"); + // TODO: Merge with existing NODE_OPTIONS if present + EnvUtil.setEnvVar(node, NpmAgentConfig.NODE_OPTIONS, "-r @gradle-tech/develocity-agent/preload"); + } + + private boolean npmAgentChanged(FilePath versionMeta, ArtifactDigest npmAgentDigest) throws IOException, InterruptedException { + if (!versionMeta.exists()) { + return true; + } + String currentDigest = versionMeta.readToString(); + return !npmAgentDigest.matches(currentDigest); + } + + private void cleanup(Node node, FilePath userHome) { + try { + FilePath scopePath = userHome.child(NODE_LIBRARIES_DIR).child(GRADLE_TECH_SCOPE); + scopePath.deleteRecursive(); + + EnvUtil.removeEnvVars(node, NpmAgentConfig.values()); + } catch (IOException | InterruptedException e) { + throw new IllegalStateException(e); + } + } + + private static Optional getUserHome(Node node, EnvVars envComputer) { + return Optional.ofNullable(EnvUtil.getEnv(envComputer, HOME)).map(node::createPath); + } +} diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmInjectionAware.java b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmInjectionAware.java new file mode 100644 index 00000000..0c5f4247 --- /dev/null +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/NpmInjectionAware.java @@ -0,0 +1,29 @@ +package hudson.plugins.gradle.injection.npm; + +import hudson.plugins.gradle.injection.InjectionAware; +import hudson.plugins.gradle.injection.InjectionConfig; +import hudson.plugins.gradle.injection.NodeLabelItem; + +import javax.annotation.Nullable; +import java.util.List; + +public interface NpmInjectionAware extends InjectionAware { + + @Nullable + @Override + default String getAgentVersion(InjectionConfig config) { + return config.getNpmAgentVersion(); + } + + @Nullable + @Override + default List getAgentInjectionDisabledNodes(InjectionConfig config) { + return config.getNpmInjectionDisabledNodes(); + } + + @Nullable + @Override + default List getAgentInjectionEnabledNodes(InjectionConfig config) { + return config.getNpmInjectionEnabledNodes(); + } +} diff --git a/plugin/src/main/java/hudson/plugins/gradle/injection/npm/UnarchiveNpmAgent.java b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/UnarchiveNpmAgent.java new file mode 100644 index 00000000..782aed54 --- /dev/null +++ b/plugin/src/main/java/hudson/plugins/gradle/injection/npm/UnarchiveNpmAgent.java @@ -0,0 +1,76 @@ +package hudson.plugins.gradle.injection.npm; + +import hudson.FilePath; +import hudson.remoting.VirtualChannel; +import hudson.util.IOUtils; +import jenkins.MasterToSlaveFileCallable; +import org.apache.tools.tar.TarEntry; +import org.apache.tools.tar.TarInputStream; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.Serial; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.attribute.FileTime; + +/** + * @see hudson.FilePath#untarFrom(InputStream, FilePath.TarCompression) + */ +public class UnarchiveNpmAgent extends MasterToSlaveFileCallable { + + @Serial + private static final long serialVersionUID = 1L; + + private static final String PACKAGE_ENTRY_PREFIX = "package/"; + + private final String version; + private final InputStream in; + + public UnarchiveNpmAgent(String version, InputStream in) { + this.version = version; + this.in = in; + } + + @Override + public Void invoke(File dir, VirtualChannel channel) throws IOException { + try (TarInputStream tar = new TarInputStream(FilePath.TarCompression.GZIP.extract(in), StandardCharsets.UTF_8.name())) { + TarEntry entry; + while ((entry = tar.getNextEntry()) != null) { + if (entry.isFile()) { + File file = new File(dir, normalizeEntryName(entry)); + File parent = file.getParentFile(); + if (parent != null) { + IOUtils.mkdirs(parent); + } + IOUtils.copy(tar, file); + + Files.setLastModifiedTime(file.toPath(), FileTime.from(entry.getModTime().toInstant())); + int mode = entry.getMode() & 0777; + if (mode != 0) { + new FilePath(file).chmod(mode); // noop on Windows + } + } + } + } catch (IOException e) { + throw wrapIntoIOException(e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw wrapIntoIOException(e); + } + return null; + } + + private IOException wrapIntoIOException(Throwable cause) { + return new IOException("Failed to extract the Develocity npm agent " + version, cause); + } + + private static String normalizeEntryName(TarEntry entry) throws IOException { + String entryName = entry.getName(); + if (!entryName.startsWith(PACKAGE_ENTRY_PREFIX)) { + throw new IOException("Unexpected entry in the agent tarball: " + entryName); + } + return entryName.substring(PACKAGE_ENTRY_PREFIX.length()); + } +} diff --git a/plugin/src/main/java/hudson/plugins/gradle/util/CollectionUtil.java b/plugin/src/main/java/hudson/plugins/gradle/util/CollectionUtil.java index 0ca2ce1d..311e5f92 100644 --- a/plugin/src/main/java/hudson/plugins/gradle/util/CollectionUtil.java +++ b/plugin/src/main/java/hudson/plugins/gradle/util/CollectionUtil.java @@ -13,7 +13,7 @@ public final class CollectionUtil { private CollectionUtil() { } - public static Stream safeStream(Collection col) { + public static Stream safeStream(@Nullable Collection col) { if (col == null) { return Stream.empty(); } diff --git a/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/config.jelly b/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/config.jelly index 1f01d897..4eb919bb 100644 --- a/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/config.jelly +++ b/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/config.jelly @@ -147,6 +147,35 @@ + + + + + + + + + + +
+ +
+
+
+
+ + + +
+ +
+
+
+
+
+ diff --git a/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/help-npmAgentRegistryUrl.html b/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/help-npmAgentRegistryUrl.html new file mode 100644 index 00000000..2813d1bd --- /dev/null +++ b/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/help-npmAgentRegistryUrl.html @@ -0,0 +1,4 @@ +
+ The URL of the registry to use when resolving the Develocity npm agent. + Defaults to https://registry.npmjs.org. +
diff --git a/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/help-npmAgentVersion.html b/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/help-npmAgentVersion.html new file mode 100644 index 00000000..784d6e83 --- /dev/null +++ b/plugin/src/main/resources/hudson/plugins/gradle/injection/InjectionConfig/help-npmAgentVersion.html @@ -0,0 +1,3 @@ +
+ The version of the Develocity npm agent to apply to npm builds. +
diff --git a/plugin/src/main/webapp/help-npmInjectionDisabledNodes.html b/plugin/src/main/webapp/help-npmInjectionDisabledNodes.html new file mode 100644 index 00000000..10986deb --- /dev/null +++ b/plugin/src/main/webapp/help-npmInjectionDisabledNodes.html @@ -0,0 +1,5 @@ +
+ The list of node labels where the Develocity npm agent auto-injection should be disabled. + By default, all nodes are enabled. + Disabled nodes have precedence over enabled nodes. +
diff --git a/plugin/src/main/webapp/help-npmInjectionEnabledNodes.html b/plugin/src/main/webapp/help-npmInjectionEnabledNodes.html new file mode 100644 index 00000000..36bf5bd9 --- /dev/null +++ b/plugin/src/main/webapp/help-npmInjectionEnabledNodes.html @@ -0,0 +1,5 @@ +
+ The list of node labels where the Develocity npm agent auto-injection should be enabled. + By default, all nodes are enabled. + Disabled nodes have precedence over enabled nodes. +
diff --git a/plugin/src/test/groovy/hudson/plugins/gradle/enriched/ScanDetailServiceTest.groovy b/plugin/src/test/groovy/hudson/plugins/gradle/enriched/ScanDetailServiceTest.groovy index f3506c6e..8bcc33d3 100644 --- a/plugin/src/test/groovy/hudson/plugins/gradle/enriched/ScanDetailServiceTest.groovy +++ b/plugin/src/test/groovy/hudson/plugins/gradle/enriched/ScanDetailServiceTest.groovy @@ -55,7 +55,7 @@ class ScanDetailServiceTest extends Specification { def httpClient = Stub(CloseableHttpClient) def response = Stub(CloseableHttpResponse) response.getStatusLine() >> new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.SC_FORBIDDEN, "") - httpClientFactory.buildHttpClient(_,_,_) >> httpClient + httpClientFactory.buildHttpClient(_, _, _) >> httpClient httpClient.execute(_) >> response when: @@ -66,7 +66,7 @@ class ScanDetailServiceTest extends Specification { } @Unroll - def 'Get scan detail with HTTP error on second request'(String buildToolType) { + def 'Get scan detail with HTTP error on second request'(String buildTool) { given: def scanDetailService = new ScanDetailService(getTestConfig()) def httpClientFactory = Stub(HttpClientFactory) @@ -77,13 +77,13 @@ class ScanDetailServiceTest extends Specification { response1.getEntity() >> new StringEntity( """ { - "buildToolType": "${buildToolType}", + "buildToolType": "${buildTool}", "buildToolVersion": "7.5.1" } """.stripIndent()) def response2 = Stub(CloseableHttpResponse) response2.getStatusLine() >> new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.SC_INTERNAL_SERVER_ERROR, "") - httpClientFactory.buildHttpClient(_,_,_) >> httpClient + httpClientFactory.buildHttpClient(_, _, _) >> httpClient httpClient.execute(_) >>> [response1, response2] when: @@ -93,7 +93,7 @@ class ScanDetailServiceTest extends Specification { scanDetail == Optional.empty() where: - buildToolType << ["gradle", "maven"] + buildTool << ["gradle", "maven", "npm"] } @Unroll @@ -108,14 +108,14 @@ class ScanDetailServiceTest extends Specification { response1.getEntity() >> new StringEntity( """ { - "buildToolType": "${buildToolType}", + "buildToolType": "${buildTool}", "buildToolVersion": "7.5.1" } """.stripIndent()) def response2 = Stub(CloseableHttpResponse) response2.getStatusLine() >> new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "") response2.getEntity() >> new StringEntity("{This is not valid JSON}") - httpClientFactory.buildHttpClient(_,_,_) >> httpClient + httpClientFactory.buildHttpClient(_, _, _) >> httpClient httpClient.execute(_) >>> [response1, response2] when: @@ -125,7 +125,7 @@ class ScanDetailServiceTest extends Specification { scanDetail == Optional.empty() where: - buildToolType << ["gradle", "maven"] + buildTool << ["gradle", "maven", "npm"] } @Unroll @@ -140,37 +140,39 @@ class ScanDetailServiceTest extends Specification { response1.getEntity() >> new StringEntity( """ { - "buildToolType": "${buildToolType}", + "buildToolType": "${buildTool}", "buildToolVersion": "7.5.1" } """.stripIndent()) def response2 = Stub(CloseableHttpResponse) response2.getStatusLine() >> new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "") response2.getEntity() >> new StringEntity(httpResponseBody) - httpClientFactory.buildHttpClient(_,_,_) >> httpClient + httpClientFactory.buildHttpClient(_, _, _) >> httpClient httpClient.execute(_) >>> [response1, response2] - httpClientFactory.buildHttpClient(_,_,_) >> httpClient + httpClientFactory.buildHttpClient(_, _, _) >> httpClient when: def scanDetailResult = scanDetailService.getScanDetail("https://foo.bar/s/scanId") then: - def scanDetail = scanDetailResult.get() - scanDetail.url == "https://foo.bar/s/scanId" - scanDetail.buildToolType.toString() == buildToolType.toUpperCase() - scanDetail.buildToolVersion == "7.5.1" - scanDetail.projectName == "project" - scanDetail.tasks == [ "clean", "build" ] - !scanDetail.hasFailed + with(scanDetailResult.get()) { + url == "https://foo.bar/s/scanId" + buildToolType.toString() == buildTool.toUpperCase() + buildToolVersion == "7.5.1" + projectName == "project" + tasks == expectedTasks + !hasFailed + } where: - buildToolType | httpResponseBody - "gradle" | '{"foo":"bar","rootProjectName":"project","requestedTasks":["clean","build"],"hasFailed":false}' - "maven" | '{"foo":"bar","topLevelProjectName":"project","requestedGoals":["clean","build"],"hasFailed":false}' + buildTool | httpResponseBody | expectedTasks + "gradle" | '{"foo":"bar","rootProjectName":"project","requestedTasks":["clean","build"],"hasFailed":false}' | ["clean", "build"] + "maven" | '{"foo":"bar","topLevelProjectName":"project","requestedGoals":["clean","build"],"hasFailed":false}' | ["clean", "build"] + "npm" | '{"foo":"bar","packageName":"project","command":{"command":"install","arguments":["arg1", "arg2"]},"hasFailed":false}' | ["install"] } @Unroll - def 'Get scan detail'(String buildToolType, String httpResponseBody) { + def 'Get scan detail'(String buildTool, String httpResponseBody) { given: def scanDetailService = new ScanDetailService(getTestConfig()) def httpClientFactory = Stub(HttpClientFactory) @@ -181,7 +183,7 @@ class ScanDetailServiceTest extends Specification { response1.getEntity() >> new StringEntity( """ { - "buildToolType": "${buildToolType}", + "buildToolType": "${buildTool}", "buildToolVersion": "7.5.1" } """.stripIndent()) @@ -189,24 +191,25 @@ class ScanDetailServiceTest extends Specification { response2.getStatusLine() >> new BasicStatusLine(HttpVersion.HTTP_1_1, HttpStatus.SC_OK, "") response2.getEntity() >> new StringEntity(httpResponseBody) httpClient.execute(_) >>> [response1, response2] - httpClientFactory.buildHttpClient(_,_,_) >> httpClient + httpClientFactory.buildHttpClient(_, _, _) >> httpClient when: def scanDetailResult = scanDetailService.getScanDetail("https://foo.bar/s/scanId") then: - def scanDetail = scanDetailResult.get() - scanDetail.url == "https://foo.bar/s/scanId" - scanDetail.buildToolType.toString() == buildToolType.toUpperCase() - scanDetail.buildToolVersion == "7.5.1" - scanDetail.projectName == "project" - scanDetail.tasks == [ "clean", "build" ] - !scanDetail.hasFailed + with(scanDetailResult.get()) { + url == "https://foo.bar/s/scanId" + buildToolType.toString() == buildTool.toUpperCase() + buildToolVersion == "7.5.1" + projectName == "project" + tasks == expectedTasks + !hasFailed + } where: - buildToolType | httpResponseBody - "gradle" | '{"foo":"bar","rootProjectName":"project","requestedTasks":["clean","build"],"hasFailed":false}' - "maven" | '{"topLevelProjectName":"project","requestedGoals":["clean","build"],"hasFailed":false}' + buildTool | httpResponseBody | expectedTasks + "gradle" | '{"rootProjectName":"project","requestedTasks":["clean","build"],"hasFailed":false}' | ["clean", "build"] + "maven" | '{"topLevelProjectName":"project","requestedGoals":["clean","build"],"hasFailed":false}' | ["clean", "build"] + "npm" | '{"packageName":"project","command":{"command":"install","arguments":["arg1", "arg2"]},"hasFailed":false}' | ["install"] } - } diff --git a/plugin/src/test/groovy/hudson/plugins/gradle/injection/BuildScanInjectionMavenCrossVersionIntegrationTest.groovy b/plugin/src/test/groovy/hudson/plugins/gradle/injection/BuildScanInjectionMavenCrossVersionIntegrationTest.groovy index 822a7c38..b9b45392 100644 --- a/plugin/src/test/groovy/hudson/plugins/gradle/injection/BuildScanInjectionMavenCrossVersionIntegrationTest.groovy +++ b/plugin/src/test/groovy/hudson/plugins/gradle/injection/BuildScanInjectionMavenCrossVersionIntegrationTest.groovy @@ -5,6 +5,7 @@ import hudson.plugins.gradle.BuildScanAction import hudson.plugins.gradle.BuildScanBuildWrapper import hudson.tasks.Maven import org.jvnet.hudson.test.CreateFileBuilder +import org.jvnet.hudson.test.recipes.WithTimeout import spock.lang.Unroll @Unroll @@ -12,6 +13,7 @@ class BuildScanInjectionMavenCrossVersionIntegrationTest extends BaseMavenIntegr private static final String MINIMUM_SUPPORTED_MAVEN_VERSION = '3.3.1' + @WithTimeout(300) // 5 minutes def 'build scan is discovered from Maven build - #mavenVersion'(String mavenVersion) { given: mavenInstallationRule.mavenVersion = mavenVersion diff --git a/plugin/src/test/groovy/hudson/plugins/gradle/injection/DevelocityComputerListenerTest.groovy b/plugin/src/test/groovy/hudson/plugins/gradle/injection/DevelocityComputerListenerTest.groovy index 8ec54419..252cd698 100644 --- a/plugin/src/test/groovy/hudson/plugins/gradle/injection/DevelocityComputerListenerTest.groovy +++ b/plugin/src/test/groovy/hudson/plugins/gradle/injection/DevelocityComputerListenerTest.groovy @@ -4,12 +4,16 @@ import hudson.EnvVars import hudson.model.Computer import hudson.model.Node import hudson.model.TaskListener +import hudson.plugins.gradle.injection.npm.NpmAgentDownloadHandler +import hudson.plugins.gradle.injection.npm.NpmBuildScanInjection import org.junit.Rule import org.junit.rules.TemporaryFolder import spock.lang.Specification import spock.lang.Subject import spock.lang.Unroll +import java.util.concurrent.Callable + import static hudson.plugins.gradle.injection.InjectionUtil.JENKINSGRADLEPLUGIN_GLOBAL_AUTO_INJECTION_CHECK class DevelocityComputerListenerTest extends Specification { @@ -19,11 +23,20 @@ class DevelocityComputerListenerTest extends Specification { def gradleBuildScanInjection = Mock(GradleBuildScanInjection) def mavenBuildScanInjection = Mock(MavenBuildScanInjection) def mavenExtensionDownloadHandler = Mock(MavenExtensionDownloadHandler) + def npmBuildScanInjection = Mock(NpmBuildScanInjection) + def npmAgentDownloadHandler = Mock(NpmAgentDownloadHandler) def injectionConfig = Mock(InjectionConfig) @Subject def gradleEnterpriseComputerListener = - new DevelocityComputerListener(gradleBuildScanInjection, mavenBuildScanInjection, mavenExtensionDownloadHandler, { injectionConfig }) + new DevelocityComputerListener( + gradleBuildScanInjection, + mavenBuildScanInjection, + mavenExtensionDownloadHandler, + npmBuildScanInjection, + npmAgentDownloadHandler, + { injectionConfig } + ) @Rule public TemporaryFolder tempFolder = new TemporaryFolder() @@ -38,18 +51,21 @@ class DevelocityComputerListenerTest extends Specification { injectionConfig.disabled >> !isGlobalInjectionEnabled def node = Mock(Node) - def computerEnvVars = new EnvVars([COMPUTER: "ture"]) + def computerEnvVars = new EnvVars([COMPUTER: "true"]) computer.getNode() >> node computer.getEnvironment() >> computerEnvVars def root = tempFolder.newFolder() - mavenExtensionDownloadHandler.ensureExtensionsDownloaded({ root }, injectionConfig) >> { } + mavenExtensionDownloadHandler.getExtensionDigests({ root }, injectionConfig) >> [:] + npmBuildScanInjection.ifInjectionEnabledGlobally(injectionConfig, _ as Callable) >> Optional.empty() when: gradleEnterpriseComputerListener.onOnline(computer, Mock(TaskListener)) then: (isInjectionExpected ? 1 : 0) * gradleBuildScanInjection.inject(node, globalEnvVars, computerEnvVars) + (isInjectionExpected ? 1 : 0) * mavenBuildScanInjection.inject(node, [:]) + (isInjectionExpected ? 1 : 0) * npmBuildScanInjection.inject(node, null, computerEnvVars) where: isGlobalAutoInjectionCheckEnabled | isGlobalInjectionEnabled || isInjectionExpected @@ -65,19 +81,23 @@ class DevelocityComputerListenerTest extends Specification { computer.name >> "testComputer" def node = Mock(Node) - def computerEnvVars = new EnvVars([COMPUTER: "ture"]) + def computerEnvVars = new EnvVars([COMPUTER: "true"]) computer.getNode() >> node computer.getEnvironment() >> computerEnvVars def root = tempFolder.newFolder() - mavenExtensionDownloadHandler.ensureExtensionsDownloaded({ root }, injectionConfig) >> { } + mavenExtensionDownloadHandler.getExtensionDigests({ root }, injectionConfig) >> [:] + npmBuildScanInjection.ifInjectionEnabledGlobally(injectionConfig, _ as Callable) >> Optional.empty() gradleBuildScanInjection.inject(node, globalEnvVars, computerEnvVars) >> { throw new ExpectedException() } - mavenBuildScanInjection.inject(node, [:]) when: gradleEnterpriseComputerListener.onOnline(computer, Mock(TaskListener)) then: noExceptionThrown() + + and: "other injections are not performed" + 0 * mavenBuildScanInjection.inject(node, [:]) + 0 * npmBuildScanInjection.inject(node, null, computerEnvVars) } } diff --git a/plugin/src/test/groovy/hudson/plugins/gradle/injection/InjectionConfigChangeListenerTest.groovy b/plugin/src/test/groovy/hudson/plugins/gradle/injection/InjectionConfigChangeListenerTest.groovy index 1d1e6151..9da8e9da 100644 --- a/plugin/src/test/groovy/hudson/plugins/gradle/injection/InjectionConfigChangeListenerTest.groovy +++ b/plugin/src/test/groovy/hudson/plugins/gradle/injection/InjectionConfigChangeListenerTest.groovy @@ -4,12 +4,16 @@ import hudson.EnvVars import hudson.XmlFile import hudson.model.Computer import hudson.model.Node +import hudson.plugins.gradle.injection.npm.NpmAgentDownloadHandler +import hudson.plugins.gradle.injection.npm.NpmBuildScanInjection import org.junit.Rule import org.junit.rules.TemporaryFolder import spock.lang.Specification import spock.lang.Subject import spock.lang.Unroll +import java.util.concurrent.Callable + import static hudson.plugins.gradle.injection.InjectionUtil.JENKINSGRADLEPLUGIN_GLOBAL_AUTO_INJECTION_CHECK class InjectionConfigChangeListenerTest extends Specification { @@ -21,6 +25,8 @@ class InjectionConfigChangeListenerTest extends Specification { def gradleBuildScanInjection = Mock(GradleBuildScanInjection) def mavenBuildScanInjection = Mock(MavenBuildScanInjection) def mavenExtensionDownloadHandler = Mock(MavenExtensionDownloadHandler) + def npmBuildScanInjection = Mock(NpmBuildScanInjection) + def npmAgentDownloadHandler = Mock(NpmAgentDownloadHandler) def injectionConfig = Mock(InjectionConfig) @Rule @@ -28,7 +34,15 @@ class InjectionConfigChangeListenerTest extends Specification { @Subject def injectionConfigChangeListener = - new InjectionConfigChangeListener(gradleBuildScanInjection, mavenBuildScanInjection, mavenExtensionDownloadHandler, { globalEnvVars }, { [computer] }) + new InjectionConfigChangeListener( + gradleBuildScanInjection, + mavenBuildScanInjection, + mavenExtensionDownloadHandler, + npmBuildScanInjection, + npmAgentDownloadHandler, + { globalEnvVars }, + { [computer] } + ) @Unroll def "performs injection when configuration changes (isGlobalAutoInjectionCheckEnabled=#isGlobalAutoInjectionCheckEnabled, isGlobalInjectionEnabled=#isGlobalInjectionEnabled, isComputerOffline=#isComputerOffline)"() { @@ -40,17 +54,20 @@ class InjectionConfigChangeListenerTest extends Specification { computer.offline >> isComputerOffline def node = Mock(Node) - def computerEnvVars = new EnvVars([COMPUTER: "ture"]) + def computerEnvVars = new EnvVars([COMPUTER: "true"]) computer.getNode() >> node computer.getEnvironment() >> computerEnvVars def root = tempFolder.newFolder() - mavenExtensionDownloadHandler.ensureExtensionsDownloaded({ root }, injectionConfig) >> {} + mavenExtensionDownloadHandler.ensureExtensionsDownloaded({ root }, injectionConfig) >> [:] + npmBuildScanInjection.ifInjectionEnabledGlobally(injectionConfig, _ as Callable) >> Optional.empty() when: injectionConfigChangeListener.onChange(injectionConfig, UNUSED_XML_FILE) then: (isInjectionExpected ? 1 : 0) * gradleBuildScanInjection.inject(node, globalEnvVars, computerEnvVars) + (isInjectionExpected ? 1 : 0) * mavenBuildScanInjection.inject(node, [:]) + (isInjectionExpected ? 1 : 0) * npmBuildScanInjection.inject(node, null, computerEnvVars) where: isGlobalAutoInjectionCheckEnabled | isGlobalInjectionEnabled | isComputerOffline || isInjectionExpected @@ -70,19 +87,23 @@ class InjectionConfigChangeListenerTest extends Specification { computer.name >> "testComputer" def node = Mock(Node) - def computerEnvVars = new EnvVars([COMPUTER: "ture"]) + def computerEnvVars = new EnvVars([COMPUTER: "true"]) computer.getNode() >> node computer.getEnvironment() >> computerEnvVars def root = tempFolder.newFolder() - mavenExtensionDownloadHandler.ensureExtensionsDownloaded({ root }, injectionConfig) >> {} + mavenExtensionDownloadHandler.ensureExtensionsDownloaded({ root }, injectionConfig) >> [:] + npmBuildScanInjection.ifInjectionEnabledGlobally(injectionConfig, _ as Callable) >> Optional.empty() gradleBuildScanInjection.inject(node, globalEnvVars, computerEnvVars) >> { throw new ExpectedException() } - mavenBuildScanInjection.inject(node, [:]) when: injectionConfigChangeListener.onChange(injectionConfig, UNUSED_XML_FILE) then: noExceptionThrown() + + and: "other injections are not performed" + 0 * mavenBuildScanInjection.inject(node, [:]) + 0 * npmBuildScanInjection.inject(node, null, computerEnvVars) } } diff --git a/plugin/src/test/groovy/hudson/plugins/gradle/injection/InjectionConfigWithCasCIntegrationTest.groovy b/plugin/src/test/groovy/hudson/plugins/gradle/injection/InjectionConfigWithCasCIntegrationTest.groovy index 3b946670..0a5b47b3 100644 --- a/plugin/src/test/groovy/hudson/plugins/gradle/injection/InjectionConfigWithCasCIntegrationTest.groovy +++ b/plugin/src/test/groovy/hudson/plugins/gradle/injection/InjectionConfigWithCasCIntegrationTest.groovy @@ -12,6 +12,7 @@ import spock.lang.Unroll @Unroll @Subject(InjectionConfig.class) class InjectionConfigWithCasCIntegrationTest extends AbstractIntegrationTest { + @Rule public final RuleChain rules = RuleChain.outerRule(noSpaceInTmpDirs).around(new JenkinsConfiguredWithCodeRule()) @@ -19,29 +20,33 @@ class InjectionConfigWithCasCIntegrationTest extends AbstractIntegrationTest { def 'current configuration is readable with JCasC'() { expect: with(InjectionConfig.get()) { - it.allowUntrusted == true - it.ccudExtensionCustomCoordinates == "mycustom-ccud:ext" - it.ccudExtensionVersion == "2.0.1" - it.ccudPluginVersion == "2.0.2" - it.checkForBuildAgentErrors == true - it.enabled == true - it.enforceUrl == true - it.gradleCaptureTaskInputFiles == true - it.gradleInjectionDisabledNodes*.label == ["non-gradle-node"] - it.gradleInjectionEnabledNodes*.label == ["gradle-node"] - it.gradlePluginRepositoryUrl == "https://plugins.gradle.org" - it.gradlePluginVersion == "3.18.1" - it.injectMavenExtension == true - it.injectCcudExtension == true - it.mavenCaptureGoalInputFiles == true - it.mavenExtensionCustomCoordinates == "mycustom:ext" - it.mavenExtensionRepositoryUrl == "https://repo1.maven.org/maven2" - it.mavenExtensionVersion == "2.1" - it.mavenInjectionDisabledNodes*.label == ["non-maven-node"] - it.mavenInjectionEnabledNodes*.label == ["maven-node"] - it.server == "http://localhost:5086" - it.shortLivedTokenExpiry == 24 - it.vcsRepositoryFilter == "+:myrepo" + allowUntrusted + ccudExtensionCustomCoordinates == "mycustom-ccud:ext" + ccudExtensionVersion == "2.0.1" + ccudPluginVersion == "2.0.2" + checkForBuildAgentErrors + enabled + enforceUrl + gradleCaptureTaskInputFiles + gradleInjectionDisabledNodes*.label == ["non-gradle-node"] + gradleInjectionEnabledNodes*.label == ["gradle-node"] + gradlePluginRepositoryUrl == "https://plugins.gradle.org" + gradlePluginVersion == "3.18.1" + injectMavenExtension + injectCcudExtension + mavenCaptureGoalInputFiles == true + mavenExtensionCustomCoordinates == "mycustom:ext" + mavenExtensionRepositoryUrl == "https://repo1.maven.org/maven2" + mavenExtensionVersion == "2.1" + mavenInjectionDisabledNodes*.label == ["non-maven-node"] + mavenInjectionEnabledNodes*.label == ["maven-node"] + npmAgentVersion == "3.0.0" + npmAgentRegistryUrl == "https://registry.npmjs.org" + npmInjectionDisabledNodes*.label == ["non-npm-node"] + npmInjectionEnabledNodes*.label == ["npm-node"] + server == "http://localhost:5086" + shortLivedTokenExpiry == 24 + vcsRepositoryFilter == "+:myrepo" } } } diff --git a/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionDownloadHandlerTest.groovy b/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionDownloadHandlerTest.groovy index 2912fd69..cc8b159e 100644 --- a/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionDownloadHandlerTest.groovy +++ b/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionDownloadHandlerTest.groovy @@ -36,16 +36,16 @@ class MavenExtensionDownloadHandlerTest extends Specification { originalDevelocityDigest != null && originalCcudDigest != null when: - def originalDevelocityLastModified = new File(controllerFolder, MavenExtensionDownloadHandler.DOWNLOAD_CACHE_DIR + "/" + MavenExtension.DEVELOCITY.getEmbeddedJarName()).lastModified() - def originalCcudLastModified = new File(controllerFolder, MavenExtensionDownloadHandler.DOWNLOAD_CACHE_DIR + "/" + MavenExtension.CCUD.getEmbeddedJarName()).lastModified() + def originalDevelocityLastModified = new File(controllerFolder, InjectionUtil.DOWNLOAD_CACHE_DIR + "/" + MavenExtension.DEVELOCITY.getEmbeddedJarName()).lastModified() + def originalCcudLastModified = new File(controllerFolder, InjectionUtil.DOWNLOAD_CACHE_DIR + "/" + MavenExtension.CCUD.getEmbeddedJarName()).lastModified() def sameExtensions = mavenExtensionDownloadHandler.ensureExtensionsDownloaded({ controllerFolder }, originalConfig) then: sameExtensions.size() == 2 - def sameDevelocityLastModified = new File(controllerFolder, MavenExtensionDownloadHandler.DOWNLOAD_CACHE_DIR + "/" + MavenExtension.DEVELOCITY.getEmbeddedJarName()).lastModified() - def sameCcudLastModified = new File(controllerFolder, MavenExtensionDownloadHandler.DOWNLOAD_CACHE_DIR + "/" + MavenExtension.CCUD.getEmbeddedJarName()).lastModified() + def sameDevelocityLastModified = new File(controllerFolder, InjectionUtil.DOWNLOAD_CACHE_DIR + "/" + MavenExtension.DEVELOCITY.getEmbeddedJarName()).lastModified() + def sameCcudLastModified = new File(controllerFolder, InjectionUtil.DOWNLOAD_CACHE_DIR + "/" + MavenExtension.CCUD.getEmbeddedJarName()).lastModified() def sameDevelocityDigest = sameExtensions.get(MavenExtension.DEVELOCITY) def sameCcudDigest = sameExtensions.get(MavenExtension.CCUD) diff --git a/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionsHandlerIntegrationTest.groovy b/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionsHandlerIntegrationTest.groovy index a4476d69..6438e692 100644 --- a/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionsHandlerIntegrationTest.groovy +++ b/plugin/src/test/groovy/hudson/plugins/gradle/injection/MavenExtensionsHandlerIntegrationTest.groovy @@ -1,7 +1,7 @@ package hudson.plugins.gradle.injection import hudson.FilePath -import hudson.plugins.gradle.injection.extension.ExtensionClient +import hudson.plugins.gradle.injection.download.AgentDownloadClient import org.junit.Rule import org.junit.rules.TemporaryFolder import spock.lang.Specification @@ -15,7 +15,7 @@ class MavenExtensionsHandlerIntegrationTest extends Specification { @Subject MavenExtensionsHandler mavenExtensionsHandler = new MavenExtensionsHandler() - ExtensionClient extensionClient = new ExtensionClient() + AgentDownloadClient downloadClient = new AgentDownloadClient() def "only copies configuration extension if it doesn't exist"() { given: @@ -46,16 +46,16 @@ class MavenExtensionsHandlerIntegrationTest extends Specification { def agentFolder = tempFolder.newFolder() def controllerRoot = new FilePath(controllerFolder) def agentRoot = new FilePath(agentFolder) - def cacheDirectory = controllerRoot.child(MavenExtensionDownloadHandler.DOWNLOAD_CACHE_DIR) + def cacheDirectory = controllerRoot.child(InjectionUtil.DOWNLOAD_CACHE_DIR) cacheDirectory.child(MavenExtension.DEVELOCITY.getEmbeddedJarName()).write() .withCloseable { - extensionClient.downloadExtension(MavenExtension.DEVELOCITY.createDownloadUrl("2.1", null), null, it) + downloadClient.download(MavenExtension.DEVELOCITY.createDownloadUrl("2.1"), it) } cacheDirectory.child(MavenExtension.CCUD.getEmbeddedJarName()).write() .withCloseable { - extensionClient.downloadExtension(MavenExtension.CCUD.createDownloadUrl("2.0.1", null), null, it) + downloadClient.download(MavenExtension.CCUD.createDownloadUrl("2.0.1"), it) } when: @@ -77,16 +77,16 @@ class MavenExtensionsHandlerIntegrationTest extends Specification { def agentFolder = tempFolder.newFolder() def controllerRoot = new FilePath(controllerFolder) def agentRoot = new FilePath(agentFolder) - def cacheDirectory = controllerRoot.child(MavenExtensionDownloadHandler.DOWNLOAD_CACHE_DIR) + def cacheDirectory = controllerRoot.child(InjectionUtil.DOWNLOAD_CACHE_DIR) cacheDirectory.child(MavenExtension.DEVELOCITY.getEmbeddedJarName()).write() .withCloseable { - extensionClient.downloadExtension(MavenExtension.DEVELOCITY.createDownloadUrl("2.1", null), null, it) + downloadClient.download(MavenExtension.DEVELOCITY.createDownloadUrl("2.1"), it) } cacheDirectory.child(MavenExtension.CCUD.getEmbeddedJarName()).write() .withCloseable { - extensionClient.downloadExtension(MavenExtension.CCUD.createDownloadUrl("2.0.1", null), null, it) + downloadClient.download(MavenExtension.CCUD.createDownloadUrl("2.0.1"), it) } when: diff --git a/plugin/src/test/resources/injection-config.yml b/plugin/src/test/resources/injection-config.yml index 8e51dc33..1ba3b06d 100644 --- a/plugin/src/test/resources/injection-config.yml +++ b/plugin/src/test/resources/injection-config.yml @@ -25,6 +25,12 @@ unclassified: - label: "non-maven-node" mavenInjectionEnabledNodes: - label: "maven-node" + npmAgentVersion: "3.0.0" + npmAgentRegistryUrl: "https://registry.npmjs.org" + npmInjectionDisabledNodes: + - label: "non-npm-node" + npmInjectionEnabledNodes: + - label: "npm-node" server: "http://localhost:5086" shortLivedTokenExpiry: 24 vcsRepositoryFilter: "+:myrepo"