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

Support for automatically generating SemVer branches #5000

Merged
merged 3 commits into from
Aug 9, 2024
Merged
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
19 changes: 9 additions & 10 deletions app/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-logging-json</artifactId>
</dependency>

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-fault-tolerance</artifactId>
Expand All @@ -160,21 +159,23 @@
<artifactId>quarkus-jdbc-mssql</artifactId>
</dependency>

<!-- Third Party Libraries -->
<dependency>
<groupId>io.strimzi</groupId>
<artifactId>kafka-oauth-client</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.jgit</groupId>
<artifactId>org.eclipse.jgit</artifactId>
</dependency>

<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<groupId>org.semver4j</groupId>
<artifactId>semver4j</artifactId>
</dependency>
<dependency>
<groupId>io.strimzi</groupId>
<artifactId>kafka-oauth-client</artifactId>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>

<!-- Third Party Libraries -->
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
Expand All @@ -198,13 +199,11 @@
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>${snakeyaml.version}</version>
</dependency>

<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.apicurio.registry.semver;

import io.apicurio.common.apps.config.Dynamic;
import io.apicurio.common.apps.config.Info;
import jakarta.inject.Singleton;
import org.eclipse.microprofile.config.inject.ConfigProperty;

import java.util.function.Supplier;

@Singleton
public class SemVerConfigProperties {

@Dynamic(label = "Ensure all version numbers are 'semver' compatible", description = "When enabled, validate that all artifact versions conform to Semantic Versioning 2 format (https://semver.org).")
@ConfigProperty(name = "apicurio.semver.validation.enabled", defaultValue = "false")
@Info(category = "semver", description = "Validate that all artifact versions conform to Semantic Versioning 2 format (https://semver.org).", availableSince = "3.0.0")
public Supplier<Boolean> validationEnabled;

@Dynamic(label = "Automatically create semver branches", description = "When enabled, automatically create or update branches for major ('A.x') and minor ('A.B.x') artifact versions.")
@ConfigProperty(name = "apicurio.semver.branching.enabled", defaultValue = "false")
@Info(category = "semver", description = "Automatically create or update branches for major ('A.x') and minor ('A.B.x') artifact versions.", availableSince = "3.0.0")
public Supplier<Boolean> branchingEnabled;

@Dynamic(label = "Coerce invalid semver versions", description = "When enabled and automatically creating semver branches, invalid versions will be coerced to Semantic Versioning 2 format (https://semver.org) if possible.", requires = "apicurio.semver.branching.enabled=true")
@ConfigProperty(name = "apicurio.semver.branching.coerce", defaultValue = "false")
@Info(category = "semver", description = "If true, invalid versions will be coerced to Semantic Versioning 2 format (https://semver.org) if possible.", availableSince = "3.0.0")
public Supplier<Boolean> coerceInvalidVersions;

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,12 @@ protected void initialize(AgroalDataSource dataSource, String dataSourceId, Logg
public <R, X extends Exception> R withHandle(HandleCallback<R, X> callback) throws X {
LocalState state = state();
try {
// Create a new handle, or throw if one already exists (only one handle allowed at a time)
// Create a new handle if necessary. Increment the "level" if a handle already exists.
if (state.handle == null) {
state.handle = new HandleImpl(dataSource.getConnection());
state.level = 0;
} else {
throw new RegistryStorageException("Attempt to acquire a nested DB Handle.");
state.level++;
}

// Invoke the callback with the handle. This will either return a value (success)
Expand All @@ -54,32 +55,39 @@ public <R, X extends Exception> R withHandle(HandleCallback<R, X> callback) thro
}
throw e;
} finally {
// Commit or rollback the transaction
try {
if (state.handle != null) {
if (state.handle.isRollback()) {
log.trace("Rollback: {} #{}", state.handle.getConnection(),
state.handle.getConnection().hashCode());
state.handle.getConnection().rollback();
} else {
log.trace("Commit: {} #{}", state.handle.getConnection(),
state.handle.getConnection().hashCode());
state().handle.getConnection().commit();
if (state.level > 0) {
log.trace("Exiting nested call (level {}): {} #{}", state().level,
state().handle.getConnection(), state().handle.getConnection().hashCode());
state.level--;
} else {
// Commit or rollback the transaction
try {
if (state.handle != null) {
if (state.handle.isRollback()) {
log.trace("Rollback: {} #{}", state.handle.getConnection(),
state.handle.getConnection().hashCode());
state.handle.getConnection().rollback();
} else {
log.trace("Commit: {} #{}", state.handle.getConnection(),
state.handle.getConnection().hashCode());
state().handle.getConnection().commit();
}
}
} catch (Exception e) {
log.error("Could not release database connection/transaction", e);
}
} catch (Exception e) {
log.error("Could not release database connection/transaction", e);
}

// Close the connection
try {
if (state.handle != null) {
state.handle.close();
state.handle = null;
// Close the connection
try {
if (state.handle != null) {
state.handle.close();
state.handle = null;
state.level = 0;
}
} catch (Exception ex) {
// Nothing we can do
log.error("Could not close a database connection.", ex);
}
} catch (Exception ex) {
// Nothing we can do
log.error("Could not close a database connection.", ex);
}
}
}
Expand Down Expand Up @@ -109,5 +117,6 @@ private LocalState state() {

private static class LocalState {
HandleImpl handle;
int level = 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import io.apicurio.registry.model.GA;
import io.apicurio.registry.model.GAV;
import io.apicurio.registry.model.VersionId;
import io.apicurio.registry.semver.SemVerConfigProperties;
import io.apicurio.registry.storage.RegistryStorage;
import io.apicurio.registry.storage.StorageBehaviorProperties;
import io.apicurio.registry.storage.StorageEvent;
Expand Down Expand Up @@ -113,9 +114,11 @@
import io.quarkus.security.identity.SecurityIdentity;
import jakarta.enterprise.event.Event;
import jakarta.inject.Inject;
import jakarta.validation.ValidationException;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.semver4j.Semver;
import org.slf4j.Logger;

import java.sql.ResultSet;
Expand Down Expand Up @@ -187,6 +190,9 @@ public abstract class AbstractSqlRegistryStorage implements RegistryStorage {
@Inject
RegistryStorageContentUtils utils;

@Inject
SemVerConfigProperties semVerConfigProps;

protected SqlStatements sqlStatements() {
return sqlStatements;
}
Expand Down Expand Up @@ -554,7 +560,6 @@ private ArtifactVersionMetaDataDto createArtifactVersionRaw(Handle handle, boole
.bind(12, contentId).execute();

gav = new GAV(groupId, artifactId, finalVersion1);
createOrUpdateBranchRaw(handle, gav, BranchId.LATEST, true);
} else {
handle.createUpdate(sqlStatements.insertVersion(false)).bind(0, globalId)
.bind(1, normalizeGroupId(groupId)).bind(2, artifactId).bind(3, version)
Expand All @@ -571,7 +576,6 @@ private ArtifactVersionMetaDataDto createArtifactVersionRaw(Handle handle, boole
}

gav = getGAVByGlobalId(handle, globalId);
createOrUpdateBranchRaw(handle, gav, BranchId.LATEST, true);
}

// Insert labels into the "version_labels" table
Expand All @@ -583,6 +587,10 @@ private ArtifactVersionMetaDataDto createArtifactVersionRaw(Handle handle, boole
});
}

// Update system generated branches
createOrUpdateBranchRaw(handle, gav, BranchId.LATEST, true);
createOrUpdateSemverBranches(handle, gav);

// Create any user defined branches
if (branches != null && !branches.isEmpty()) {
branches.forEach(branch -> {
Expand All @@ -595,6 +603,54 @@ private ArtifactVersionMetaDataDto createArtifactVersionRaw(Handle handle, boole
.map(ArtifactVersionMetaDataDtoMapper.instance).one();
}

/**
* If SemVer support is enabled, create (or update) the automatic system generated semantic versioning
* branches.
*
* @param handle
* @param gav
*/
private void createOrUpdateSemverBranches(Handle handle, GAV gav) {
boolean validationEnabled = semVerConfigProps.validationEnabled.get();
boolean branchingEnabled = semVerConfigProps.branchingEnabled.get();
boolean coerceInvalidVersions = semVerConfigProps.coerceInvalidVersions.get();

// Validate the version if validation is enabled.
if (validationEnabled) {
Semver semver = Semver.parse(gav.getRawVersionId());
if (semver == null) {
throw new ValidationException("Version '" + gav.getRawVersionId()
+ "' does not conform to Semantic Versioning 2 format.");
}
}

// Create branches if branching is enabled
if (!branchingEnabled) {
return;
}

Semver semver = null;
if (coerceInvalidVersions) {
semver = Semver.coerce(gav.getRawVersionId());
if (semver == null) {
throw new ValidationException("Version '" + gav.getRawVersionId()
+ "' cannot be coerced to Semantic Versioning 2 format.");
}
} else {
semver = Semver.parse(gav.getRawVersionId());
if (semver == null) {
throw new ValidationException("Version '" + gav.getRawVersionId()
+ "' does not conform to Semantic Versioning 2 format.");
}
}
if (semver == null) {
throw new UnreachableCodeException("Unexpectedly reached unreachable code!");
}
createOrUpdateBranchRaw(handle, gav, new BranchId(semver.getMajor() + ".x"), true);
createOrUpdateBranchRaw(handle, gav, new BranchId(semver.getMajor() + "." + semver.getMinor() + ".x"),
true);
}

/**
* Store the content in the database and return the content ID of the new row. If the content already
* exists, just return the content ID of the existing row.
Expand Down Expand Up @@ -3031,6 +3087,11 @@ public BranchMetaDataDto createBranch(GA ga, BranchId branchId, String descripti

@Override
public void updateBranchMetaData(GA ga, BranchId branchId, EditableBranchMetaDataDto dto) {
BranchMetaDataDto bmd = getBranchMetaData(ga, branchId);
if (bmd.isSystemDefined()) {
throw new NotAllowedException("System generated branches cannot be modified.");
}

String modifiedBy = securityIdentity.getPrincipal().getName();
Date modifiedOn = new Date();
log.debug("Updating metadata for branch {} of {}/{}.", branchId, ga.getRawGroupIdWithNull(),
Expand Down Expand Up @@ -3220,6 +3281,11 @@ public VersionSearchResultsDto getBranchVersions(GA ga, BranchId branchId, int o

@Override
public void appendVersionToBranch(GA ga, BranchId branchId, VersionId version) {
BranchMetaDataDto bmd = getBranchMetaData(ga, branchId);
if (bmd.isSystemDefined()) {
throw new NotAllowedException("System generated branches cannot be modified.");
}

try {
handles.withHandle(handle -> {
appendVersionToBranchRaw(handle, ga, branchId, version);
Expand Down Expand Up @@ -3257,6 +3323,11 @@ private void appendVersionToBranchRaw(Handle handle, GA ga, BranchId branchId, V

@Override
public void replaceBranchVersions(GA ga, BranchId branchId, List<VersionId> versions) {
BranchMetaDataDto bmd = getBranchMetaData(ga, branchId);
if (bmd.isSystemDefined()) {
throw new NotAllowedException("System generated branches cannot be modified.");
}

handles.withHandle(handle -> {
// Delete all previous versions.
handle.createUpdate(sqlStatements.deleteBranchVersions()).bind(0, ga.getRawGroupId())
Expand Down Expand Up @@ -3341,8 +3412,9 @@ private GAV getGAVByGlobalId(Handle handle, long globalId) {

@Override
public void deleteBranch(GA ga, BranchId branchId) {
if (BranchId.LATEST.equals(branchId)) {
throw new NotAllowedException("Artifact branch 'latest' cannot be deleted.");
BranchMetaDataDto bmd = getBranchMetaData(ga, branchId);
if (bmd.isSystemDefined()) {
throw new NotAllowedException("System generated branches cannot be deleted.");
}

handles.withHandleNoException(handle -> {
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ apicurio.authn.basic-client-credentials.enabled.dynamic.allow=${apicurio.config.
apicurio.rest.deletion.group.enabled.dynamic.allow=${apicurio.config.dynamic.allow-all}
apicurio.rest.deletion.artifact.enabled.dynamic.allow=${apicurio.config.dynamic.allow-all}
apicurio.rest.deletion.artifactVersion.enabled.dynamic.allow=${apicurio.config.dynamic.allow-all}

apicurio.semver.validation.enabled.dynamic.allow=${apicurio.config.dynamic.allow-all}
apicurio.semver.branching.enabled.dynamic.allow=${apicurio.config.dynamic.allow-all}
apicurio.semver.branching.coerce.dynamic.allow=${apicurio.config.dynamic.allow-all}

# Error
apicurio.api.errors.include-stack-in-response=false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import io.apicurio.registry.rest.client.models.CreateBranch;
import io.apicurio.registry.rest.client.models.CreateVersion;
import io.apicurio.registry.rest.client.models.EditableBranchMetaData;
import io.apicurio.registry.rest.client.models.Error;
import io.apicurio.registry.rest.client.models.ReplaceBranchVersions;
import io.apicurio.registry.rest.client.models.VersionMetaData;
import io.apicurio.registry.rest.client.models.VersionSearchResults;
Expand Down Expand Up @@ -39,6 +40,13 @@ public void testLatestBranch() throws Exception {
VersionSearchResults versions = clientV3.groups().byGroupId(groupId).artifacts()
.byArtifactId(artifactId).branches().byBranchId("latest").versions().get();
Assertions.assertEquals(2, versions.getCount());

// Not allowed to delete the latest branch.
var error = Assertions.assertThrows(Error.class, () -> {
clientV3.groups().byGroupId(groupId).artifacts().byArtifactId(artifactId).branches()
.byBranchId("latest").delete();
});
Assertions.assertEquals("System generated branches cannot be deleted.", error.getMessageEscaped());
}

@Test
Expand Down
Loading
Loading