Merge pull request '当请求为Nacos时,Nacos返回的所有非JSON响应都会被自动转换为结构化的错误信息,同时将HTTP状态码设置为200,适合用于规范化微服务架构的响应格式。' (#831) from otto/microservices:third-party-tool-forward into third-party-tool-forward

This commit is contained in:
otto 2025-02-24 17:02:22 +08:00
commit edb5e65035
23 changed files with 1125 additions and 23 deletions

View File

@ -0,0 +1,46 @@
package com.microservices.system.api;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.common.core.constant.ServiceNameConstants;
import com.microservices.system.api.factory.RemoteGatewayFallbackFactory;
import feign.Response;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.Map;
/**
* 网关
*
* @author microservices
*/
@Component
@FeignClient(contextId = "remoteGatewayService", value = ServiceNameConstants.GATEWAY_SERVICE, fallbackFactory = RemoteGatewayFallbackFactory.class)
public interface RemoteGatewayService {
/**
* 登录Sentinel
*
* @return 响应
*/
@PostMapping("/sentinel/auth/login")
Response loginSentinel(@RequestParam("username") String username, @RequestParam("password") String password);
/**
* 登录Nacos
*
* @return 响应
*/
@PostMapping(value = "/nacos/v1/auth/users/login", consumes = {"application/x-www-form-urlencoded"})
Response loginNacos(Map<String, ?> formParams);
/**
* 登录Portainer
*
* @return 响应
*/
@PostMapping("/portainer/api/auth")
Response loginPortainer(@RequestBody JSONObject loginBody);
}

View File

@ -4,7 +4,7 @@ import com.microservices.common.core.constant.SecurityConstants;
import com.microservices.common.core.constant.ServiceNameConstants;
import com.microservices.common.core.domain.R;
import com.microservices.system.api.domain.SysDept;
import com.microservices.system.api.factory.RemoteCmsFallbackFactory;
import com.microservices.system.api.factory.RemoteZoneFallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
@ -15,7 +15,7 @@ import java.util.List;
*
* @author microservices
*/
@FeignClient(contextId = "remoteZoneService", value = ServiceNameConstants.ZONE_SERVICE, fallbackFactory = RemoteCmsFallbackFactory.class)
@FeignClient(contextId = "remoteZoneService", value = ServiceNameConstants.ZONE_SERVICE, fallbackFactory = RemoteZoneFallbackFactory.class)
public interface RemoteZoneService {
/**
* 新增组织时自动创建特色专区

View File

@ -0,0 +1,44 @@
package com.microservices.system.api.factory;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.system.api.RemoteGatewayService;
import feign.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 用户服务降级处理
*
* @author microservices
*/
@Component
public class RemoteGatewayFallbackFactory implements FallbackFactory<RemoteGatewayService> {
private static final Logger log = LoggerFactory.getLogger(RemoteGatewayFallbackFactory.class);
@Override
public RemoteGatewayService create(Throwable throwable) {
log.error("网关服务调用失败:{}", throwable.getMessage());
return new RemoteGatewayService() {
@Override
public Response loginSentinel(String password, String username) {
return null;
}
@Override
public Response loginNacos(Map<String, ?> formParams) {
return null;
}
@Override
public Response loginPortainer(JSONObject loginBody) {
System.out.println(throwable.getMessage());
return null;
}
};
}
}

View File

@ -5,3 +5,4 @@ com.microservices.system.api.factory.RemoteFileFallbackFactory
com.microservices.system.api.factory.RemoteCmsFallbackFactory
com.microservices.system.api.factory.RemoteZoneFallbackFactory
com.microservices.system.api.factory.RemotePmsFallbackFactory
com.microservices.system.api.factory.RemoteGatewayFallbackFactory

View File

@ -208,6 +208,22 @@ public class CacheConstants {
return GITLINK_ORG_ID_OPEN_ENTERPRISE_KEY + gitlinkOrgId;
}
/**
* Sentinel Token缓存Key
*/
public final static String SENTINEL_TOKEN = "sentinel_token";
/**
* Nacos Token缓存Key
*/
public static final String NACOS_TOKEN = "nacos_token";
/**
* Portainer Token缓存Key
*/
public static final String PORTAINER_TOKEN = "portainer_token";
/**
* 专区项目Gitlink信息 Key前缀
*/

View File

@ -32,4 +32,8 @@ public class ServiceNameConstants {
* 项目管理模块的serviceid
*/
public static final String PMS_SERVICE = "microservices-pms";
/**
* 网关模块的serviceid
*/
public static final String GATEWAY_SERVICE = "microservices-gateway";
}

View File

@ -18,6 +18,14 @@ public class TokenConstants {
* GitLink令牌标识
*/
public static final String GitLink_Token_Key = "autologin_trustie=";
/**
* Sentinel令牌标识
*/
public static final String Sentinel_Token_Key = "sentinel_dashboard_cookie";
/**
* Sentinel令牌标识
*/
public static final String Nacos_Token_Key = "Accesstoken";
/**
* Cookie中令牌标识
*/

View File

@ -0,0 +1,40 @@
package com.microservices.common.core.utils;
import java.util.HashMap;
public class CookieUtil {
public static HashMap<String, String> getCookieMap(String cookie) {
HashMap<String, String> map = new HashMap<>();
if (cookie != null) {
String[] cookies = cookie.split(";");
for (String cookieStr : cookies) {
String[] cookieArr = cookieStr.split("=");
String key = cookieArr[0].trim();
String value = cookieArr[1].trim();
map.put(key, value);
}
}
return map;
}
public static String getCookieValue(String cookie, String key) {
HashMap<String, String> map = getCookieMap(cookie);
return map.get(key);
}
public static String removeCookieKey(String cookie, String key) {
HashMap<String, String> map = getCookieMap(cookie);
map.remove(key);
return genCookieStr(map);
}
private static String genCookieStr(HashMap<String, String> cookieMap) {
StringBuilder stringBuilder = new StringBuilder();
for (String key : cookieMap.keySet()) {
stringBuilder.append(key).append("=").append(cookieMap.get(key)).append(";");
}
return stringBuilder.toString();
}
}

View File

@ -1,9 +1,16 @@
package com.microservices.common.security.feign;
import feign.RequestInterceptor;
import feign.codec.Encoder;
import feign.form.FormEncoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.SpringEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Feign 配置注册
*
@ -12,9 +19,20 @@ import org.springframework.context.annotation.Configuration;
@Configuration
public class FeignAutoConfiguration
{
// 这里会由容器自动注入HttpMessageConverters的对象工厂
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
@Bean
public RequestInterceptor requestInterceptor()
{
return new FeignRequestInterceptor();
}
// new一个form编码器实现支持form表单提交
// 注意这里方法名称也就是bean的名称是什么不重要
// 重要的是返回类型要是 Encoder 并且实现类必须是 FormEncoder 或者其子类
@Bean
public Encoder feignFormEncoder() {
return new FormEncoder(new SpringEncoder(messageConverters));
}
}

View File

@ -0,0 +1,35 @@
package com.microservices.gateway;
import com.microservices.common.core.exception.ServiceException;
import com.microservices.common.core.threadPool.ThreadPoolExecutorWrap;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* @author OTTO
*/
public class CustomExecutorFactory {
/**
* 创建线程池
* 存在并发处理的情况设置核心线程为2设置有界队列长度为100最大线程数为10
* 当超出队列已满且达到最大线程数时抛出异常
* Ncpu=CPU数量
* Ucpu=目标CPU的使用率0<=Ucpu<=1
* W/C=任务等待时间与任务计算时间的比率
* Nthreads =Ncpu*Ucpu*(1+W/C)
*/
public static ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutorWrap(
4
, 10
, 10
, TimeUnit.SECONDS
, new ArrayBlockingQueue<>(100)
, Executors.defaultThreadFactory()
, (r, executor) -> {
throw new ServiceException("目前处理的人太多了,请稍后再试");
}
);
}

View File

@ -2,13 +2,18 @@ package com.microservices.gateway.config;
import feign.codec.Decoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import java.util.stream.Collectors;
/**
* @author otto
*/
@ -25,4 +30,10 @@ public class FeignConfig {
(new MappingJackson2HttpMessageConverter());
return () -> httpMessageConverters;
}
@Bean
@ConditionalOnMissingBean
public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));
}
}

