Add Artifactory release creation.

Closes #81
This commit is contained in:
Mark Paluch
2024-05-06 09:40:02 +02:00
parent 7d95684562
commit 34420310f3
8 changed files with 456 additions and 18 deletions

View File

@@ -20,6 +20,7 @@ import static org.springframework.data.release.model.Projects.*;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import org.springframework.data.release.model.ArtifactCoordinate;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Project;
@@ -77,4 +78,8 @@ public class MavenArtifact {
return version.getNextDevelopmentVersion();
}
public ArtifactCoordinate toArtifactCoordinate() {
return ArtifactCoordinate.of(getGroupId(), getArtifactId());
}
}

View File

@@ -0,0 +1,81 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.deployment;
import lombok.SneakyThrows;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import org.springframework.data.release.build.MavenArtifact;
import org.springframework.data.release.model.ArtifactCoordinate;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.ModuleIteration;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @author Mark Paluch
*/
public class AqlWriter {
private final DeploymentProperties.Authentication targetServer;
private final ObjectMapper objectMapper;
public AqlWriter(DeploymentProperties.Authentication targetServer, ObjectMapper objectMapper) {
this.targetServer = targetServer;
this.objectMapper = objectMapper;
}
/**
* Create an AQL statement to find all associated artifacts with {@link ModuleIteration}.
*
* @param module
* @return
*/
@SneakyThrows
public String createFindAqlStatement(ModuleIteration module) {
MavenArtifact mavenArtifact = new MavenArtifact(module);
Set<Map<String, Map<String, String>>> matches = new LinkedHashSet<>();
matches.add(createAqlFilter(mavenArtifact.toArtifactCoordinate(), module));
module.getProject().doWithAdditionalArtifacts(artifactCoordinate -> {
matches.add(createAqlFilter(artifactCoordinate, module));
});
Map<String, String> repo = Collections.singletonMap("repo", targetServer.getTargetRepository());
Map<String, Object> orMatches = Collections.singletonMap("$or", matches);
return String.format("items.find(%s, %s)", objectMapper.writeValueAsString(repo),
objectMapper.writeValueAsString(orMatches));
}
private static Map<String, Map<String, String>> createAqlFilter(ArtifactCoordinate coordinate,
ModuleIteration module) {
ArtifactVersion version = ArtifactVersion.of(module);
String groupIdPath = coordinate.getGroupId();
String modulePath = String.format("%s/%s/%s", groupIdPath.replace('.', '/'), coordinate.getArtifactId(), version);
return Collections.singletonMap("path", Collections.singletonMap("$match", modulePath));
}
}

View File

