데스크탑 앱 추가수정
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package com.goldenchart.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.goldenchart.dto.DesktopAppBuildStatusDto;
|
||||
import com.goldenchart.dto.DesktopAppBuildTriggerDto;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -15,9 +16,16 @@ import org.springframework.web.reactive.function.client.WebClientResponseExcepti
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
import org.springframework.web.util.UriUtils;
|
||||
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
@@ -29,6 +37,7 @@ public class DesktopAppBuildService {
|
||||
|
||||
private final WebClient.Builder webClientBuilder;
|
||||
private final RolePermissionService rolePermissionService;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Value("${goldenchart.desktop-app.jenkins.enabled:true}")
|
||||
private boolean jenkinsEnabled;
|
||||
@@ -195,23 +204,120 @@ public class DesktopAppBuildService {
|
||||
}
|
||||
|
||||
private void triggerJenkinsBuild() {
|
||||
WebClient client = jenkinsClient();
|
||||
String buildUrl = buildTriggerUrl();
|
||||
Map<String, String> crumb = fetchCrumbOptional(client);
|
||||
try {
|
||||
postBuild(client, buildUrl, crumb);
|
||||
} catch (WebClientResponseException.Forbidden e) {
|
||||
if (crumb != null) throw e;
|
||||
postBuild(client, buildUrl, fetchCrumb(client));
|
||||
if (hasBasicAuth()) {
|
||||
try {
|
||||
triggerJenkinsBuildWithSession();
|
||||
return;
|
||||
} catch (ResponseStatusException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.warn("[desktop-build] session trigger failed: {}", e.getMessage());
|
||||
throw new ResponseStatusException(HttpStatus.BAD_GATEWAY,
|
||||
"Jenkins 빌드 요청 실패: " + rootMessage(e));
|
||||
}
|
||||
}
|
||||
triggerJenkinsBuildWebClient();
|
||||
}
|
||||
|
||||
private String buildTriggerUrl() {
|
||||
String url = jobApiPath() + "/build";
|
||||
if (hasTriggerToken()) {
|
||||
url += "?token=" + UriUtils.encodeQueryParam(jenkinsTriggerToken.trim(), StandardCharsets.UTF_8);
|
||||
/** Jenkins CSRF는 세션 쿠키 + crumb 조합이 필요 — java.net.http CookieManager 사용 */
|
||||
private void triggerJenkinsBuildWithSession() throws Exception {
|
||||
CookieManager cookieManager = new CookieManager(null, CookiePolicy.ACCEPT_ALL);
|
||||
HttpClient httpClient = HttpClient.newBuilder()
|
||||
.cookieHandler(cookieManager)
|
||||
.connectTimeout(Duration.ofSeconds(15))
|
||||
.build();
|
||||
String base = normalizeBase(jenkinsUrl);
|
||||
String auth = Base64.getEncoder().encodeToString(
|
||||
(jenkinsUser.trim() + ":" + jenkinsToken.trim()).getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
String crumbField = "Jenkins-Crumb";
|
||||
String crumbValue = null;
|
||||
HttpRequest crumbReq = HttpRequest.newBuilder()
|
||||
.uri(URI.create(base + "/crumbIssuer/api/json"))
|
||||
.header("Authorization", "Basic " + auth)
|
||||
.header("Accept", MediaType.APPLICATION_JSON_VALUE)
|
||||
.timeout(Duration.ofSeconds(10))
|
||||
.GET()
|
||||
.build();
|
||||
HttpResponse<String> crumbResp = httpClient.send(crumbReq, HttpResponse.BodyHandlers.ofString());
|
||||
if (crumbResp.statusCode() == 200 && StringUtils.hasText(crumbResp.body())) {
|
||||
JsonNode node = objectMapper.readTree(crumbResp.body());
|
||||
crumbField = node.path("crumbRequestField").asText("Jenkins-Crumb");
|
||||
crumbValue = textOrNull(node.path("crumb"));
|
||||
}
|
||||
return url;
|
||||
|
||||
int lastCode = 0;
|
||||
for (String path : buildTriggerUrlCandidates()) {
|
||||
HttpRequest.Builder req = HttpRequest.newBuilder()
|
||||
.uri(URI.create(base + path))
|
||||
.header("Authorization", "Basic " + auth)
|
||||
.header("Content-Type", MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
.timeout(Duration.ofSeconds(20))
|
||||
.POST(HttpRequest.BodyPublishers.noBody());
|
||||
if (StringUtils.hasText(crumbValue)) {
|
||||
req.header(crumbField, crumbValue);
|
||||
}
|
||||
HttpResponse<Void> resp = httpClient.send(req.build(), HttpResponse.BodyHandlers.discarding());
|
||||
lastCode = resp.statusCode();
|
||||
if (lastCode >= 200 && lastCode < 400) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (lastCode == 401 || lastCode == 403) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, jenkinsHttpErrorForCode(lastCode));
|
||||
}
|
||||
throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "Jenkins HTTP " + lastCode);
|
||||
}
|
||||
|
||||
private void triggerJenkinsBuildWebClient() {
|
||||
WebClient client = jenkinsClient();
|
||||
WebClientResponseException lastAuthError = null;
|
||||
|
||||
for (String buildUrl : buildTriggerUrlCandidates()) {
|
||||
Map<String, String> crumb = fetchCrumbOptional(client);
|
||||
try {
|
||||
postBuild(client, buildUrl, crumb);
|
||||
return;
|
||||
} catch (WebClientResponseException.Forbidden e) {
|
||||
lastAuthError = e;
|
||||
if (crumb == null) {
|
||||
try {
|
||||
postBuild(client, buildUrl, fetchCrumb(client));
|
||||
return;
|
||||
} catch (WebClientResponseException.Forbidden e2) {
|
||||
lastAuthError = e2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (lastAuthError != null) {
|
||||
throw lastAuthError;
|
||||
}
|
||||
throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "Jenkins 빌드 트리거 URL을 구성할 수 없습니다.");
|
||||
}
|
||||
|
||||
/** 인증+토큰 조합을 순서대로 시도 (대부분의 Jenkins는 USER/TOKEN 없이 token만으로는 403). */
|
||||
private List<String> buildTriggerUrlCandidates() {
|
||||
String base = jobApiPath() + "/build";
|
||||
String tokenQuery = hasTriggerToken()
|
||||
? "?token=" + UriUtils.encodeQueryParam(jenkinsTriggerToken.trim(), StandardCharsets.UTF_8)
|
||||
: "";
|
||||
|
||||
List<String> urls = new ArrayList<>();
|
||||
if (hasBasicAuth() && hasTriggerToken()) {
|
||||
urls.add(base + tokenQuery);
|
||||
}
|
||||
if (hasBasicAuth()) {
|
||||
urls.add(base);
|
||||
}
|
||||
if (hasTriggerToken()) {
|
||||
urls.add(base + tokenQuery);
|
||||
}
|
||||
if (urls.isEmpty()) {
|
||||
urls.add(base);
|
||||
}
|
||||
return urls;
|
||||
}
|
||||
|
||||
private Map<String, String> fetchCrumbOptional(WebClient client) {
|
||||
@@ -293,15 +399,24 @@ public class DesktopAppBuildService {
|
||||
return t == null || t.isBlank() ? null : t;
|
||||
}
|
||||
|
||||
private static String jenkinsHttpError(WebClientResponseException e) {
|
||||
int code = e.getStatusCode().value();
|
||||
private String jenkinsHttpError(WebClientResponseException e) {
|
||||
return jenkinsHttpErrorForCode(e.getStatusCode().value());
|
||||
}
|
||||
|
||||
private String jenkinsHttpErrorForCode(int code) {
|
||||
if (code == 401 || code == 403) {
|
||||
return "Jenkins 인증 실패 — GC_DESKTOP_JENKINS_TRIGGER_TOKEN 또는 USER/TOKEN·Job 원격 트리거 토큰을 확인하세요.";
|
||||
if (!hasBasicAuth()) {
|
||||
return "Jenkins 인증 실패 — 서버 .env에 GC_DESKTOP_JENKINS_USER·GC_DESKTOP_JENKINS_TOKEN(Jenkins API Token)을 설정하고 "
|
||||
+ "GC_DESKTOP_JENKINS_TRIGGER_TOKEN(기본 goldenchart-desktop-build)과 Job authToken이 일치하는지 확인하세요. "
|
||||
+ "설정: ./scripts/configure-desktop-jenkins-backend.sh";
|
||||
}
|
||||
return "Jenkins 인증 실패 — GC_DESKTOP_JENKINS_USER/TOKEN·GC_DESKTOP_JENKINS_TRIGGER_TOKEN·Job authToken을 확인하세요. "
|
||||
+ "Job 재등록: ./scripts/server-install-desktop-jenkins.sh";
|
||||
}
|
||||
if (code == 404) {
|
||||
return "Jenkins Job을 찾을 수 없습니다 — ./scripts/server-install-desktop-jenkins.sh 로 Job을 등록하세요.";
|
||||
}
|
||||
return "Jenkins HTTP " + code + ": " + e.getStatusText();
|
||||
return "Jenkins HTTP " + code;
|
||||
}
|
||||
|
||||
private static String rootMessage(Throwable e) {
|
||||
|
||||
Reference in New Issue
Block a user