View File

@ -10,7 +10,9 @@ import com.microservices.common.core.utils.JwtUtils;
import com.microservices.common.core.utils.ServletUtils;
import com.microservices.common.core.utils.StringUtils;
import com.microservices.common.redis.service.RedisService;
import com.microservices.gateway.CustomExecutorFactory;
import com.microservices.gateway.config.properties.IgnoreWhiteProperties;
import com.microservices.gateway.service.ThirdPartyToolService;
import com.microservices.system.api.RemoteUserService;
import com.microservices.system.api.domain.SysUserDeptRole;
import com.microservices.system.api.utils.FeignUtils;
@ -28,9 +30,8 @@ import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
/**
* 网关鉴权
@ -51,7 +52,12 @@ public class AuthFilter implements GlobalFilter, Ordered {
@Autowired
private RemoteUserService remoteUserService;
ExecutorService executorService = Executors.newFixedThreadPool(1);
@Lazy
@Autowired
private ThirdPartyToolService thirdPartyToolService;
ThreadPoolExecutor threadPoolExecutor = CustomExecutorFactory.threadPoolExecutor;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
@ -75,6 +81,9 @@ public class AuthFilter implements GlobalFilter, Ordered {
}
if (StringUtils.isEmpty(token)) {
if (StringUtils.isNotEmpty(gitLinkCookie)) {
return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");
}
return unauthorizedResponse(exchange, "令牌不能为空");
}
Claims claims;
@ -105,6 +114,11 @@ public class AuthFilter implements GlobalFilter, Ordered {
ServletUtils.addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);
// 内部请求来源参数清除
ServletUtils.removeHeader(mutate, SecurityConstants.FROM_SOURCE);
Mono<Void> thirdPartyRes = thirdPartyToolService.handleRequestForThirdPartyTools(exchange, request, mutate);
if (thirdPartyRes != null) {
return thirdPartyRes;
}
return chain.filter(exchange.mutate().request(mutate.build()).build());
}
@ -116,7 +130,7 @@ public class AuthFilter implements GlobalFilter, Ordered {
boolean hasUserIdentifyList = redisService.hasKey(redisKey);
if (!hasUserIdentifyList) {
// 网关采用异步架构所以此处需要通过异步请求获取本系统Token
Future<R<List<SysUserDeptRole>>> future = executorService.submit(
Future<R<List<SysUserDeptRole>>> future = threadPoolExecutor.submit(
() -> remoteUserService.getSysUserDeptRoleListByUserName(username, SecurityConstants.INNER));
try {
List<SysUserDeptRole> feignResult = FeignUtils.getReturnData(
@ -130,7 +144,7 @@ public class AuthFilter implements GlobalFilter, Ordered {
}
private String genCookieByToken(String token) {
return String.format("%s%s;",TokenConstants.GitLink_Token_Key, token);
return String.format("%s%s;", TokenConstants.GitLink_Token_Key, token);
}
private String getGitLinkRequestCookie(ServerHttpRequest request) {
@ -141,7 +155,7 @@ public class AuthFilter implements GlobalFilter, Ordered {
String token = request.getHeaders().getFirst(TokenConstants.AUTHENTICATION);
if (token != null) {
// 网关采用异步架构所以此处需要通过异步请求获取本系统Token
Future<R<Boolean>> future = executorService.submit(
Future<R<Boolean>> future = threadPoolExecutor.submit(
() -> remoteUserService.checkGitLinkUserLogin(token, SecurityConstants.INNER));
try {
Boolean feignResult = FeignUtils.getReturnData(
@ -194,7 +208,7 @@ public class AuthFilter implements GlobalFilter, Ordered {
private String getGitLinkToken(String cookie) {
if (StringUtils.isNotEmpty(cookie)) {
// 网关采用异步架构所以此处需要通过异步请求获取本系统Token
Future<R<String>> future = executorService.submit(() -> remoteUserService
Future<R<String>> future = threadPoolExecutor.submit(() -> remoteUserService
.getSysUserTokenByGitLinkCookie(cookie, SecurityConstants.INNER));
R<String> feignResult;
try {

View File

@ -0,0 +1,90 @@
package com.microservices.gateway.filter;
import com.alibaba.fastjson2.JSON;
import com.microservices.common.core.web.domain.AjaxResult;
import lombok.extern.slf4j.Slf4j;
import org.reactivestreams.Publisher;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferFactory;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
@Slf4j
@Component
public class GlobalResponseFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String url = request.getURI().getPath();
// 当请求为Nacos时Nacos返回的所有非JSON响应都会被自动转换为结构化的错误信息同时将HTTP状态码设置为200适合用于规范化微服务架构的响应格式
if (url.toLowerCase().startsWith("/nacos")) {
ServerHttpResponse originalResponse = exchange.getResponse();
DataBufferFactory bufferFactory = originalResponse.bufferFactory();
ServerHttpResponseDecorator decoratedResponse = new ServerHttpResponseDecorator(originalResponse) {
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
MediaType contentType = getHeaders().getContentType();
HttpStatus status = getStatusCode() != null ? getStatusCode() : HttpStatus.INTERNAL_SERVER_ERROR;
// 排除无内容响应和二进制类型
if (status == HttpStatus.NO_CONTENT ||
(contentType != null && (contentType.includes(MediaType.APPLICATION_OCTET_STREAM) ||
contentType.includes(MediaType.MULTIPART_FORM_DATA)))) {
return super.writeWith(body);
}
// 保留原始状态码
originalResponse.setStatusCode(HttpStatus.OK);
getHeaders().setContentType(MediaType.APPLICATION_JSON);
return Flux.from(body).<String>handle((dataBuffer, synchronousSink) -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
//释放掉内存
DataBufferUtils.release(dataBuffer);
String result = new String(bytes, StandardCharsets.UTF_8);
if (!JSON.isValid(result)) {
synchronousSink.next(result);
} else {
synchronousSink.complete();
}
}).flatMap(resBody -> {
byte[] bytes = JSON.toJSONBytes(AjaxResult.error(resBody));
getHeaders().setContentLength(bytes.length);
DataBuffer buffer = bufferFactory.wrap(bytes);
return super.writeWith(Mono.just(buffer));
}).then();
}
};
return chain.filter(exchange.mutate().response(decoratedResponse).build());
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
//WRITE_RESPONSE_FILTER 之前执行
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
}
}

View File

@ -1,5 +1,6 @@
package com.microservices.gateway.handler;
import com.microservices.common.core.exception.ServiceException;
import com.microservices.common.core.utils.ServletUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -19,33 +20,27 @@ import reactor.core.publisher.Mono;
*/
@Order(-1)
@Configuration
public class GatewayExceptionHandler implements ErrorWebExceptionHandler
{
public class GatewayExceptionHandler implements ErrorWebExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GatewayExceptionHandler.class);
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex)
{
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
if (exchange.getResponse().isCommitted())
{
if (exchange.getResponse().isCommitted()) {
return Mono.error(ex);
}
String msg;
if (ex instanceof NotFoundException)
{
if (ex instanceof NotFoundException) {
msg = "服务未找到";
}
else if (ex instanceof ResponseStatusException)
{
} else if (ex instanceof ServiceException) {
msg = ex.getMessage();
} else if (ex instanceof ResponseStatusException) {
ResponseStatusException responseStatusException = (ResponseStatusException) ex;
msg = responseStatusException.getMessage();
}
else
{
} else {
msg = "内部服务器错误";
}

View File

@ -0,0 +1,22 @@
package com.microservices.gateway.service;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* 第三方工具服务类
*
* @author OTTO
*/
public interface ThirdPartyToolService {
/**
* 处理第三方工具请求
*
* @param exchange
* @param request 请求
* @param mutate 请求头获取器
*/
Mono<Void> handleRequestForThirdPartyTools(ServerWebExchange exchange, ServerHttpRequest request, ServerHttpRequest.Builder mutate);
}

View File

@ -0,0 +1,189 @@
package com.microservices.gateway.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.microservices.common.core.constant.CacheConstants;
import com.microservices.common.core.constant.TokenConstants;
import com.microservices.common.core.exception.ServiceException;
import com.microservices.common.core.utils.CookieUtil;
import com.microservices.common.core.utils.ServletUtils;
import com.microservices.common.core.utils.StringUtils;
import com.microservices.common.redis.service.RedisService;
import com.microservices.gateway.CustomExecutorFactory;
import com.microservices.gateway.service.ThirdPartyToolService;
import com.microservices.system.api.RemoteGatewayService;
import feign.Response;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Lazy;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.io.IOException;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.*;
/**
* @author OTTO
*/
@Service
public class ThirdPartyToolServiceImpl implements ThirdPartyToolService {
private static final Logger log = LoggerFactory.getLogger(ThirdPartyToolServiceImpl.class);
@Value("${thirdPartyTools.nacos.auth.username:}")
public String nacosUsername;
@Value("${thirdPartyTools.nacos.auth.password:}")
public String nacosPassword;
@Value("${thirdPartyTools.sentinel.auth.username:}")
public String sentinelUsername;
@Value("${thirdPartyTools.sentinel.auth.password:}")
public String sentinelPassword;
@Value("${thirdPartyTools.portainer.auth.username:}")
public String portainerUsername;
@Value("${thirdPartyTools.portainer.auth.password:}")
public String portainerPassword;
ThreadPoolExecutor threadPoolExecutor = CustomExecutorFactory.threadPoolExecutor;
@Lazy
@Autowired
private RemoteGatewayService remoteGatewayService;
@Autowired
private RedisService redisService;
/**
* 处理第三方工具请求
*/
@Override
public Mono<Void> handleRequestForThirdPartyTools(ServerWebExchange exchange, ServerHttpRequest request, ServerHttpRequest.Builder mutate) {
String url = request.getURI().getPath();
if (StringUtils.isEmpty(url)) {
return null;
}
// 处理Sentinel请求
if (url.toLowerCase().startsWith("/sentinel")) {
// 检查Cookie中是否携带sentinel cookie
String cookie = request.getHeaders().getFirst(TokenConstants.Cookie);
if (StringUtils.isNotEmpty(CookieUtil.getCookieValue(cookie, TokenConstants.Sentinel_Token_Key))) {
cookie = CookieUtil.removeCookieKey(cookie, TokenConstants.Sentinel_Token_Key);
}
String sentinelCookie = null;
if (redisService.hasKey(CacheConstants.SENTINEL_TOKEN)) {
sentinelCookie = redisService.getCacheObject(CacheConstants.SENTINEL_TOKEN);
} else {
// 网关采用异步架构所以此处需要通过异步请求获取Sentinel Token
Future<Response> future = threadPoolExecutor.submit(() -> remoteGatewayService.loginSentinel(sentinelUsername, sentinelPassword));
try {
Response response = future.get(1, TimeUnit.SECONDS);
Map<String, Collection<String>> header = response.headers();
if (header != null && header.containsKey("set-cookie")) {
sentinelCookie = header.get("set-cookie").iterator().next();
redisService.setCacheObject(CacheConstants.SENTINEL_TOKEN, sentinelCookie, 12L, TimeUnit.HOURS);
}
} catch (TimeoutException e) {
log.error("获取Sentinel Token超时");
} catch (InterruptedException | ExecutionException e) {
log.error("获取Sentinel Token失败{}", e.getMessage());
}
}
if (StringUtils.isNotEmpty(sentinelCookie)) {
ServletUtils.removeHeader(mutate, TokenConstants.Cookie);
mutate.header(TokenConstants.Cookie, String.format("%s;%s;", cookie, sentinelCookie));
} else {
throw new ServiceException("Sentinel服务获取Token失败");
}
}
// 处理Nacos请求
if (url.toLowerCase().startsWith("/nacos")) {
String nacosToken = null;
if (redisService.hasKey(CacheConstants.NACOS_TOKEN)) {
nacosToken = redisService.getCacheObject(CacheConstants.NACOS_TOKEN);
} else {
Map<String, String> nacosMap = new HashMap<>();
nacosMap.put("username", nacosUsername);
nacosMap.put("password", nacosPassword);
// 网关采用异步架构所以此处需要通过异步请求获取Nacos Token
Future<Response> future = threadPoolExecutor.submit(() -> remoteGatewayService.loginNacos(nacosMap));
try {
Response res = future.get(1, TimeUnit.SECONDS);
StringWriter writer = new StringWriter();
IOUtils.copy(res.body().asInputStream(), writer, StandardCharsets.UTF_8.name());
String str = writer.toString();
if (!JSON.isValid(str)) {
return exceptionResponse(exchange, str, res.status());
}
JSONObject resJsonObject = JSONObject.parseObject(str);
nacosToken = resJsonObject.getString("accessToken");
Long tokenTtl = resJsonObject.getLong("tokenTtl");
if (nacosToken != null && tokenTtl != null) {
redisService.setCacheObject(CacheConstants.NACOS_TOKEN, nacosToken, tokenTtl - 10, TimeUnit.SECONDS);
}
} catch (TimeoutException e) {
log.error("获取Nacos Token超时");
} catch (InterruptedException | ExecutionException | IOException | NullPointerException e) {
log.error("获取Nacos Token失败{}", e.getMessage());
}
}
if (StringUtils.isNotEmpty(nacosToken)) {
mutate.header(TokenConstants.Nacos_Token_Key, nacosToken);
} else {
throw new ServiceException("Nacos服务获取Token失败");
}
}
// 处理portainer请求
if (url.toLowerCase().startsWith("/portainer")) {
String portainerToken = null;
if (redisService.hasKey(CacheConstants.PORTAINER_TOKEN)) {
portainerToken = redisService.getCacheObject(CacheConstants.PORTAINER_TOKEN);
} else {
// 网关采用异步架构所以此处需要通过异步请求获取Portainer Token
JSONObject loginPortainerBody = new JSONObject();
loginPortainerBody.put("username", portainerUsername);
loginPortainerBody.put("password", portainerPassword);
Future<Response> future = threadPoolExecutor.submit(() -> remoteGatewayService.loginPortainer(loginPortainerBody));
try {
Response res = future.get(1, TimeUnit.SECONDS);
StringWriter writer = new StringWriter();
IOUtils.copy(res.body().asInputStream(), writer, StandardCharsets.UTF_8.name());
String str = writer.toString();
if (!JSON.isValid(str)) {
return exceptionResponse(exchange, str, res.status());
}
JSONObject resJsonObject = JSONObject.parseObject(str);
portainerToken = resJsonObject.getString("jwt");
String[] tokenSplit = portainerToken.split("\\.");
String tokenBodyStr = new String(Base64.getDecoder().decode(tokenSplit[1]), StandardCharsets.UTF_8);
JSONObject tokenBody = JSONObject.parseObject(tokenBodyStr);
Long expiration = tokenBody.getLong("exp");
Long now = new Date().getTime() / 1000;
long portainerTokenTtl = expiration - now;
if (portainerTokenTtl > 20) {
redisService.setCacheObject(CacheConstants.PORTAINER_TOKEN, portainerToken, portainerTokenTtl - 10, TimeUnit.SECONDS);
}
} catch (TimeoutException e) {
log.error("获取Portainer Token超时");
} catch (InterruptedException | ExecutionException | IOException | NullPointerException e) {
log.error("获取Portainer Token失败{}", e.getMessage());
}
}
if (StringUtils.isNotEmpty(portainerToken)) {
mutate.header(TokenConstants.AUTHENTICATION, TokenConstants.PREFIX + portainerToken);
} else {
throw new ServiceException("Portainer服务获取Token失败");
}
}
return null;
}
private Mono<Void> exceptionResponse(ServerWebExchange exchange, String msg, int code) {
log.error("[第三方工具请求处理异常]请求路径:{}", exchange.getRequest().getPath());
return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, code);
}
}

View File

@ -204,6 +204,14 @@ public class PmsCiPipelinesController extends BaseController
return success(pmsCiPipelinesService.savePipelineYamlByGraphicJson(id, pmsCiPipelineBuildInputVo));
}
@PostMapping(value = "/{id}/savePipelineYamlNew")
@ApiOperation("流水线图形构建声明式yaml并保存")
public AjaxResult savePipelineYamlNew(@ApiParam(name = "id", value = "流水线id")@PathVariable Long id,
@RequestBody @Validated PmsCiPipelineBuildYamlInputVo pmsCiPipelineBuildInputVo)
{
return success(pmsCiPipelinesService.savePipelineYamlByGraphicJsonNew(id, pmsCiPipelineBuildInputVo));
}
/**
* 启动流水线执行记录任务
*/
@ -251,4 +259,16 @@ public class PmsCiPipelinesController extends BaseController
pmsCiPipelinesService.getPipelineRunJobLogs(id, run, job, response);
}
/**
* 流水线执行测试报告结果查询
*/
@GetMapping(value = "/{id}/actions/runs/{run}/results")
@ApiOperation("流水线执行测试报告结果查询")
public AjaxResult getPipelineRunResults(@ApiParam(name = "id", value = "流水线id")@PathVariable Long id,
@ApiParam(name = "run", value = "执行记录序号")@PathVariable String run)
{
return success(pmsCiPipelinesService.getPipelineRunResults(id, run));
}
}