@@ -20,14 +20,22 @@ import lombok.Value;
import java.io.IOException;
import java.net.URI;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Consumer;
import org.springframework.data.release.deployment.DeploymentProperties.Authentication;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.SupportStatusAware;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.data.release.utils.Logger;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.Assert;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpStatusCodeException;
import org.springframework.web.client.RestOperations;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -41,6 +49,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
@RequiredArgsConstructor
class ArtifactoryClient {
private final static String CREATE_RELEASE_BUNDLE_PATH = "/lifecycle/api/v2/release_bundle?project=spring";
private final static String DISTRIBUTE_RELEASE_BUNDLE_PATH = "/lifecycle/api/v2/distribution/distribute/{releaseBundle}/{version}?project=spring";
private final Logger logger;
private final DeploymentProperties properties;
private final RestOperations operations;
@@ -74,10 +85,7 @@ class ArtifactoryClient {
public void verify(SupportStatusAware status) {
URI verificationResource = properties
.getAuthentication(status)
.getServer()
.getVerificationResource();
URI verificationResource = properties.getAuthentication(status).getServer().getVerificationResource();
try {
@@ -110,10 +118,64 @@ class ArtifactoryClient {
}
public void deleteArtifacts(DeploymentInformation information) {
operations.delete(information.getDeleteBuildResource());
}
public void createRelease(String context, ArtifactoryReleaseBundle releaseBundle,
Authentication authentication) {
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
headers.add("X-JFrog-Signing-Key-Name", "packagesKey");
HttpEntity<ArtifactoryReleaseBundle> entity = new HttpEntity<>(releaseBundle, headers);
try {
ResponseEntity<Map> response = operations
.postForEntity(authentication.getServer().getUri() + CREATE_RELEASE_BUNDLE_PATH, entity, Map.class);
if (!response.getStatusCode().is2xxSuccessful()) {
logger.warn(context, "Artifactory request failed: %d %s", response.getStatusCode().value(),
response.getBody());
} else {
logger.log(context, "Artifactory request succeeded: %s %s", releaseBundle.getName(),
releaseBundle.getVersion());
}
} catch (HttpStatusCodeException e) {
logger.warn(context, "Artifactory request failed: %d %s", e.getStatusCode().value(),
e.getResponseBodyAsString());
}
}
public void distributeRelease(TrainIteration train, String releaseName, String version,
Authentication authentication) {
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
String body = "{\n" + "\t\"auto_create_missing_repositories\": \"false\",\n" + "\t\"distribution_rules\": [\n"
+ "\t\t{\n" + "\t\t\t\"site_name\": \"JP-SaaS\"\n" + "\t\t}\n" + "\t],\n" + "\t\"modifications\": {\n"
+ "\t\t\"mappings\": [\n" + "\t\t\t{\n" + "\t\t\t\t\"input\": \"spring-enterprise-maven-prod-local/(.*)\",\n"
+ "\t\t\t\t\"output\": \"spring-enterprise/$1\"\n" + "\t\t\t}\n" + "\t\t]\n" + "\t}\n" + "}";
HttpEntity<String> entity = new HttpEntity<>(body, headers);
Map<String, Object> parameters = new LinkedHashMap<>();
parameters.put("releaseBundle", releaseName);
parameters.put("version", version);
try {
ResponseEntity<Map> response = operations
.postForEntity(authentication.getServer().getUri() + DISTRIBUTE_RELEASE_BUNDLE_PATH, entity, Map.class,
parameters);
if (!response.getStatusCode().is2xxSuccessful()) {
logger.warn(train, "Artifactory request failed: %d %s", response.getStatusCode().value(), response.getBody());
} else {
logger.log(train, "Artifactory request succeeded: %s %s", releaseName, version);
}
} catch (HttpStatusCodeException e) {
logger.warn(train, "Artifactory request failed: %d %s", e.getStatusCode().value(), e.getResponseBodyAsString());
}
}
@Value
static class PromotionRequest {
String targetRepo, sourceRepo;

View File

@@ -17,13 +17,17 @@ package org.springframework.data.release.deployment;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import java.util.stream.Stream;
import org.springframework.data.release.CliComponent;
import org.springframework.data.release.TimedCommand;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.SupportStatus;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.shell.core.annotation.CliCommand;
import org.springframework.shell.core.annotation.CliOption;
/**
* Commands to interact with Artifactory.
@@ -35,11 +39,34 @@ import org.springframework.shell.core.annotation.CliCommand;
class ArtifactoryCommands extends TimedCommand {
private final @NonNull DeploymentOperations deployment;
private final @NonNull ArtifactoryOperations operations;
@CliCommand(value = "artifactory verify", help = "Verifies authentication at Artifactory.")
public void verify() {
Stream.of(SupportStatus.OSS, SupportStatus.COMMERCIAL)
.forEach(deployment::verifyAuthentication);
Stream.of(SupportStatus.OSS, SupportStatus.COMMERCIAL).forEach(deployment::verifyAuthentication);
}
@CliCommand(value = "artifactory release create")
@SneakyThrows
public void createArtifactoryReleases(@CliOption(key = "", mandatory = true) TrainIteration trainIteration) {
for (ModuleIteration moduleIteration : trainIteration) {
operations.createArtifactoryRelease(moduleIteration);
}
// aggregator creation requires a bit of time
// otherwise we will see 16:19:04 "message" : "Release Bundle path not found:
// spring-release-bundles-v2/TNZ-spring-data-rest-commercial/4.0.15/release-bundle.json.evd"
Thread.sleep(2000);
operations.createArtifactoryReleaseAggregator(trainIteration);
}
@CliCommand(value = "artifactory release distribute")
@SneakyThrows
public void distributeArtifactoryReleases(@CliOption(key = "", mandatory = true) TrainIteration trainIteration) {
operations.distributeArtifactoryReleaseAggregator(trainIteration);
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.deployment;
import lombok.SneakyThrows;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.stream.Collectors;
import org.springframework.data.release.model.ArtifactVersion;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.TrainIteration;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* @author Mark Paluch
*/
@Component
class ArtifactoryOperations {
private final ObjectMapper objectMapper = new ObjectMapper();
private final AqlWriter aqlWriter;
private final DeploymentProperties.Authentication authentication;
private final ArtifactoryClient client;
public ArtifactoryOperations(DeploymentProperties properties, ArtifactoryClient client) {
this.authentication = properties.getCommercial();
this.aqlWriter = new AqlWriter(authentication, objectMapper);
this.client = client;
}
@SneakyThrows
public void createArtifactoryRelease(ModuleIteration module) {
ArtifactoryReleaseBundle releaseBundle = createReleaseBundle(module);
client.createRelease(module.getProject().getName(), releaseBundle, authentication);
}
@SneakyThrows
public void createArtifactoryReleaseAggregator(TrainIteration train) {
ModuleIteration bom = train.getModule(Projects.BOM);
String releaseName = "TNZ-spring-data-commercial-release";
String version = ArtifactVersion.of(bom).toString();
List<ArtifactoryReleaseBundle> releaseBundles = train.stream().map(this::createReleaseBundleRef)
.collect(Collectors.toList());
ArtifactoryReleaseBundle aggregator = new ArtifactoryReleaseBundle(releaseName, version, null, null,
"release_bundles", Collections.singletonMap("release_bundles", releaseBundles));
client.createRelease(train.toString(), aggregator, authentication);
}
public void distributeArtifactoryReleaseAggregator(TrainIteration train) {
ModuleIteration bom = train.getModule(Projects.BOM);
String releaseName = "TNZ-spring-data-commercial-release";
String version = ArtifactVersion.of(bom).toString();
client.distributeRelease(train, releaseName, version, authentication);
}
ArtifactoryReleaseBundle createReleaseBundle(ModuleIteration module) {
String releaseName = getReleaseName(module);
String version = ArtifactVersion.of(module).toString();
String aql = aqlWriter.createFindAqlStatement(module);
return new ArtifactoryReleaseBundle(releaseName, version, null, null, "aql", Collections.singletonMap("aql", aql));
}
ArtifactoryReleaseBundle createReleaseBundleRef(ModuleIteration module) {
String releaseName = getReleaseName(module);
String version = ArtifactVersion.of(module).toString();
String aql = aqlWriter.createFindAqlStatement(module);
return new ArtifactoryReleaseBundle(releaseName, version, "spring", "spring-release-bundles-v2", null, null);
}
private static String getReleaseName(ModuleIteration module) {
String projectName = module.getProject().getName().toLowerCase(Locale.ROOT);
return String.format("TNZ-spring-data-%s-commercial", projectName);
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.deployment;
import lombok.Value;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* @author Mark Paluch
*/
@Value
@JsonInclude(value = JsonInclude.Include.NON_NULL)
class ArtifactoryReleaseBundle {
@JsonProperty("release_bundle_name") String name;
@JsonProperty("release_bundle_version") String version;
@JsonProperty("project_key") String projectKey;
@JsonProperty("repository_key") String repositoryKey;
@JsonProperty("source_type") String source_type;
@JsonProperty("source") Object source;
}

View File

@@ -53,19 +53,21 @@ public class Projects {
BUILD = new Project("DATABUILD", "Build", Tracker.GITHUB) //
.withAdditionalArtifacts(ArtifactCoordinates.forGroupId("org.springframework.data.build")
.artifacts("spring-data-build-parent", "spring-data-build-resources")
.artifacts("spring-data-build", "spring-data-parent", "spring-data-build-parent",
"spring-data-build-resources")
.and(ArtifactCoordinate.of("org.springframework.data", "spring-data-releasetrain")));
COMMONS = new Project("DATACMNS", "Commons", Tracker.GITHUB).withDependencies(BUILD);
JPA = new Project("DATAJPA", "JPA", Tracker.GITHUB) //
.withDependencies(COMMONS) //
.withAdditionalArtifacts(ArtifactCoordinates.SPRING_DATA.artifacts("spring-data-envers"));
.withAdditionalArtifacts(ArtifactCoordinates.SPRING_DATA.artifacts("spring-data-envers",
"spring-data-jpa-parent", "spring-data-jpa-distribution"));
MONGO_DB = new Project("DATAMONGO", "MongoDB", Tracker.GITHUB) //
.withDependencies(COMMONS) //
.withAdditionalArtifacts(
ArtifactCoordinates.SPRING_DATA.artifacts("spring-data-mongodb-cross-store", "spring-data-mongodb-log4j"));
.withAdditionalArtifacts(ArtifactCoordinates.SPRING_DATA.artifacts("spring-data-mongodb-parent",
"spring-data-mongodb-distribution"));
NEO4J = new Project("DATAGRAPH", "Neo4j", Tracker.GITHUB).withDependencies(COMMONS)
.withMaintainer(ProjectMaintainer.COMMUNITY);
@@ -79,7 +81,8 @@ public class Projects {
CASSANDRA = new Project("DATACASS", "Cassandra", Tracker.GITHUB) //
.withDependencies(COMMONS) //
.withAdditionalArtifacts(ArtifactCoordinates.SPRING_DATA.artifacts("spring-cql"))
.withAdditionalArtifacts(ArtifactCoordinates.SPRING_DATA.artifacts("spring-data-cassandra-parent",
"spring-data-cassandra-distribution"))
.withFullName("Spring Data for Apache Cassandra");
ELASTICSEARCH = new Project("DATAES", "Elasticsearch", Tracker.GITHUB).withDependencies(COMMONS)
@@ -89,13 +92,13 @@ public class Projects {
REDIS = new Project("DATAREDIS", "Redis", Tracker.GITHUB).withDependencies(KEY_VALUE);
JDBC = new Project("DATAJDBC", "JDBC", Tracker.GITHUB)
.withAdditionalArtifacts(
ArtifactCoordinates.SPRING_DATA.artifacts("spring-data-relational", "spring-data-jdbc"))
JDBC = new Project("DATAJDBC", "JDBC", Tracker.GITHUB).withAdditionalArtifacts(ArtifactCoordinates.SPRING_DATA
.artifacts("spring-data-relational", "spring-data-relational-parent", "spring-data-jdbc"))
.withDependencies(COMMONS);
RELATIONAL = new Project("DATAJDBC", "Relational", Tracker.GITHUB).withAdditionalArtifacts(
ArtifactCoordinates.SPRING_DATA.artifacts("spring-data-relational", "spring-data-jdbc", "spring-data-r2dbc"))
RELATIONAL = new Project("DATAJDBC", "Relational", Tracker.GITHUB)
.withAdditionalArtifacts(ArtifactCoordinates.SPRING_DATA.artifacts("spring-data-relational",
"spring-data-relational-parent", "spring-data-jdbc", "spring-data-jdbc-distribution", "spring-data-r2dbc"))
.withDependencies(COMMONS);
R2DBC = new Project("DATAR2DBC", "R2DBC", Tracker.GITHUB).withDependencies(COMMONS, JDBC);
@@ -109,7 +112,8 @@ public class Projects {
REST = new Project("DATAREST", "REST", Tracker.GITHUB) //
.withDependencies(JPA, MONGO_DB, NEO4J, CASSANDRA, KEY_VALUE) //
.withAdditionalArtifacts(ArtifactCoordinates.SPRING_DATA //
.artifacts("spring-data-rest-core", "spring-data-rest-core", "spring-data-rest-hal-browser",
.artifacts("spring-data-rest-parent", "spring-data-rest-core", "spring-data-rest-core",
"spring-data-rest-webmvc", "spring-data-rest-hal-browser", "spring-data-rest-distribution",
"spring-data-rest-hal-explorer"));
ENVERS = new Project("DATAENV", "Envers", Tracker.GITHUB).withDependencies(JPA);

View File

@@ -0,0 +1,114 @@
/*
* Copyright 2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.release.deployment;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.util.Map;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.data.release.model.Iteration;
import org.springframework.data.release.model.ModuleIteration;
import org.springframework.data.release.model.Projects;
import org.springframework.data.release.model.ReleaseTrains;
import org.springframework.data.release.model.TrainIteration;
/**
* Unit tests for {@link ArtifactoryOperations}.
*
* @author Mark Paluch
*/
class ArtifactoryOperationsUnitTests {
DeploymentProperties properties = new DeploymentProperties();
ArtifactoryOperations operations;
@BeforeEach
void setUp() {
DeploymentProperties.Authentication authentication = new DeploymentProperties.Authentication();
authentication.setProject("spring");
authentication.setTargetRepository("spring-enterprise-maven-prod-local");
properties.setCommercial(authentication);
operations = new ArtifactoryOperations(properties, mock(ArtifactoryClient.class));
}
@Test
void shouldCreateReleaseBundle() {
TrainIteration iteration = ReleaseTrains.ULLMAN.getIteration(Iteration.SR1);
ModuleIteration module = iteration.getModule(Projects.COMMONS);
ArtifactoryReleaseBundle releaseBundle = operations.createReleaseBundle(module);
assertThat(releaseBundle.getName()).isEqualTo("TNZ-spring-data-commons-commercial");
assertThat(releaseBundle.getVersion()).isEqualTo("3.1.1");
assertThat(releaseBundle.getSource_type()).isEqualTo("aql");
assertThat(releaseBundle.getSource()).isInstanceOf(Map.class);
String aql = ((Map<String, String>) releaseBundle.getSource()).get("aql");
assertThat(aql).contains(
"items.find({\"repo\":\"spring-enterprise-maven-prod-local\"}, {\"$or\":[{\"path\":{\"$match\":\"org/springframework/data/spring-data-commons/3.1.1\"}}]})");
}
@Test
void shouldCreateMultiModuleBundle() {
TrainIteration iteration = ReleaseTrains.ULLMAN.getIteration(Iteration.SR1);
ModuleIteration module = iteration.getModule(Projects.RELATIONAL);
ArtifactoryReleaseBundle releaseBundle = operations.createReleaseBundle(module);
assertThat(releaseBundle.getName()).isEqualTo("TNZ-spring-data-relational-commercial");
assertThat(releaseBundle.getVersion()).isEqualTo("3.1.1");
assertThat(releaseBundle.getSource_type()).isEqualTo("aql");
assertThat(releaseBundle.getSource()).isInstanceOf(Map.class);
String aql = ((Map<String, String>) releaseBundle.getSource()).get("aql");
assertThat(aql).contains("items.find({\"repo\":\"spring-enterprise-maven-prod-local\"}");
assertThat(aql).contains("\"$match\":\"org/springframework/data/spring-data-relational-parent/3.1.1");
assertThat(aql).contains("\"$match\":\"org/springframework/data/spring-data-jdbc/3.1.1");
assertThat(aql).contains("\"$match\":\"org/springframework/data/spring-data-r2dbc/3.1.1");
assertThat(aql).contains("\"$match\":\"org/springframework/data/spring-data-jdbc-distribution/3.1.1");
}
@Test
void shouldCreateBomBundle() {
TrainIteration iteration = ReleaseTrains.ULLMAN.getIteration(Iteration.SR1);
ModuleIteration module = iteration.getModule(Projects.BOM);
ArtifactoryReleaseBundle releaseBundle = operations.createReleaseBundle(module);
assertThat(releaseBundle.getName()).isEqualTo("TNZ-spring-data-bom-commercial");
assertThat(releaseBundle.getVersion()).isEqualTo("2023.0.1");
assertThat(releaseBundle.getSource_type()).isEqualTo("aql");
assertThat(releaseBundle.getSource()).isInstanceOf(Map.class);
String aql = ((Map<String, String>) releaseBundle.getSource()).get("aql");
assertThat(aql).contains(
"items.find({\"repo\":\"spring-enterprise-maven-prod-local\"}, {\"$or\":[{\"path\":{\"$match\":\"org/springframework/data/spring-data-bom/2023.0.1\"}}]})");
}
}