Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/ansible galaxy v3 compatibility #24

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Build
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 11 for x64
uses: actions/setup-java@v4
with:
java-version: '11'
distribution: 'temurin'
architecture: x64
- name: Build with Maven
run: |
mvn --batch-mode clean package -PbuildKar


38 changes: 38 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Release
on:
push:
tags:
- "v*"
permissions:
contents: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 11 for x64
uses: actions/setup-java@v4
with:
java-version: '11'
distribution: 'temurin'
architecture: x64
- name: Build with Maven
run: |
mvn --batch-mode clean package -PbuildKar

- name: Extract version
id: version
run: |
REF=${{ github.ref }}
VERSION=${REF#refs/tags/v}
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT

- name: Release with Notes
uses: softprops/action-gh-release@v2
with:
name: ${{ steps.version.outputs.VERSION }}
files: |
nexus-repository-ansiblegalaxy/target/*.jar
nexus-repository-ansiblegalaxy/target/*.kar
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ The table below outlines what version of Nexus Repository the plugin was built a
| v0.2.1 | 3.31.0-01 |
| v0.2.2 | 3.38.0-01 / 3.39.0.01 |
| v0.3.0 | \>= 3.41.0 |
| v0.3.1 | \>= 3.41.0 |
| v0.3.3 | \>= 3.67.0 (Java 11) |
| v0.4.x (TBD) | \>= 3.69.0 (Java 17) |

If a new version of Nexus Repository is released and the plugin needs changes, a new release will be made, and this
table will be updated to indicate which version of Nexus Repository it will function against. This is done on a time
Expand Down
2 changes: 1 addition & 1 deletion nexus-repository-ansiblegalaxy-it/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<parent>
<groupId>org.sonatype.nexus.plugins</groupId>
<artifactId>nexus-repository-ansiblegalaxy-parent</artifactId>
<version>0.3.0</version>
<version>0.3.3</version>
</parent>

<artifactId>nexus-repository-ansiblegalaxy-it</artifactId>
Expand Down
2 changes: 1 addition & 1 deletion nexus-repository-ansiblegalaxy/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
<parent>
<groupId>org.sonatype.nexus.plugins</groupId>
<artifactId>nexus-repository-ansiblegalaxy-parent</artifactId>
<version>0.3.0</version>
<version>0.3.3</version>
</parent>

<artifactId>nexus-repository-ansiblegalaxy</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public enum AssetKind {
API_METADATA(METADATA),
COLLECTION_DETAIL(METADATA),
COLLECTION_VERSION_LIST(METADATA),
COLLECTION_VERSION_LIST_LIMIT(METADATA),
COLLECTION_VERSION_DETAIL(METADATA),
COLLECTION_ARTIFACT(CONTENT),
ROLE_SEARCH(METADATA),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ abstract class AnsibleGalaxyRecipeSupport
setAssetKind(AssetKind.COLLECTION_VERSION_LIST)
)
}
// /api/v3/plugin/ansible/content/published/collections/index/ansible/netcommon/versions/
static Matcher collectionVersionListMatcherPages() {
LogicMatchers.and(
new ActionMatcher(GET, HEAD),
new QueryTokenMatcher("/api/{apiversion}/plugin/ansible/content/published/collections/index/{author}/{module}/versions/", [limit: "limit", offset: "offset"]),
setAssetKind(AssetKind.COLLECTION_VERSION_LIST_LIMIT)
)
}

static Matcher collectionVersionDetailMatcher() {
LogicMatchers.and(
Expand All @@ -161,17 +169,27 @@ abstract class AnsibleGalaxyRecipeSupport
}

static Matcher collectionArtifactMatcher() {
LogicMatchers.and(
new ActionMatcher(GET, HEAD),
new TokenMatcher("/api/{apiversion}/plugin/ansible/content/published/collections/artifacts/{author}-{module}-{version}.tar.gz"),
setAssetKind(AssetKind.COLLECTION_ARTIFACT)
)
}

static Matcher collectionArtifactV2Matcher() {
LogicMatchers.and(
new ActionMatcher(GET, HEAD),
new TokenMatcher("/download/{author}-{module}-{version}.tar.gz"),
setAssetKind(AssetKind.COLLECTION_ARTIFACT)
)
}

// https://repo.angeloxx.lan/repository/galaxy/api/v3/plugin/ansible/content/published/collections/artifacts/telekom_mms-icinga_director-1.34.1.tar.gz
// /api/v3/plugin/ansible/content/published/collections/artifacts/telekom_mms-icinga_director-1.34.1.tar.gz
static Matcher collectionArtifactIhmMatcher() {
LogicMatchers.and(
new ActionMatcher(GET, HEAD),
new TokenMatcher("/collection/{author}/{module}/{version}/{author}-{module}-{version}.tar.gz"),
new TokenMatcher("/api/{apiversion}/plugin/ansible/content/published/collections/artifacts/{author}-{module}-{version}.tar.gz"),
setAssetKind(AssetKind.COLLECTION_ARTIFACT)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
import static org.sonatype.nexus.plugins.ansiblegalaxy.internal.util.AnsibleGalaxyDataAccess.HASH_ALGORITHMS;
import static org.sonatype.nexus.repository.storage.AssetEntityAdapter.P_ASSET_KIND;
import static org.sonatype.nexus.repository.storage.MetadataNodeEntityAdapter.P_NAME;
import static org.sonatype.nexus.repository.storage.ComponentEntityAdapter.P_VERSION;


@Named
Expand Down Expand Up @@ -85,7 +86,7 @@ public AnsibleGalaxyHostedFacetImpl(AnsibleGalaxyDataAccess ansibleGalaxyDataAcc

this.sorting = new ArrayList<>();
this.sorting.add(SortBuilders.fieldSort(P_NAME).order(SortOrder.ASC));
this.sorting.add(SortBuilders.fieldSort(ComponentEntityAdapter.P_VERSION).order(SortOrder.ASC));
this.sorting.add(SortBuilders.fieldSort(P_VERSION).order(SortOrder.ASC));
}

@TransactionalTouchBlob
Expand Down Expand Up @@ -171,8 +172,8 @@ private AnsibleGalaxyModules getModuleReleasesFromSearchResponse(
list.add(getRepository());
String baseUrlRepo = getRepository().getUrl();
Iterable<Asset> assets;
assets = tx.findAssets(Query.builder().where(P_NAME).like(String.format("%%%s/%s%%", user, module)).build(), list);
long count = tx.countAssets(Query.builder().where(P_NAME).like(String.format("%%%s/%s%%", user, module)).build(), list);
assets = tx.findAssets(Query.builder().where(P_NAME).like(String.format("%%%s-%s%%", user, module)).build(), list);
long count = tx.countAssets(Query.builder().where(P_NAME).like(String.format("%%%s-%s%%", user, module)).build(), list);

AnsibleGalaxyModules releases = moduleBuilder.parse(count, count, 0, context);
for (Asset asset : assets) {
Expand All @@ -194,14 +195,20 @@ public Content moduleByNameAndVersion(Context context, String user, String modul
list.add(getRepository());
String baseUrlRepo = getRepository().getUrl();
Asset asset = null;
String asset_version = null;
Iterable<Asset> assets;
assets = tx.findAssets(Query.builder().where(P_NAME).like(String.format("%%%s/%s%%", user, module)).build(), list);
assets = tx.findAssets(Query
.builder()
.where(P_NAME).like(String.format("%%%s-%s%%", user, module, version))
.build(), list);
for (Asset assetX : assets) {
asset = assetX;
break;
asset_version = (String) asset.formatAttributes().get("version");
if ( asset_version.equals(version) ) { break; }
}

AnsibleGalaxyModule result = new AnsibleGalaxyModule();
result.setVersion(version)
result.setVersion(asset_version)
.setHref(ansibleGalaxyPathUtils.parseHref(baseUrlRepo, asset.attributes().child("ansiblegalaxy").get("name").toString(), asset.attributes().child("ansiblegalaxy").get("version").toString()))
.setDownload_url(ansibleGalaxyPathUtils.download(baseUrlRepo, user, module, version))
.setNamespace(new AnsibleGalaxyModule.AnsibleGalaxyModuleNamespace().setName(user))
Expand Down Expand Up @@ -273,5 +280,4 @@ private Component findOrCreateComponent(final StorageTx tx,
}
return component;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,6 @@ class AnsibleGalaxyHostedRecipe
.handler(hostedHandlers.collectionVersionDetail)
.create())


builder.route(new Route.Builder().matcher(collectionArtifactMatcher())
.handler(timingHandler)
.handler(securityHandler)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@

import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.HttpRequestBase;
import org.eclipse.aether.util.version.GenericVersionScheme;
import org.eclipse.aether.version.InvalidVersionSpecificationException;
import org.eclipse.aether.version.Version;
import org.slf4j.Logger;
import org.sonatype.goodies.common.Loggers;
import org.sonatype.nexus.plugins.ansiblegalaxy.AssetKind;
Expand Down Expand Up @@ -112,6 +115,8 @@ protected Content getCachedContent(final Context context) {
return getAsset(ansiblegalaxyPathUtils.collectionDetailPath(matcherState));
case COLLECTION_VERSION_LIST:
return getAsset(ansiblegalaxyPathUtils.collectionVersionPagedPath(matcherState));
case COLLECTION_VERSION_LIST_LIMIT:
return getAsset(ansiblegalaxyPathUtils.collectionVersionLimitPath(matcherState));
case COLLECTION_VERSION_DETAIL:
return getAsset(ansiblegalaxyPathUtils.collectionVersionDetailPath(matcherState));
case COLLECTION_ARTIFACT:
Expand Down Expand Up @@ -152,6 +157,8 @@ protected Content store(final Context context, final Content content) throws IOE
return putAsset(context, content, ansiblegalaxyPathUtils.collectionDetailPath(matcherState), assetKind);
case COLLECTION_VERSION_LIST:
return putAsset(context, content, ansiblegalaxyPathUtils.collectionVersionPagedPath(matcherState), assetKind);
case COLLECTION_VERSION_LIST_LIMIT:
return putAsset(context, content, ansiblegalaxyPathUtils.collectionVersionLimitPath(matcherState), assetKind);
case COLLECTION_VERSION_DETAIL:
return putComponent(context, ansiblegalaxyPathUtils.getCollectionAttributes(matcherState), content,
ansiblegalaxyPathUtils.collectionVersionDetailPath(matcherState), assetKind);
Expand Down Expand Up @@ -222,6 +229,26 @@ private InputStream getUpdatedContent(Context context, AssetKind assetKind, Inpu
replacers.add(new JsonPostpendReplacer(downloadUrlFieldName, queryString));
}

return new ReplacerStream(replacers).getReplacedContent(in);
} else if (assetKind == AssetKind.COLLECTION_VERSION_LIST || assetKind == AssetKind.COLLECTION_VERSION_LIST_LIMIT) {

List<Replacer> replacers = new ArrayList<>();

/* API Bug IMHO - https://github.com/ansible/awx/issues/14495 */
replacers.add(new JsonPrependReplacer("first", "/repository/" + getRepository().getName()));
replacers.add(new JsonPrependReplacer("previous", "/repository/" + getRepository().getName()));
replacers.add(new JsonPrependReplacer("last", "/repository/" + getRepository().getName()));
replacers.add(new JsonPrependReplacer("next", "/repository/" + getRepository().getName()));

// If client version < 2.13.9 remove "next" (backward compatibility, reduce options)
String userAgent = context.getRequest().getHeaders().get("User-Agent");
if (userAgent != null && userAgent.startsWith("ansible-galaxy/")) {
if (isUserAgentVersionLowerOrEqual(userAgent, "2.13.9")) {
log.info("Backward compatibility layer in action, mind some versions will be omitted");
replacers.add(new JsonSearchReplacer("next", ""));
}
}

return new ReplacerStream(replacers).getReplacedContent(in);
}

