diff --git a/README.md b/README.md index af8a0a7..c50b45f 100644 --- a/README.md +++ b/README.md @@ -237,7 +237,7 @@ To see more information on your steps, you can increase logging and re-run your ### Failed Installations -By default, Snyk Installations will download Snyk's binaries over the network from `static.snyk.io`. If this fails there +By default, Snyk Installations will download Snyk's binaries over the network from `downloads.snyk.io` and use `static.snyk.io` as a fallback. If this fails there may be a network or proxy issue. If you cannot fix the issue, you can use a [Manual Installation](#2-configure-a-snyk-installation) instead. --- diff --git a/src/main/java/io/snyk/jenkins/tools/SnykInstaller.java b/src/main/java/io/snyk/jenkins/tools/SnykInstaller.java index 7276de4..01aeab2 100644 --- a/src/main/java/io/snyk/jenkins/tools/SnykInstaller.java +++ b/src/main/java/io/snyk/jenkins/tools/SnykInstaller.java @@ -88,34 +88,40 @@ private boolean isUpToDate(FilePath expectedLocation) throws IOException, Interr } private FilePath downloadSnykBinaries(FilePath expected, Node node) { - try { - LOG.info("Installing Snyk '{}' on Build Node '{}'", version, node.getDisplayName()); + for (String snykUrlTemplate : DownloadService.SNYK_CLI_DOWNLOAD_URLS) { + try { + LOG.info("Installing Snyk '{}' on Build Node '{}'", version, node.getDisplayName()); - final VirtualChannel nodeChannel = node.getChannel(); - if (nodeChannel == null) { - throw new IOException(format("Build Node '%s' is offline.", node.getDisplayName())); - } + final VirtualChannel nodeChannel = node.getChannel(); + if (nodeChannel == null) { + throw new IOException(format("Build Node '%s' is offline.", node.getDisplayName())); + } - Platform platform = PlatformItem.convert(this.platform); - if (platform == null) { - LOG.info("Installer architecture is not configured or use AUTO mode"); - platform = nodeChannel.call(new GetPlatform()); - } - LOG.info("Configured installer architecture is {}", platform); + Platform platform = PlatformItem.convert(this.platform); + if (platform == null) { + LOG.info("Installer architecture is not configured or use AUTO mode"); + platform = nodeChannel.call(new GetPlatform()); + } + LOG.info("Configured installer architecture is {}", platform); - URL snykDownloadUrl = DownloadService.getDownloadUrlForSnyk(version, platform); - URL snykToHtmlDownloadUrl = DownloadService.getDownloadUrlForSnykToHtml("latest", platform); + URL snykDownloadUrl = DownloadService.constructDownloadUrlForSnyk(snykUrlTemplate, "cli", version, platform); + URL snykToHtmlDownloadUrl = DownloadService.constructDownloadUrlForSnyk(snykUrlTemplate, "snyk-to-html", "latest", platform); - expected.mkdirs(); - nodeChannel.call(new Downloader(snykDownloadUrl, expected.child(platform.snykWrapperFileName))); - nodeChannel.call(new Downloader(snykToHtmlDownloadUrl, expected.child(platform.snykToHtmlWrapperFileName))); - expected.child(INSTALLED_FROM).write(snykDownloadUrl.toString(), UTF_8.name()); - expected.child(TIMESTAMP_FILE).write(valueOf(Instant.now().toEpochMilli()), UTF_8.name()); + LOG.info("Downloading CLI from {}", snykDownloadUrl); + LOG.info("Downloading snyk-to-html from {}", snykToHtmlDownloadUrl); - return expected; - } catch (RuntimeException | IOException | InterruptedException ex) { - throw new RuntimeException("Failed to install Snyk.", ex); + expected.mkdirs(); + nodeChannel.call(new Downloader(snykDownloadUrl, expected.child(platform.snykWrapperFileName))); + nodeChannel.call(new Downloader(snykToHtmlDownloadUrl, expected.child(platform.snykToHtmlWrapperFileName))); + expected.child(INSTALLED_FROM).write(snykDownloadUrl.toString(), UTF_8.name()); + expected.child(TIMESTAMP_FILE).write(valueOf(Instant.now().toEpochMilli()), UTF_8.name()); + + return expected; + } catch (RuntimeException | IOException | InterruptedException ex) { + LOG.error("Failed to install Snyk.", ex); + } } + throw new RuntimeException("Failed to install Snyk."); } @SuppressWarnings("unused") diff --git a/src/main/java/io/snyk/jenkins/tools/internal/DownloadService.java b/src/main/java/io/snyk/jenkins/tools/internal/DownloadService.java index 619e243..1574db8 100644 --- a/src/main/java/io/snyk/jenkins/tools/internal/DownloadService.java +++ b/src/main/java/io/snyk/jenkins/tools/internal/DownloadService.java @@ -1,24 +1,34 @@ package io.snyk.jenkins.tools.internal; +import io.snyk.jenkins.PluginMetadata; import io.snyk.jenkins.tools.Platform; import javax.annotation.Nonnull; import java.io.IOException; import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import static java.lang.String.format; public class DownloadService { + private static final String SNYK_DOWNLOAD_PRIMARY = "https://downloads.snyk.io/%s/%s/%s"; + private static final String SNYK_DOWNLOAD_SECONDARY = "https://static.snyk.io/%s/%s/%s"; + public static final List SNYK_CLI_DOWNLOAD_URLS = Collections.unmodifiableList(Arrays.asList(SNYK_DOWNLOAD_PRIMARY, SNYK_DOWNLOAD_SECONDARY)); private DownloadService() { // squid:S1118 } - public static URL getDownloadUrlForSnyk(@Nonnull String version, @Nonnull Platform platform) throws IOException { - return new URL(format("https://static.snyk.io/cli/%s/%s", version, platform.snykWrapperFileName)); - } + public static URL constructDownloadUrlForSnyk(@Nonnull String urlTemplate, @Nonnull String product, @Nonnull String version, @Nonnull Platform platform) throws IOException { + URL urlNoUtm; - public static URL getDownloadUrlForSnykToHtml(@Nonnull String version, @Nonnull Platform platform) throws IOException { - return new URL(format("https://static.snyk.io/snyk-to-html/%s/%s", version, platform.snykToHtmlWrapperFileName)); + if (product.equals("cli")) { + urlNoUtm = new URL(format(urlTemplate, product, version, platform.snykWrapperFileName)); + } else { // snyk-to-html + urlNoUtm = new URL(format(urlTemplate, product, version, platform.snykToHtmlWrapperFileName)); + } + return new URL(urlNoUtm.toString() + "?utm_source=" + PluginMetadata.getIntegrationName()); } } diff --git a/src/test/java/io/snyk/jenkins/tools/internal/DownloadServiceTest.java b/src/test/java/io/snyk/jenkins/tools/internal/DownloadServiceTest.java new file mode 100644 index 0000000..b823a21 --- /dev/null +++ b/src/test/java/io/snyk/jenkins/tools/internal/DownloadServiceTest.java @@ -0,0 +1,47 @@ +package io.snyk.jenkins.tools.internal; + +import io.snyk.jenkins.PluginMetadata; +import org.junit.Test; + +import io.snyk.jenkins.tools.Platform; + +import java.io.IOException; +import java.net.URL; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.core.IsEqual.equalTo; +import static org.hamcrest.core.IsNull.notNullValue; + + +public class DownloadServiceTest { + + @Test + public void constructDownloadUrlForSnyk_shouldReturnExpectedUrlForCli() throws IOException { + String urlTemplate = "https://downloads.snyk.io/%s/%s/%s"; + String product = "cli"; + String version = "stable"; + String queryParam = "?utm_source=" + PluginMetadata.getIntegrationName(); + Platform platform = Platform.MAC_OS; + + URL expectedUrl = new URL("https://downloads.snyk.io/" + product + "/" + version + "/" + "snyk-macos" + queryParam); + URL actualUrl = DownloadService.constructDownloadUrlForSnyk(urlTemplate, product, version, platform); + + assertThat(actualUrl, notNullValue()); + assertThat(actualUrl, equalTo(expectedUrl)); + } + + @Test + public void constructDownloadUrlForSnyk_shouldReturnExpectedUrlForSnykToHtml() throws IOException { + String urlTemplate = "https://downloads.snyk.io/%s/%s/%s"; + String product = "snyk-to-html"; + String version = "stable"; + String queryParam = "?utm_source=" + PluginMetadata.getIntegrationName(); + Platform platform = Platform.MAC_OS; + + URL expectedUrl = new URL("https://downloads.snyk.io/" + product + "/" + version + "/" + "snyk-to-html-macos" + queryParam); + URL actualUrl = DownloadService.constructDownloadUrlForSnyk(urlTemplate, product, version, platform); + + assertThat(actualUrl, notNullValue()); + assertThat(actualUrl, equalTo(expectedUrl)); + } +}