Skip to content

Commit

Permalink
chore: update download CLI URL (#192)
Browse files Browse the repository at this point in the history
  • Loading branch information
j-luong authored Aug 12, 2024
1 parent 382c084 commit 96148ed
Show file tree
Hide file tree
Showing 10 changed files with 149 additions and 32 deletions.
25 changes: 21 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,24 +41,41 @@ jobs:

- name: Install Snyk CLI (Ubuntu/macOS)
if: ${{ matrix.os != 'windows' }}
run: sudo npm install -g snyk
run: |
sudo npm install -g snyk
snyk -v
which snyk
- name: Install Snyk CLI (Windows)
if: ${{ matrix.os == 'windows' }}
run: npm install -g snyk
run: |
npm install -g snyk
snyk -v
where snyk
- name: Run Acceptance Tests (Ubuntu/macOS)
if: ${{ matrix.os != 'windows' }}
- name: Run Acceptance Tests (Ubuntu)
if: ${{ matrix.os == 'ubuntu' }}
run: mvn -B invoker:install invoker:run
env:
POM_EXCLUDE_CODE_TEST: "test-code-test/pom.xml"
SNYK_TEST_TOKEN: ${{secrets.SNYK_TEST_TOKEN}}
SNYK_CLI_EXECUTABLE: /usr/local/bin/snyk
SNYK_DOWNLOAD_DESTINATION: "downloads/snyk"

- name: Run Acceptance Tests (macOS)
if: ${{ matrix.os == 'macos' }}
run: mvn -B invoker:install invoker:run
env:
POM_EXCLUDE_CODE_TEST: "test-code-test/pom.xml"
SNYK_TEST_TOKEN: ${{secrets.SNYK_TEST_TOKEN}}
SNYK_CLI_EXECUTABLE: /opt/homebrew/bin/snyk
SNYK_DOWNLOAD_DESTINATION: "downloads/snyk"

- name: Run Acceptance Tests (Windows)
if: ${{ matrix.os == 'windows' }}
run: mvn -B invoker:install invoker:run
env:
POM_EXCLUDE_CODE_TEST: "test-code-test/pom.xml"
POM_EXCLUDE_PATTERN: "test-container-test/pom.xml"
SNYK_TEST_TOKEN: ${{secrets.SNYK_TEST_TOKEN}}
SNYK_CLI_EXECUTABLE: "C:\\npm\\prefix\\snyk.cmd"
Expand Down
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@
</pomIncludes>
<pomExcludes>
<pomExclude>${env.POM_EXCLUDE_PATTERN}</pomExclude>
<pomExclude>${env.POM_EXCLUDE_CODE_TEST}</pomExclude>
</pomExcludes>
<preBuildHookScript>setup</preBuildHookScript>
<postBuildHookScript>verify</postBuildHookScript>
Expand Down
1 change: 1 addition & 0 deletions src/it/test-code-test/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
</cli>
<args>
<arg>--print-deps</arg>
<arg>-d</arg>
</args>
<apiToken>${env.SNYK_TEST_TOKEN}</apiToken>
</configuration>
Expand Down
2 changes: 2 additions & 0 deletions src/it/test-code-test/verify.groovy
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import org.codehaus.plexus.util.FileUtils;

// TODO: debug issue with this test

String log = FileUtils.fileRead(new File(basedir, "build.log"))

if (!log.contains("SQL Injection")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import java.util.regex.Pattern;

public class CLIVersions {
public static final String LATEST_VERSION_KEYWORD = "latest";
public static final String STABLE_VERSION_KEYWORD = "stable";

static final Pattern versionRegex = Pattern.compile("^(?:\\d+\\.){2}\\d+$");

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.snyk.snyk_maven_plugin.download;

import com.google.common.collect.Lists;
import org.apache.commons.codec.digest.DigestUtils;

import java.io.BufferedReader;
Expand All @@ -10,14 +11,18 @@
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

import static io.snyk.snyk_maven_plugin.download.UpdatePolicy.shouldUpdate;

public class ExecutableDownloader {

private static final String SNYK_CLI_DOWNLOAD_FORMAT = "https://static.snyk.io/cli/%s/%s";
public static final String SNYK_INTEGRATION_NAME = "MAVEN_PLUGIN";
private static final String SNYK_CLI_DOWNLOAD_PRIMARY = "https://downloads.snyk.io/cli/%s/%s";
private static final String SNYK_CLI_DOWNLOAD_SECONDARY = "https://static.snyk.io/cli/%s/%s";
private static final List<String> SNYK_CLI_DOWNLOAD_URLS = Arrays.asList(SNYK_CLI_DOWNLOAD_PRIMARY, SNYK_CLI_DOWNLOAD_SECONDARY);

/**
* Ensure that the CLI is downloaded and that it matches the corresponding `.sha256` file.
Expand Down Expand Up @@ -51,9 +56,10 @@ public static File ensure(URL cliDownloadURL, File cliFile, String updatePolicy,
checksumFile.delete();
cliFile.getParentFile().mkdirs();

downloader.download(cliDownloadURL, cliFile);
URL cliUrlWithUtm = new URL(cliDownloadURL.toString() + "?utm_source=" + SNYK_INTEGRATION_NAME);
downloader.download(cliUrlWithUtm, cliFile);

URL checksumUrl = new URL(cliDownloadURL.toString() + ".sha256");
URL checksumUrl = new URL(cliDownloadURL.toString() + ".sha256" + "?utm_source=" + SNYK_INTEGRATION_NAME);
downloader.download(checksumUrl, checksumFile);

if (!verifyChecksum(cliFile, checksumFile)) {
Expand All @@ -67,24 +73,61 @@ public static File ensure(URL cliDownloadURL, File cliFile, String updatePolicy,
}
}

/**
* Iterate a list of download URLs and ensures the CLI is downloaded and that it matches the corresponding `.sha256` file.
* Check that the CLI exists, that the sha 256 file exists, that they match, and that we don't need to do an update (per the update policy).
* If any of those conditions are not true, then we delete both the CLI file and the sha256 file and (re-)download them.
* If a download URL fails, try the next URL in the list
*
* @param cliDownloadURLs the list of URLs to try and download the CLI from.
* @param cliFile the File representing where the CLI should either be downloaded to, or the location in which it should be verified.
* @param updatePolicy describes how/when to do CLI version updates.
* @param downloader the FileDownloader instance to be used for downloading.
* @return
*/
public static File iterateAndEnsure(List<URL> cliDownloadURLs, File cliFile, String updatePolicy, FileDownloader downloader) {
for (URL cliDownloadURL : cliDownloadURLs) {
try {
File downloadedFile = ensure(cliDownloadURL, cliFile, updatePolicy, downloader);

if (downloadedFile.exists()) {
return downloadedFile;
}
} catch (RuntimeException e) {
System.err.println("Failed to download from '" + cliDownloadURL + "': " + e);
}
}
throw new RuntimeException("Failed to download from URLs");
}

static String downloadToString(URL url) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8));
String all = br.lines().collect(Collectors.joining("\n"));
return all;
}

public static String getLatestVersion() throws IOException {
String version = "v" + downloadToString(new URL("https://static.snyk.io/cli/latest/version")).trim();
String version = "v" + downloadToString(new URL("https://downloads.snyk.io/cli/stable/version")).trim();
return version;
}

public static URL getDownloadUrl(Platform platform, String version) {
public static List<URL> getDownloadUrls(Platform platform, String version) {
List<URL> downloadUrls = new ArrayList<>();

for (String url : SNYK_CLI_DOWNLOAD_URLS) {
downloadUrls.add(getDownloadUrl(url, platform, version));
}

return downloadUrls;
}

public static URL getDownloadUrl(String url, Platform platform, String version) {
try {
if (version.equals("latest")) {
if (version.equals("stable")) {
String actualLatestVersion = getLatestVersion();
return new URL(String.format(SNYK_CLI_DOWNLOAD_FORMAT, actualLatestVersion, platform.snykExecutableFileName));
return new URL(String.format(url, actualLatestVersion, platform.snykExecutableFileName));
}
return new URL(String.format(SNYK_CLI_DOWNLOAD_FORMAT, version, platform.snykExecutableFileName));
return new URL(String.format(url, version, platform.snykExecutableFileName));
} catch (MalformedURLException e) {
throw new RuntimeException("Download URL is malformed", e);
} catch (IOException e) {
Expand Down
8 changes: 4 additions & 4 deletions src/main/java/io/snyk/snyk_maven_plugin/goal/SnykMojo.java
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ public String getDownloadVersion() {
return Optional.ofNullable(cli)
.map(cli -> cli.version)
.map(CLIVersions::sanitize)
.orElse(CLIVersions.LATEST_VERSION_KEYWORD);
.orElse(CLIVersions.STABLE_VERSION_KEYWORD);
}

public boolean shouldSkip() {
Expand All @@ -102,12 +102,12 @@ public File getDownloadDestination() {
});
}

public URL getDownloadUrl() {
return ExecutableDownloader.getDownloadUrl(platform, getDownloadVersion());
public List<URL> getDownloadUrls() {
return ExecutableDownloader.getDownloadUrls(platform, getDownloadVersion());
}

public String getUpdatePolicy() {
if (!getDownloadVersion().equals(CLIVersions.LATEST_VERSION_KEYWORD)) {
if (!getDownloadVersion().equals(CLIVersions.STABLE_VERSION_KEYWORD)) {
return UpdatePolicy.ALWAYS;
}
return Optional.ofNullable(cli)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,8 @@ private String getVersion(String executablePath) {
}

private File downloadExecutable() {
return ExecutableDownloader.ensure(
mojo.getDownloadUrl(),
return ExecutableDownloader.iterateAndEnsure(
mojo.getDownloadUrls(),
mojo.getDownloadDestination(),
mojo.getUpdatePolicy(),
FileDownloader::downloadFile
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,20 @@
import org.junit.jupiter.api.Test;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static io.snyk.snyk_maven_plugin.download.ExecutableDownloader.ensure;
import static org.junit.jupiter.api.Assertions.*;

public class ExecutableDownloaderTest {
private final byte[] incorrectShasum = "0000000000000000000000000000000000000000000000000000000000000000 snyk-macos".getBytes();
private final byte[] helloWorldShasum = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9 snyk-macos".getBytes();
private final byte[] helloWorldBytes = "hello world".getBytes();

@Test
public void throwsWhenShaDoesntMatch() throws Exception {
Expand All @@ -19,23 +25,50 @@ public void throwsWhenShaDoesntMatch() throws Exception {
File cliFile = new File(cliPath.toString());

FileDownloader mockDownloader = (URL url, File target) -> {
if (url.toString().equals("https://static.snyk.io/cli/latest/snyk-macos.sha256")) {
Files.write(
target.toPath(),
"0000000000000000000000000000000000000000000000000000000000000000 snyk-macos".getBytes()
);
if (url.toString().equals("https://static.snyk.io/cli/stable/snyk-macos.sha256?utm_source=MAVEN_PLUGIN")) {
Files.write(target.toPath(), incorrectShasum);
} else {
FileDownloader.downloadFile(url, target);
Files.write(target.toPath(), helloWorldBytes);
}
};

RuntimeException e = assertThrows(RuntimeException.class, () -> ExecutableDownloader.ensure(
new URL("https://static.snyk.io/cli/latest/snyk-macos"),
RuntimeException e = assertThrows(RuntimeException.class, () -> ensure(
new URL("https://static.snyk.io/cli/stable/snyk-macos"),
cliFile,
"never",
mockDownloader
));

assertEquals(e.getMessage(), "computed sha256 checksum for CLI download does not expected");
assertEquals("computed sha256 checksum for CLI download does not expected", e.getMessage());
}

@Test
public void iteratesToNextURLOnDownloadFailure() throws Exception {
// Mock objects
Path tempDirectory = Files.createTempDirectory(getClass().getSimpleName());
Path cliPath = tempDirectory.resolve("snyk-macos");
File cliFile = new File(cliPath.toString());

FileDownloader mockDownloader = (URL url, File target) -> {
if (url.toString().contains("https://downloads.snyk.io/cli/")) {
throw new IOException("mock download failure");
}
if (url.toString().equals("https://static.snyk.io/cli/1.1292.0/snyk-macos.sha256?utm_source=MAVEN_PLUGIN")) {
Files.write(target.toPath(), helloWorldShasum);
}
if (url.toString().equals("https://static.snyk.io/cli/1.1292.0/snyk-macos?utm_source=MAVEN_PLUGIN")) {
Files.write(target.toPath(), helloWorldBytes);
}
};

// Prepare test data
List<URL> urls = ExecutableDownloader.getDownloadUrls(Platform.MAC_OS, "1.1292.0");

// Call the method
File result = ExecutableDownloader.iterateAndEnsure(urls, cliFile, "never", mockDownloader);

// Verify that the downloaded file exists (from the second URL)
assertEquals(cliFile, result);
assertTrue(result.exists());
}
}
22 changes: 21 additions & 1 deletion src/test/java/io/snyk/snyk_maven_plugin/goal/SnykMojoTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@
import io.snyk.snyk_maven_plugin.download.CLIVersions;
import org.junit.jupiter.api.Test;

import java.net.URL;
import java.util.Arrays;
import java.util.Optional;
import java.util.List;

import static java.util.Collections.emptyList;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

class SnykMojoTest {

Expand All @@ -26,7 +30,23 @@ public void shouldDefaultToNoExecutable() {
@Test
public void shouldDefaultToLatestVersion() {
SnykMojo mojo = createMojo();
assertEquals(CLIVersions.LATEST_VERSION_KEYWORD, mojo.getDownloadVersion());
assertEquals(CLIVersions.STABLE_VERSION_KEYWORD, mojo.getDownloadVersion());
}

@Test
public void shouldGetDownloadUrls() {
String expectedPrimaryUrl = "https://downloads.snyk.io";
String expectedSecondaryUrl = "https://static.snyk.io";
List<String> expectedUrl = Arrays.asList(expectedPrimaryUrl, expectedSecondaryUrl);

SnykMojo mojo = createMojo();

List<URL> actualUrls = mojo.getDownloadUrls();

for (int i = 0; i < actualUrls.size(); i++) {
boolean containsExpectedUrl = actualUrls.get(i).toString().contains(expectedUrl.get(i));
assertTrue(containsExpectedUrl);
}
}

private SnykMojo createMojo() {
Expand Down

0 comments on commit 96148ed

Please sign in to comment.