Expand All @@ -230,6 +257,20 @@ private InputStream getUpdatedContent(Context context, AssetKind assetKind, Inpu
return new ReplacerStream(urlReplacer).getReplacedContent(in);
}

private boolean isUserAgentVersionLowerOrEqual(String userAgent, String targetVersionString) {
try {
String version = userAgent.split("/")[1];
GenericVersionScheme versionScheme = new GenericVersionScheme();
Version userAgentVersion = versionScheme.parseVersion(version);
Version targetVersion = versionScheme.parseVersion(targetVersionString);

return userAgentVersion.compareTo(targetVersion) <= 0;
} catch (InvalidVersionSpecificationException e) {
log.info("isUserAgentVersionLowerOrEqual returns an error ({} vs {}), assuming false", userAgent, targetVersionString);
return false;
}
}

private String getModuleName(Context context) {
String path = context.getRequest().getPath().replaceFirst(ROLE_VERSION_URI_SUFFIX, "");
Request roleDetailRequest = new Request.Builder().copy(context.getRequest()).action(HttpMethods.GET).path(path)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,10 @@ class AnsibleGalaxyProxyRecipe
apiInternalsMatcher(),
collectionDetailMatcher(),
collectionVersionListMatcher(),
collectionVersionListMatcherPages(),
collectionVersionDetailMatcher(),
collectionArtifactMatcher(),
collectionArtifactV2Matcher(),
collectionArtifactIhmMatcher(),
roleSearchMatcher(),
roleDetailMatcher(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,17 @@ public JsonSearchReplacer(String jsonFieldName, String search, String replace) {
this.replace = checkNotNull(replace);
}

public JsonSearchReplacer(String jsonFieldName, String replace) {
super(jsonFieldName);
this.search = null;
this.replace = checkNotNull(replace);
}

@Override
protected String getUpdatedContent(JsonNode field) {
if (search == null) {
return replace;
}
return field.asText().replaceAll(search, replace);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ public String page_size(final TokenMatcher.State state) {
return StringUtils.defaultIfBlank(state.getTokens().get("page_size"), "20");
}

public String offset(final TokenMatcher.State state) {
return StringUtils.defaultIfBlank(state.getTokens().get("offset"), "0");
}
public String limit(final TokenMatcher.State state) {
return StringUtils.defaultIfBlank(state.getTokens().get("limit"), "100");
}

public TokenMatcher.State matcherState(final Context context) {
State state = context.getAttributes().require(TokenMatcher.State.class);
log.debug("matched state tokens: {}", state.getTokens());
Expand Down Expand Up @@ -112,6 +119,15 @@ public String collectionVersionPagedPath(final State matcherState) {
return String.format("%s/%s/%s/versions%s.json", COLLECTION_PATH, author, module, page);
}

public String collectionVersionLimitPath(final State matcherState) {
String author = author(matcherState);
String module = module(matcherState);
String offset = offset(matcherState);
String limit = limit(matcherState);

return String.format("%s/%s/%s/versions-offset-%s-%s.json", COLLECTION_PATH, author, module, offset, limit);
}

public String roleSearchPath(final State matcherState) {
String author = author(matcherState);
String module = module(matcherState);
Expand Down Expand Up @@ -156,11 +172,15 @@ public String artifactPath(final String author, final String module, final Strin

public String download(String baseUrlRepo, final String author, final String module, final String version) {

return String.format("%s/download/%s.tar.gz", baseUrlRepo, author + "-" + module + "-" + version);
return String.format("%s/api/v3/plugin/ansible/content/published/collections/artifacts/%s.tar.gz", baseUrlRepo, author + "-" + module + "-" + version);
}

public String collectionArtifactPath(final State matcherState) {
return String.format("%s/%s", COLLECTION_PATH, artifactPath(matcherState));
String author = author(matcherState);
String module = module(matcherState);
String version = version(matcherState);
//return String.format("%s/%s", COLLECTION_PATH, artifactPath(matcherState));
return String.format("api/v3/plugin/ansible/content/published/collections/artifacts/%s-%s-%s.tar.gz", author, module, version);
}

public String parseHref(String baseUrlRepo, final String name, final String version) {
Expand All @@ -169,7 +189,8 @@ public String parseHref(String baseUrlRepo, final String name, final String vers
}

public String collectionArtifactPath(final String author, final String module, final String version) {
return String.format("%s/%s", COLLECTION_PATH, artifactPath(author, module, version));
// return String.format("%s/%s", COLLECTION_PATH, artifactPath(author, module, version));
return String.format("api/v3/plugin/ansible/content/published/collections/artifacts/%s-%s-%s.tar.gz", author, module, version);
}

public String collectionNamePath(final String author, final String module) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public void setUp() {

@Test
public void testCollectionArtifactTar() {
when(request.getPath()).thenReturn("/download/azure-azcollection-1.3.1.tar.gz");
when(request.getPath()).thenReturn("/api/v3/plugin/ansible/content/published/collections/artifacts/azure-azcollection-1.3.1.tar.gz");
Matcher matcher = collectionArtifactMatcher();
boolean matches = matcher.matches(context);
assertTrue(matches);
Expand All @@ -76,7 +76,7 @@ public void testCollectionArtifactTar() {

@Test
public void testCollectionBranchArtifactTar() {
when(request.getPath()).thenReturn("/download/azure-azcollection-master.tar.gz");
when(request.getPath()).thenReturn("/api/v3/plugin/ansible/content/published/collections/artifacts/azure-azcollection-master.tar.gz");
Matcher matcher = collectionArtifactMatcher();
boolean matches = matcher.matches(context);
assertTrue(matches);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ public void roleMetadataPagedPath() {
public void collectionArtifactPath() {
String result = underTest.collectionArtifactPath(state);

assertThat(result, is(equalTo("collection/azure/azcollection/1.2.0/azure-azcollection-1.2.0.tar.gz")));
assertThat(result, is(equalTo("api/v3/plugin/ansible/content/published/collections/artifacts/azure-azcollection-1.2.0.tar.gz")));
}

@Test
Expand Down
Loading