View File

@ -0,0 +1,148 @@
package com.microservices.pms.pipeline.domain.vo;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
import java.util.stream.Collectors;
@Getter
@Setter
@NoArgsConstructor
public class NodeActionVo {
private String name;
private final Map<String, Object> on = new LinkedHashMap<>();
private final Map<String, Object> jobs = new LinkedHashMap<>();
@JsonIgnore
private Map<String, Object> params = new LinkedHashMap<>();
public void buildName(String name) {
this.name = name;
}
public void parse(List<Map<String, Object>> steps, PipelineVo.PipelineJson.Nodes node){
String nodeName = Optional.ofNullable(node.getName()).orElse("");
if (nodeName.startsWith("on-")) {
buildOn(node);
}else if (nodeName.startsWith("setup-")) {
buildSetupStep(steps, node);
}else if (Objects.equals(nodeName,"shell")) {
buildShellStep(steps, node);
}else if (StringUtils.isNotEmpty(nodeName)) {
buildOtherStep(steps, node);
}
}
public void buildOn(PipelineVo.PipelineJson.Nodes node) {
String nodeName = node.getName();
Map<String, Object> step = new LinkedHashMap<>();
List<PipelineVo.PipelineJson.Nodes.Inputs> inputs = getInputs(node);
if (Objects.equals(nodeName, "on-schedule")) {
inputs.forEach(e -> {
step.put(e.getName(), e.getValue());
});
this.on.put(nodeName.split("-")[1], step);
return;
}
inputs.forEach(e -> {
step.put(e.getName(), e.getValue().replace("\'","").split(","));
});
this.on.put(nodeName.split("-")[1], step);
}
public void buildJobs(String jobKey, String jobName, List<Map<String, Object>> steps) {
if (steps.isEmpty()) return;
Map<String, Object> job = new LinkedHashMap<>();
job.put("runs-on", "ubuntu-latest");
job.put("name", jobName);
job.put("steps", steps);
this.jobs.put(jobKey+(this.jobs.size()), job);
}
public void buildSetupStep(List<Map<String, Object>> steps, PipelineVo.PipelineJson.Nodes node) {
String nodeName = node.getName();
Map<String, Object> step = new LinkedHashMap<>();
List<PipelineVo.PipelineJson.Nodes.Inputs> inputs = getInputs(node);
if (nodeName.startsWith("setup-java")) {
Map<String, String> name2Value = inputs.stream()
.collect(Collectors.toMap(e -> e.getName(), e -> e.getValue(), (o, n) -> n));
Map<String, Object> with = new LinkedHashMap<>();
if (name2Value.containsKey("download_url") && StringUtils.isNotBlank(name2Value.get("download_url"))) {
steps.add(Collections.singletonMap("run", "download_url=" + name2Value.get("download_url") + " && " + "wget -O $RUNNER_TEMP/java_package.tar.gz $download_url"));
with.put("distribution", "jdkfile");
with.put("jdkFile", "${{ runner.temp }}/java_package.tar.gz");
}
step.put("name", node.getLabel());
step.put("uses", node.getFull_name().trim());
if (!inputs.isEmpty()) {
inputs.forEach(i -> {
with.put(i.getName(), Optional.ofNullable(i.getValue()).orElse(""));
});
step.put("with", with);
}
steps.add(step);
return;
}
step.put("name", node.getLabel());
step.put("uses", node.getFull_name().trim());
if (!inputs.isEmpty()) {
Map<String, Object> with = new LinkedHashMap<>();
inputs.forEach(i -> {
with.put(i.getName(), Optional.ofNullable(i.getValue()).orElse(""));
});
step.put("with", with);
}
steps.add(step);
}
public void buildShellStep(List<Map<String, Object>> steps, PipelineVo.PipelineJson.Nodes node) {
Map<String, Object> step = new LinkedHashMap<>();
step.put("name", node.getLabel());
List<PipelineVo.PipelineJson.Nodes.Inputs> inputs = Optional.ofNullable(node.getInputs()).orElse(new ArrayList<>());
inputs.forEach(i -> {
step.put(i.getName(), Optional.ofNullable(i.getValue()).orElse(""));
});
steps.add(step);
}
public void buildOtherStep(List<Map<String, Object>> steps, PipelineVo.PipelineJson.Nodes node) {
Map<String, Object> step = new LinkedHashMap<>();
List<PipelineVo.PipelineJson.Nodes.Inputs> inputs = getInputs(node);
step.put("name", node.getLabel());
step.put("uses", node.getFull_name().trim());
if (!inputs.isEmpty()) {
Map<String, Object> with = new LinkedHashMap<>();
inputs.forEach(i -> {
with.put(i.getName(), Optional.ofNullable(i.getValue()).orElse(""));
});
step.put("with", with);
}
steps.add(step);
}
private List<PipelineVo.PipelineJson.Nodes.Inputs> getInputs(PipelineVo.PipelineJson.Nodes node) {
return Optional.ofNullable(node.getInputs()).orElse(new ArrayList<>())
.stream()
.filter(e -> StringUtils.isNotBlank(e.getValue())).collect(Collectors.toList());
}
}

