Skip to content

Commit 3442031

Browse files
committed
Add Artifactory release creation.
Closes #81
1 parent 7d95684 commit 3442031

File tree

8 files changed

+456
-18
lines changed

8 files changed

+456
-18
lines changed

src/main/java/org/springframework/data/release/build/MavenArtifact.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import lombok.EqualsAndHashCode;
2121
import lombok.Getter;
2222

23+
import org.springframework.data.release.model.ArtifactCoordinate;
2324
import org.springframework.data.release.model.ArtifactVersion;
2425
import org.springframework.data.release.model.ModuleIteration;
2526
import org.springframework.data.release.model.Project;
@@ -77,4 +78,8 @@ public ArtifactVersion getNextDevelopmentVersion() {
7778
return version.getNextDevelopmentVersion();
7879
}
7980

81+
public ArtifactCoordinate toArtifactCoordinate() {
82+
return ArtifactCoordinate.of(getGroupId(), getArtifactId());
83+
}
84+
8085
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.release.deployment;
17+
18+
import lombok.SneakyThrows;
19+
20+
import java.util.Collections;
21+
import java.util.LinkedHashSet;
22+
import java.util.Map;
23+
import java.util.Set;
24+
25+
import org.springframework.data.release.build.MavenArtifact;
26+
import org.springframework.data.release.model.ArtifactCoordinate;
27+
import org.springframework.data.release.model.ArtifactVersion;
28+
import org.springframework.data.release.model.ModuleIteration;
29+
30+
import com.fasterxml.jackson.databind.ObjectMapper;
31+
32+
/**
33+
* @author Mark Paluch
34+
*/
35+
public class AqlWriter {
36+
37+
private final DeploymentProperties.Authentication targetServer;
38+
39+
private final ObjectMapper objectMapper;
40+
41+
public AqlWriter(DeploymentProperties.Authentication targetServer, ObjectMapper objectMapper) {
42+
this.targetServer = targetServer;
43+
this.objectMapper = objectMapper;
44+
}
45+
46+
/**
47+
* Create an AQL statement to find all associated artifacts with {@link ModuleIteration}.
48+
*
49+
* @param module
50+
* @return
51+
*/
52+
@SneakyThrows
53+
public String createFindAqlStatement(ModuleIteration module) {
54+
55+
MavenArtifact mavenArtifact = new MavenArtifact(module);
56+
Set<Map<String, Map<String, String>>> matches = new LinkedHashSet<>();
57+
58+
matches.add(createAqlFilter(mavenArtifact.toArtifactCoordinate(), module));
59+
60+
module.getProject().doWithAdditionalArtifacts(artifactCoordinate -> {
61+
matches.add(createAqlFilter(artifactCoordinate, module));
62+
});
63+
64+
Map<String, String> repo = Collections.singletonMap("repo", targetServer.getTargetRepository());
65+
Map<String, Object> orMatches = Collections.singletonMap("$or", matches);
66+
67+
return String.format("items.find(%s, %s)", objectMapper.writeValueAsString(repo),
68+
objectMapper.writeValueAsString(orMatches));
69+
}
70+
71+
private static Map<String, Map<String, String>> createAqlFilter(ArtifactCoordinate coordinate,
72+
ModuleIteration module) {
73+
74+
ArtifactVersion version = ArtifactVersion.of(module);
75+
String groupIdPath = coordinate.getGroupId();
76+
String modulePath = String.format("%s/%s/%s", groupIdPath.replace('.', '/'), coordinate.getArtifactId(), version);
77+
78+
return Collections.singletonMap("path", Collections.singletonMap("$match", modulePath));
79+
}
80+
81+
}

src/main/java/org/springframework/data/release/deployment/ArtifactoryClient.java

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,22 @@
2020

2121
import java.io.IOException;
2222
import java.net.URI;
23+
import java.util.LinkedHashMap;
24+
import java.util.Map;
2325
import java.util.function.Consumer;
2426

