Skip to content

Commit

Permalink
Merge pull request #39 from devatherock/31-semver
Browse files Browse the repository at this point in the history
Added sort parameter to /version endpoint - closes #31
  • Loading branch information
devatherock authored Nov 21, 2020
2 parents 45849f3 + af67862 commit f7f98d7
Show file tree
Hide file tree
Showing 6 changed files with 253 additions and 34 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- `/metrics` endpoint
- Enabled access logs
- [#24](https://github.com/devatherock/artifactory-badge/issues/24): Unit tests for 100% code coverage
- [#31](https://github.com/devatherock/artifactory-badge/issues/31): `sort` parameter to `/version` endpoint with default value as `date`. With value `semver`, it'll pull the latest out of only semantic version tags

### Changed
- Caught exception when JSON processing fails
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,14 @@ public class VersionController {
*
* @param packageName
* @param badgeLabel
* @param sort
* @return the version badge
*/
@Get(value = "/version", produces = CONTENT_TYPE_BADGE)
public String getLatestVersion(@QueryValue("package") String packageName,
@QueryValue(value = "label", defaultValue = "version") String badgeLabel) {
@QueryValue(value = "label", defaultValue = "version") String badgeLabel,
@QueryValue(value = "sort", defaultValue = "date") String sortType) {
LOGGER.debug("In getLatestVersion");
return badgeService.getLatestVersionBadge(packageName, badgeLabel);
return badgeService.getLatestVersionBadge(packageName, badgeLabel, sortType);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
package io.github.devatherock.artifactory.service;

import java.text.DecimalFormat;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

import javax.inject.Singleton;

import io.github.devatherock.artifactory.config.ArtifactoryProperties;
import io.github.devatherock.artifactory.entities.ArtifactoryFileStats;
import io.github.devatherock.artifactory.entities.ArtifactoryFolderElement;
Expand All @@ -17,14 +26,6 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

import javax.inject.Singleton;
import java.text.DecimalFormat;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Pattern;

/**
* Service class to fetch information required for the badges and then generate
* them
Expand All @@ -41,6 +42,26 @@ public class DockerBadgeService {
private static final String HDR_API_KEY = "X-JFrog-Art-Api";
private static final double PULLS_REDUCER = 1000;
private static final int MAX_REDUCTIONS = 3;

/**
* Constant for sort by semantic version
*/
private static final String SORT_TYPE_SEMVER = "semver";
/**
* Major version part of a semantic version
*/
private static final String VERSION_PART_MAJOR = "major";
/**
* Minor version part of a semantic version
*/
private static final String VERSION_PART_MINOR = "minor";
/**
* Patch version part of a semantic version
*/
private static final String VERSION_PART_PATCH = "patch";
/**
* Map containing suffixes for download count
*/
private static final Map<Integer, String> PULLS_SUFFIX = new HashMap<>();
/**
* Formatter to parse dates like {@code 2020-10-01T00:00:00.000Z}
Expand All @@ -50,7 +71,12 @@ public class DockerBadgeService {
/**
* Pattern to match versions like {@code 1}, {@code 1.2} and {@code 1.2.2}
*/
private static final Pattern PTRN_NUMERIC_VERSION = Pattern.compile("^[0-9]+([\\.0-9]+)*$");
private static final Pattern PTRN_NUMERIC_VERSION = Pattern.compile(
"^(0|[1-9][0-9]*)(\\.(0|[1-9][0-9]*))*(\\-[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?$");
/**
* Pattern to match numbers
*/
private static final Pattern PTRN_NUMBER = Pattern.compile("^[0-9]+$");

static {
PULLS_SUFFIX.put(0, "");
Expand Down Expand Up @@ -116,8 +142,16 @@ public String getPullsCountBadge(String packageName, String badgeLabel) {
}
}

/**
* Generates the latest version badge for the input package
*
* @param packageName
* @param badgeLabel
* @param sortType
* @return the latest version badge
*/
@Cacheable("version-cache")
public String getLatestVersionBadge(String packageName, String badgeLabel) {
public String getLatestVersionBadge(String packageName, String badgeLabel, String sortType) {
LOGGER.debug("In getLatestVersionBadge");
ArtifactoryFolderInfo folderInfo = getArtifactoryFolderInfo(packageName);

Expand All @@ -126,12 +160,31 @@ public String getLatestVersionBadge(String packageName, String badgeLabel) {

for (ArtifactoryFolderElement child : folderInfo.getChildren()) {
if (child.isFolder()) {
ArtifactoryFolderInfo currentVersion = getArtifactoryFolderInfo(packageName + child.getUri());

if (null == latestVersion || (null != currentVersion
&& Instant.from(MODIFIED_TIME_PARSER.parse(currentVersion.getLastModified())).compareTo(
Instant.from(MODIFIED_TIME_PARSER.parse(latestVersion.getLastModified()))) > 0)) {
latestVersion = currentVersion;
if (SORT_TYPE_SEMVER.equals(sortType)) {
// Substring to remove the leading slash
String currentVersion = child.getUri().substring(1);

if (PTRN_NUMERIC_VERSION.matcher(currentVersion).matches()) {
if (null == latestVersion) {
latestVersion = ArtifactoryFolderInfo.builder().path(child.getUri()).build();
} else {
// Substring to remove the leading slash
int result = compareVersions(latestVersion.getPath().substring(1), currentVersion,
VERSION_PART_MAJOR);
if (result == -1) {
latestVersion = ArtifactoryFolderInfo.builder().path(child.getUri()).build();
}
}
}
} else {
ArtifactoryFolderInfo currentVersion = getArtifactoryFolderInfo(packageName + child.getUri());

if (null == latestVersion || (null != currentVersion
&& Instant.from(MODIFIED_TIME_PARSER.parse(currentVersion.getLastModified())).compareTo(
Instant.from(
MODIFIED_TIME_PARSER.parse(latestVersion.getLastModified()))) > 0)) {
latestVersion = currentVersion;
}
}
}
}
Expand Down Expand Up @@ -168,16 +221,6 @@ private ArtifactoryFolderInfo getArtifactoryFolderInfo(String packageName) {
return folderInfo;
}

/**
* Generates a not found badge
*
* @param badgeLabel the badge label
* @return a not found badge
*/
private String generateNotFoundBadge(String badgeLabel) {
return badgeGenerator.generateBadge(badgeLabel, "Not Found");
}

/**
* Reads a docker {@code manifest.json} file
*
Expand All @@ -204,7 +247,7 @@ private DockerManifest readManifest(String packageName, String tag) {
/**
* Reads statistics of the {@code manifest.json} file for the supplied docker
* image and tag
*
*
* @param packageName the docker image name
* @param tagUri subfolder path to a docker image tag
* @return {@link ArtifactoryFileStats}
Expand All @@ -225,6 +268,80 @@ private ArtifactoryFileStats getManifestStats(String packageName, String tagUri)
return fileStats;
}

/**
* Generates a not found badge
*
* @param badgeLabel the badge label
* @return a not found badge
*/
private String generateNotFoundBadge(String badgeLabel) {
return badgeGenerator.generateBadge(badgeLabel, "Not Found");
}

/**
* Compares two versions
*
* @param versionOne
* @param versionTwo
* @param versionPartType
* @return {@literal -1} if {@code versionTwo} greater than {@code versionOne},
* {@literal 1} otherwise
*/
private int compareVersions(String versionOne, String versionTwo, String versionPartType) {
int result = 0;

int versionPartEndOne = getVersionPartEndIndex(versionOne);
String versionPartOneText = versionOne.substring(0,
versionPartEndOne != -1 ? versionPartEndOne : versionOne.length());
long versionPartOne = readVersionAsNumber(versionPartOneText);
int versionPartEndTwo = getVersionPartEndIndex(versionTwo);
String versionPartTwoText = versionTwo.substring(0,
versionPartEndTwo != -1 ? versionPartEndTwo : versionTwo.length());
long versionPartTwo = readVersionAsNumber(versionPartTwoText);

if (versionPartOne > versionPartTwo) {
result = 1;
} else if (versionPartOne < versionPartTwo) {
result = -1;
} else {
if ((versionPartOneText.length() + 1) >= versionOne.length()) {
if ((versionPartTwoText.length() + 1) < versionTwo.length()) {
result = -1;
}
} else if ((versionPartTwoText.length() + 1) < versionTwo.length()) {
if (!VERSION_PART_PATCH.equals(versionPartType)) {
result = compareVersions(versionOne.substring(versionPartEndOne + 1),
versionTwo.substring(versionPartEndTwo + 1),
VERSION_PART_MAJOR.equals(versionPartType) ? VERSION_PART_MINOR : VERSION_PART_PATCH);
}
} else {
result = 1;
}
}

return result;
}

/**
* Returns the index at which the first version part ends
*
* @param version
* @return the index
*/
private int getVersionPartEndIndex(String version) {
return version.indexOf('.') != -1 ? version.indexOf('.') : version.indexOf('-');
}

/**
* Converts version string into a number
*
* @param version
* @return the version as number
*/
private long readVersionAsNumber(String version) {
return PTRN_NUMBER.matcher(version).matches() ? Long.parseLong(version) : 0;
}

/**
* Returns the formatted value to be displayed in the version badge
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,4 +202,41 @@ class VersionControllerSpec extends Specification {
badge == 'dummyBadge'
cachedBadge == badge
}

void 'test get latest version badge - only semantic versions'() {
given:
String packageName = 'docker/devatherock/simple-slack'

and:
WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}")
.willReturn(WireMock.okJson(
TestUtil.getFoldersResponse('/devatherock/simple-slack', '2020-10-01T00:00:00.000Z'))))
WireMock.givenThat(WireMock.get(WireMock.urlPathEqualTo('/static/v1'))
.withQueryParam('label', equalTo('dummy'))
.withQueryParam('message', equalTo('v1.1.2'))
.withQueryParam('color', equalTo('blue'))
.willReturn(WireMock.okXml('dummyBadge')))

when:
String badge = httpClient.toBlocking().retrieve(
HttpRequest.GET(UriBuilder.of('/version')
.queryParam('package', packageName)
.queryParam('label', 'dummy')
.queryParam('sort', 'semver').build()))

then:
WireMock.verify(1,
WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}"))
.withHeader(DockerBadgeService.HDR_API_KEY, equalTo('dummyKey')))
WireMock.verify(0,
WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}/1.1.0")))
WireMock.verify(0,
WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}/1.1.2")))
WireMock.verify(0,
WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}/latest")))
WireMock.verify(0,
WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}/abcdefgh")))
WireMock.verify(1, WireMock.getRequestedFor(WireMock.urlPathEqualTo("/static/v1")))
badge == 'dummyBadge'
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,7 @@ class DockerBadgeServiceSpec extends Specification {
.willReturn(WireMock.notFound()))

when:
String badge = dockerBadgeService.getLatestVersionBadge(packageName, 'version')
String badge = dockerBadgeService.getLatestVersionBadge(packageName, 'version', 'date')

then:
WireMock.verify(1,
Expand All @@ -215,7 +215,7 @@ class DockerBadgeServiceSpec extends Specification {
.willReturn(WireMock.okJson('{}')))

when:
String badge = dockerBadgeService.getLatestVersionBadge(packageName, 'version')
String badge = dockerBadgeService.getLatestVersionBadge(packageName, 'version', 'date')

then:
WireMock.verify(1,
Expand All @@ -236,7 +236,7 @@ class DockerBadgeServiceSpec extends Specification {
.willReturn(WireMock.okJson(TestUtil.getFoldersResponse("${packageName}/1.1.0", '2020-10-01T00:00:00.000Z'))))

when:
String badge = dockerBadgeService.getLatestVersionBadge(packageName, 'version')
String badge = dockerBadgeService.getLatestVersionBadge(packageName, 'version', 'date')

then:
WireMock.verify(1,
Expand All @@ -260,7 +260,7 @@ class DockerBadgeServiceSpec extends Specification {
.willReturn(WireMock.okJson(TestUtil.getFolderWithOnlyFileResponse())))

when:
String badge = dockerBadgeService.getLatestVersionBadge(packageName, 'version')
String badge = dockerBadgeService.getLatestVersionBadge(packageName, 'version', 'date')

then:
WireMock.verify(1,
Expand Down Expand Up @@ -289,7 +289,7 @@ class DockerBadgeServiceSpec extends Specification {
.willReturn(WireMock.notFound()))

when:
String badge = dockerBadgeService.getLatestVersionBadge(packageName, 'version')
String badge = dockerBadgeService.getLatestVersionBadge(packageName, 'version', 'date')

then:
WireMock.verify(1,
Expand All @@ -311,6 +311,29 @@ class DockerBadgeServiceSpec extends Specification {
badge == 'dummyBadge'
}

void 'test get latest version badge - similar version numbers'() {
given:
String packageName = 'docker/devatherock/simple-slack'

and:
WireMock.givenThat(WireMock.get("/artifactory/api/storage/${packageName}")
.willReturn(WireMock.okJson(TestUtil.getSimilarFoldersResponse())))

when:
String badge = dockerBadgeService.getLatestVersionBadge(packageName, 'version', 'semver')

then:
WireMock.verify(1,
WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}"))
.withHeader(DockerBadgeService.HDR_API_KEY, equalTo('dummyKey')))
WireMock.verify(0,
WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}/1.1.0")))
WireMock.verify(0,
WireMock.getRequestedFor(urlEqualTo("/artifactory/api/storage/${packageName}/1.1.2")))
1 * badgeGenerator.generateBadge('version', 'v1.1.0-alpha') >> 'dummyBadge'
badge == 'dummyBadge'
}

void 'test get version badge value'() {
expect:
dockerBadgeService.getVersionBadgeValue(new ArtifactoryFolderInfo(path: path)) == outputVersion
Expand Down Expand Up @@ -343,4 +366,21 @@ class DockerBadgeServiceSpec extends Specification {
1 * badgeGenerator.generateBadge('layers', 'Not Found') >> 'dummyBadge'
badge == 'dummyBadge'
}

void 'test compare versions'() {
expect:
dockerBadgeService.compareVersions(versionOne, versionTwo, 'major') == expectedResult

where:
versionOne | versionTwo | expectedResult
'2' | '1.1' | 1
'1' | '1.1' | -1
'1.1-alpha' | '1.1-beta' | 0
'1.1.1-alpha' | '1.1.1-beta' | 0
'2.5-alpine' | '2.5' | 1
'2.5' | '2.5-alpine' | -1
'1.1.1' | '1.1.2' | -1
'1.1.2' | '1.1.1' | 1
'1.1.2' | '1.1.2' | 0
}
}
Loading

0 comments on commit f7f98d7

Please sign in to comment.