View File

@ -0,0 +1,284 @@
package com.microservices.pms.pipeline.domain.vo;
import java.io.Serializable;
import java.util.List;
public class PipelineVo implements Serializable {
private String pipelineName;
private PipelineJson pipelineJson;
public String getPipelineName() {
return this.pipelineName;
}
public void setPipelineName(String pipelineName) {
this.pipelineName = pipelineName;
}
public PipelineJson getPipelineJson() {
return this.pipelineJson;
}
public void setPipelineJson(PipelineJson pipelineJson) {
this.pipelineJson = pipelineJson;
}
public static class PipelineJson implements Serializable {
private List<Nodes> nodes;
private List<Edges> edges;
public List<Nodes> getNodes() {
return this.nodes;
}
public void setNodes(List<Nodes> nodes) {
this.nodes = nodes;
}
public List<Edges> getEdges() {
return this.edges;
}
public void setEdges(List<Edges> edges) {
this.edges = edges;
}
public static class Nodes implements Serializable {
private Integer action_node_types_id;
private String img;
private List<Inputs> inputs;
private String icon;
private String description;
private String label;
private String type;
private Integer sort_no;
private Integer use_count;
private String full_name;
private String name;
private Integer x;
private Double y;
private String id;
private Boolean isCluster;
private String yaml;
public Integer getAction_node_types_id() {
return this.action_node_types_id;
}
public void setAction_node_types_id(Integer action_node_types_id) {
this.action_node_types_id = action_node_types_id;
}
public String getImg() {
return this.img;
}
public void setImg(String img) {
this.img = img;
}
public List<Inputs> getInputs() {
return this.inputs;
}
public void setInputs(List<Inputs> inputs) {
this.inputs = inputs;
}
public String getIcon() {
return this.icon;
}
public void setIcon(String icon) {
this.icon = icon;
}
public String getDescription() {
return this.description;
}
public void setDescription(String description) {
this.description = description;
}
public String getLabel() {
return this.label;
}
public void setLabel(String label) {
this.label = label;
}
public String getType() {
return this.type;
}
public void setType(String type) {
this.type = type;
}
public Integer getSort_no() {
return this.sort_no;
}
public void setSort_no(Integer sort_no) {
this.sort_no = sort_no;
}
public Integer getUse_count() {
return this.use_count;
}
public void setUse_count(Integer use_count) {
this.use_count = use_count;
}
public String getFull_name() {
return this.full_name;
}
public void setFull_name(String full_name) {
this.full_name = full_name;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public Integer getX() {
return this.x;
}
public void setX(Integer x) {
this.x = x;
}
public Double getY() {
return this.y;
}
public void setY(Double y) {
this.y = y;
}
public String getId() {
return this.id;
}
public void setId(String id) {
this.id = id;
}
public Boolean getIsCluster() {
return this.isCluster;
}
public void setIsCluster(Boolean isCluster) {
this.isCluster = isCluster;
}
public String getYaml() {
return this.yaml;
}
public void setYaml(String yaml) {
this.yaml = yaml;
}
public static class Inputs implements Serializable {
private Boolean is_required;
private String name;
private String value;
private String input_type;
private Integer id;
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public Boolean getIs_required() {
return this.is_required;
}
public void setIs_required(Boolean is_required) {
this.is_required = is_required;
}
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
public String getInput_type() {
return this.input_type;
}
public void setInput_type(String input_type) {
this.input_type = input_type;
}
public Integer getId() {
return this.id;
}
public void setId(Integer id) {
this.id = id;
}
}
}
public static class Edges implements Serializable {
private String source;
private String target;
public String getSource() {
return this.source;
}
public void setSource(String source) {
this.source = source;
}
public String getTarget() {
return this.target;
}
public void setTarget(String target) {
this.target = target;
}
}
}
}

View File

@ -91,6 +91,8 @@ public interface IPmsCiPipelinesService
JSONObject savePipelineYamlByGraphicJson(Long id, PmsCiPipelineBuildYamlInputVo pmsCiPipelineBuildInputVo);
JSONObject savePipelineYamlByGraphicJsonNew(Long id, PmsCiPipelineBuildYamlInputVo pmsCiPipelineBuildInputVo);
JSONObject reRunPipelineRunsJob(Long id, String run, String job);
JSONObject reRunAllJobsInPipelineRun(Long id, String run);
@ -98,4 +100,6 @@ public interface IPmsCiPipelinesService
void getPipelineRunJobLogs(Long id, String run, String job, HttpServletResponse response);
JSONObject runPipelineRunsJob(Long id, String workflow, String ref);
JSONObject getPipelineRunResults(Long id, String run);
}