2527
import org.springframework.data.release.deployment.DeploymentProperties.Authentication;
2628
import org.springframework.data.release.model.ModuleIteration;
2729
import org.springframework.data.release.model.SupportStatusAware;
30+
import org.springframework.data.release.model.TrainIteration;
2831
import org.springframework.data.release.utils.Logger;
32+
import org.springframework.http.HttpEntity;
33+
import org.springframework.http.HttpHeaders;
34+
import org.springframework.http.MediaType;
35+
import org.springframework.http.ResponseEntity;
2936
import org.springframework.util.Assert;
3037
import org.springframework.web.client.HttpClientErrorException;
38+
import org.springframework.web.client.HttpStatusCodeException;
3139
import org.springframework.web.client.RestOperations;
3240

3341
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -41,6 +49,9 @@
4149
@RequiredArgsConstructor
4250
class ArtifactoryClient {
4351

52+
private final static String CREATE_RELEASE_BUNDLE_PATH = "/lifecycle/api/v2/release_bundle?project=spring";
53+
private final static String DISTRIBUTE_RELEASE_BUNDLE_PATH = "/lifecycle/api/v2/distribution/distribute/{releaseBundle}/{version}?project=spring";
54+
4455
private final Logger logger;
4556
private final DeploymentProperties properties;
4657
private final RestOperations operations;
@@ -74,10 +85,7 @@ public void promote(DeploymentInformation information) {
7485

7586
public void verify(SupportStatusAware status) {
7687

77-
URI verificationResource = properties
78-
.getAuthentication(status)
79-
.getServer()
80-
.getVerificationResource();
88+
URI verificationResource = properties.getAuthentication(status).getServer().getVerificationResource();
8189

8290
try {
8391

@@ -110,10 +118,64 @@ private void handle(Consumer<Object> logger, String message, HttpClientErrorExce
110118
}
111119

112120
public void deleteArtifacts(DeploymentInformation information) {
113-
114121
operations.delete(information.getDeleteBuildResource());
115122
}
116123

124+
public void createRelease(String context, ArtifactoryReleaseBundle releaseBundle,
125+
Authentication authentication) {
126+
127+
HttpHeaders headers = new HttpHeaders();
128+
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
129+
headers.add("X-JFrog-Signing-Key-Name", "packagesKey");
130+
HttpEntity<ArtifactoryReleaseBundle> entity = new HttpEntity<>(releaseBundle, headers);
131+
132+
try {
133+
ResponseEntity<Map> response = operations
134+
.postForEntity(authentication.getServer().getUri() + CREATE_RELEASE_BUNDLE_PATH, entity, Map.class);
135+
136+
if (!response.getStatusCode().is2xxSuccessful()) {
137+
logger.warn(context, "Artifactory request failed: %d %s", response.getStatusCode().value(),
138+
response.getBody());
139+
} else {
140+
logger.log(context, "Artifactory request succeeded: %s %s", releaseBundle.getName(),
141+
releaseBundle.getVersion());
142+
}
143+
} catch (HttpStatusCodeException e) {
144+
logger.warn(context, "Artifactory request failed: %d %s", e.getStatusCode().value(),
145+
e.getResponseBodyAsString());
146+
}
147+
}
148+
149+
public void distributeRelease(TrainIteration train, String releaseName, String version,
150+
Authentication authentication) {
151+
152+
HttpHeaders headers = new HttpHeaders();
153+
headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
154+
String body = "{\n" + "\t\"auto_create_missing_repositories\": \"false\",\n" + "\t\"distribution_rules\": [\n"
155+
+ "\t\t{\n" + "\t\t\t\"site_name\": \"JP-SaaS\"\n" + "\t\t}\n" + "\t],\n" + "\t\"modifications\": {\n"
156+
+ "\t\t\"mappings\": [\n" + "\t\t\t{\n" + "\t\t\t\t\"input\": \"spring-enterprise-maven-prod-local/(.*)\",\n"
157+
+ "\t\t\t\t\"output\": \"spring-enterprise/$1\"\n" + "\t\t\t}\n" + "\t\t]\n" + "\t}\n" + "}";
158+
HttpEntity<String> entity = new HttpEntity<>(body, headers);
159+
160+
Map<String, Object> parameters = new LinkedHashMap<>();
161+
parameters.put("releaseBundle", releaseName);
162+
parameters.put("version", version);
163+
164+
try {
165+
ResponseEntity<Map> response = operations
166+
.postForEntity(authentication.getServer().getUri() + DISTRIBUTE_RELEASE_BUNDLE_PATH, entity, Map.class,
167+
parameters);
168+
169+
if (!response.getStatusCode().is2xxSuccessful()) {
170+
logger.warn(train, "Artifactory request failed: %d %s", response.getStatusCode().value(), response.getBody());
171+
} else {
172+
logger.log(train, "Artifactory request succeeded: %s %s", releaseName, version);
173+
}
174+
} catch (HttpStatusCodeException e) {
175+
logger.warn(train, "Artifactory request failed: %d %s", e.getStatusCode().value(), e.getResponseBodyAsString());
176+
}
177+
}
178+
117179
@Value
118180
static class PromotionRequest {
119181
String targetRepo, sourceRepo;

src/main/java/org/springframework/data/release/deployment/ArtifactoryCommands.java

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,17 @@
1717

1818
import lombok.NonNull;
1919
import lombok.RequiredArgsConstructor;
20+
import lombok.SneakyThrows;
2021

2122
import java.util.stream.Stream;
2223

2324
import org.springframework.data.release.CliComponent;
2425
import org.springframework.data.release.TimedCommand;
26+
import org.springframework.data.release.model.ModuleIteration;
2527
import org.springframework.data.release.model.SupportStatus;
28+
import org.springframework.data.release.model.TrainIteration;
2629
import org.springframework.shell.core.annotation.CliCommand;
30+
import org.springframework.shell.core.annotation.CliOption;
2731

2832
/**
2933
* Commands to interact with Artifactory.
@@ -35,11 +39,34 @@
3539
class ArtifactoryCommands extends TimedCommand {
3640

3741
private final @NonNull DeploymentOperations deployment;
42+
private final @NonNull ArtifactoryOperations operations;
3843

3944
@CliCommand(value = "artifactory verify", help = "Verifies authentication at Artifactory.")
4045
public void verify() {
4146

42-
Stream.of(SupportStatus.OSS, SupportStatus.COMMERCIAL)
43-
.forEach(deployment::verifyAuthentication);
47+
Stream.of(SupportStatus.OSS, SupportStatus.COMMERCIAL).forEach(deployment::verifyAuthentication);
48+
}
49+
50+
@CliCommand(value = "artifactory release create")
51+
@SneakyThrows
52+
public void createArtifactoryReleases(@CliOption(key = "", mandatory = true) TrainIteration trainIteration) {
53+
54+
for (ModuleIteration moduleIteration : trainIteration) {
55+
operations.createArtifactoryRelease(moduleIteration);
56+
}
57+
58+
// aggregator creation requires a bit of time
59+
// otherwise we will see 16:19:04 "message" : "Release Bundle path not found:
60+
// spring-release-bundles-v2/TNZ-spring-data-rest-commercial/4.0.15/release-bundle.json.evd"
61+
Thread.sleep(2000);
62+
63+
operations.createArtifactoryReleaseAggregator(trainIteration);
64+
65+
}
66+
67+
@CliCommand(value = "artifactory release distribute")
68+
@SneakyThrows
69+
public void distributeArtifactoryReleases(@CliOption(key = "", mandatory = true) TrainIteration trainIteration) {
70+
operations.distributeArtifactoryReleaseAggregator(trainIteration);
4471
}
4572
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.release.deployment;
17+
18+
import lombok.SneakyThrows;
19+
20+
import java.util.Collections;
21+
import java.util.List;
22+
import java.util.Locale;
23+
import java.util.stream.Collectors;
24+
25+
import org.springframework.data.release.model.ArtifactVersion;
26+
import org.springframework.data.release.model.ModuleIteration;
27+
import org.springframework.data.release.model.Projects;
28+
import org.springframework.data.release.model.TrainIteration;
29+
import org.springframework.stereotype.Component;
30+
31+
import com.fasterxml.jackson.databind.ObjectMapper;
32+
33+
/**
34+
* @author Mark Paluch
35+
*/
36+
@Component
37+
class ArtifactoryOperations {
38+
39+
private final ObjectMapper objectMapper = new ObjectMapper();
40+
private final AqlWriter aqlWriter;
41+
private final DeploymentProperties.Authentication authentication;
42+
private final ArtifactoryClient client;
43+
44+
public ArtifactoryOperations(DeploymentProperties properties, ArtifactoryClient client) {
45+
this.authentication = properties.getCommercial();
46+
this.aqlWriter = new AqlWriter(authentication, objectMapper);
47+
this.client = client;
48+
}
49+
50+
@SneakyThrows
51+
public void createArtifactoryRelease(ModuleIteration module) {
52+
53+
ArtifactoryReleaseBundle releaseBundle = createReleaseBundle(module);
54+
client.createRelease(module.getProject().getName(), releaseBundle, authentication);
55+
}
56+
57+
@SneakyThrows
58+
public void createArtifactoryReleaseAggregator(TrainIteration train) {
59+
60+
ModuleIteration bom = train.getModule(Projects.BOM);
61+
String releaseName = "TNZ-spring-data-commercial-release";
62+
String version = ArtifactVersion.of(bom).toString();
63+
64+
List<ArtifactoryReleaseBundle> releaseBundles = train.stream().map(this::createReleaseBundleRef)
65+
.collect(Collectors.toList());
66+
ArtifactoryReleaseBundle aggregator = new ArtifactoryReleaseBundle(releaseName, version, null, null,
67+
"release_bundles", Collections.singletonMap("release_bundles", releaseBundles));
68+
69+
client.createRelease(train.toString(), aggregator, authentication);
70+
}
71+
72+
public void distributeArtifactoryReleaseAggregator(TrainIteration train) {
73+
74+
ModuleIteration bom = train.getModule(Projects.BOM);
75+
String releaseName = "TNZ-spring-data-commercial-release";
76+
String version = ArtifactVersion.of(bom).toString();
77+
78+
client.distributeRelease(train, releaseName, version, authentication);
79+
}
80+
81+
ArtifactoryReleaseBundle createReleaseBundle(ModuleIteration module) {
82+
83+
String releaseName = getReleaseName(module);
84+
String version = ArtifactVersion.of(module).toString();
85+
String aql = aqlWriter.createFindAqlStatement(module);
86+
87+
return new ArtifactoryReleaseBundle(releaseName, version, null, null, "aql", Collections.singletonMap("aql", aql));
88+
}
89+
90+
ArtifactoryReleaseBundle createReleaseBundleRef(ModuleIteration module) {
91+
92+
String releaseName = getReleaseName(module);
93+
String version = ArtifactVersion.of(module).toString();
94+
String aql = aqlWriter.createFindAqlStatement(module);
95+
96+
return new ArtifactoryReleaseBundle(releaseName, version, "spring", "spring-release-bundles-v2", null, null);
97+
}
98+
99+
private static String getReleaseName(ModuleIteration module) {
100+
String projectName = module.getProject().getName().toLowerCase(Locale.ROOT);
101+
return String.format("TNZ-spring-data-%s-commercial", projectName);
102+
}
103+
104+
}

0 commit comments

Comments
 (0)