View File

@ -1,11 +1,16 @@
package com.microservices.pms.pipeline.service.impl;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.microservices.common.core.exception.ServiceException;
import com.microservices.common.core.utils.DateUtils;
import com.microservices.common.core.utils.StringUtils;
@ -23,6 +28,7 @@ import com.microservices.pms.pipeline.domain.vo.*;
import com.microservices.pms.pipeline.service.IPmsCiPipelineGraphicsService;
import com.microservices.pms.utils.PmsConstants;
import com.microservices.pms.utils.PmsGitLinkRequestUrl;
import com.microservices.pms.utils.YamlUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
@ -363,6 +369,34 @@ public class PmsCiPipelinesServiceImpl implements IPmsCiPipelinesService {
return result;
}
@Override
public JSONObject savePipelineYamlByGraphicJsonNew(Long id, PmsCiPipelineBuildYamlInputVo pmsCiPipelineBuildInputVo) {
PmsCiPipelines pmsCiPipelines = pmsCiPipelinesMapper.selectPmsCiPipelinesById(id);
if (pmsCiPipelines == null) {
throw new ServiceException("该项目流水线不存在(流水线ID[%s])", id);
}
try {
PipelineVo vo = JSON.parseObject(JSON.toJSONString(pmsCiPipelineBuildInputVo), PipelineVo.class);
List<PipelineVo.PipelineJson.Nodes> nodes = vo.getPipelineJson().getNodes();
String pipelineName = vo.getPipelineName();
NodeActionVo actionVo = new NodeActionVo();
actionVo.buildName(pipelineName);
nodes.forEach(s -> {
List<Map<String, Object>> steps = new ArrayList<>();
actionVo.parse(steps, s);
actionVo.buildJobs("job", s.getLabel()+String.format("[%s]",s.getId().trim()), steps);
});
String yaml = YamlUtil.toYaml(actionVo);
JSONObject result = new JSONObject();
result.put("pipeline_yaml", yaml);
return result;
} catch (Exception e) {
logger.error("流水线图形Json解析失败)", e);
return null;
}
}
@Override
public JSONObject reRunPipelineRunsJob(Long id, String run, String job) {
PmsCiPipelines pmsCiPipelines = pmsCiPipelinesMapper.selectPmsCiPipelinesById(id);
@ -414,6 +448,23 @@ public class PmsCiPipelinesServiceImpl implements IPmsCiPipelinesService {
}
}
@Override
public JSONObject getPipelineRunResults(Long id, String run) {
PmsCiPipelines pmsCiPipelines = pmsCiPipelinesMapper.selectPmsCiPipelinesById(id);
if (pmsCiPipelines == null) {
throw new ServiceException("该项目流水线不存在(流水线ID[%s])", id);
}
try {
return gitLinkRequestHelper.doGet(PmsGitLinkRequestUrl.GET_PIPELINE_RUN_RESULTS(pmsCiPipelines.getOwner(),
pmsCiPipelines.getRepoIdentifier(),
run));
} catch (Exception e) {
logger.error(e.getMessage());
throw new ServiceException("获取流水线运行结果信息失败");
}
}
private void saveOrUpdateGraphicJsonToLocal(Long id, PmsCiPipelineBuildYamlInputVo pmsCiPipelineBuildInputVo) {
PmsCiPipelineGraphics existentialPmsCiPipelineGraphics = pmsCiPipelineGraphicsService.selectPmsCiPipelineGraphicsByPmsCiPipelineId(id);
PmsCiPipelineGraphics pmsCiPipelineGraphics = new PmsCiPipelineGraphics();

View File

@ -362,4 +362,8 @@ public class PmsGitLinkRequestUrl extends GitLinkRequestUrl {
public static GitLinkRequestUrl SAVE_PIPELINE_YAML(String owner, String repoIdentifier) {
return getGitLinkRequestUrl(String.format("/api/v1/%s/%s/pipelines/save_yaml", owner, repoIdentifier));
}
public static GitLinkRequestUrl GET_PIPELINE_RUN_RESULTS(String owner, String repoIdentifier, String run){
return getGitLinkRequestUrl(String.format("/api/v1/%s/%s/pipelines/run_results.json?run_id=%s", owner, repoIdentifier, run));
}
}

View File

@ -0,0 +1,58 @@
package com.microservices.pms.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
import com.fasterxml.jackson.dataformat.yaml.YAMLGenerator;
import com.microservices.common.core.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.io.StringWriter;
@Slf4j
public class YamlUtil {
/**
* 将yaml字符串转成类对象
*
* @param yamlStr 字符串
* @param clazz 目标类
* @param <T> 泛型
* @return 目标类
*/
public static <T> T toObject(String yamlStr, Class<T> clazz) {
ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
mapper.findAndRegisterModules();
try {
return mapper.readValue(yamlStr, clazz);
} catch (JsonProcessingException e) {
log.error("yaml文本解析成java对象错误", e);
throw new ServiceException("yaml文本解析成java对象错误");
}
}
/**
* 将类对象转yaml字符串
*
* @param object 对象
* @return yaml字符串
*/
public static String toYaml(Object object) {
ObjectMapper mapper = new ObjectMapper(new YAMLFactory());
mapper.findAndRegisterModules();
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
mapper = new ObjectMapper(new YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER));
StringWriter stringWriter = new StringWriter();
try {
mapper.writeValue(stringWriter, object);
return stringWriter.toString();
} catch (IOException e) {
log.error("Java对象解析成Yaml文本错误", e);
}
return null;
}
}