diff --git a/sentinel-dashboard/pom.xml b/sentinel-dashboard/pom.xml index 31334f5d..4b52a74d 100755 --- a/sentinel-dashboard/pom.xml +++ b/sentinel-dashboard/pom.xml @@ -38,6 +38,11 @@ sentinel-parameter-flow-control ${project.version} + + com.alibaba.csp + sentinel-api-gateway-adapter-common + ${project.version} + org.springframework.boot diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/client/SentinelApiClient.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/client/SentinelApiClient.java index e3aef79b..5db63510 100755 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/client/SentinelApiClient.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/client/SentinelApiClient.java @@ -30,9 +30,12 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; +import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule; import com.alibaba.csp.sentinel.command.CommandConstants; import com.alibaba.csp.sentinel.config.SentinelConfig; import com.alibaba.csp.sentinel.command.vo.NodeVo; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity; import com.alibaba.csp.sentinel.dashboard.util.AsyncUtils; import com.alibaba.csp.sentinel.slots.block.Rule; import com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule; @@ -109,6 +112,12 @@ public class SentinelApiClient { private static final String MODIFY_CLUSTER_SERVER_FLOW_CONFIG_PATH = "cluster/server/modifyFlowConfig"; private static final String MODIFY_CLUSTER_SERVER_NAMESPACE_SET_PATH = "cluster/server/modifyNamespaceSet"; + private static final String FETCH_GATEWAY_API_PATH = "gateway/getApiDefinitions"; + private static final String MODIFY_GATEWAY_API_PATH = "gateway/updateApiDefinitions"; + + private static final String FETCH_GATEWAY_FLOW_RULE_PATH = "gateway/getRules"; + private static final String MODIFY_GATEWAY_FLOW_RULE_PATH = "gateway/updateRules"; + private static final String FLOW_RULE_TYPE = "flow"; private static final String DEGRADE_RULE_TYPE = "degrade"; private static final String SYSTEM_RULE_TYPE = "system"; @@ -693,4 +702,90 @@ public class SentinelApiClient { return AsyncUtils.newFailedFuture(ex); } } + + public CompletableFuture> fetchApis(String app, String ip, int port) { + if (StringUtil.isBlank(ip) || port <= 0) { + return AsyncUtils.newFailedFuture(new IllegalArgumentException("Invalid parameter")); + } + + try { + return executeCommand(ip, port, FETCH_GATEWAY_API_PATH, false) + .thenApply(r -> { + List entities = JSON.parseArray(r, ApiDefinitionEntity.class); + if (entities != null) { + for (ApiDefinitionEntity entity : entities) { + entity.setApp(app); + entity.setIp(ip); + entity.setPort(port); + } + } + return entities; + }); + } catch (Exception ex) { + logger.warn("Error when fetching gateway apis", ex); + return AsyncUtils.newFailedFuture(ex); + } + } + + public boolean modifyApis(String app, String ip, int port, List apis) { + if (apis == null) { + return true; + } + + try { + AssertUtil.notEmpty(app, "Bad app name"); + AssertUtil.notEmpty(ip, "Bad machine IP"); + AssertUtil.isTrue(port > 0, "Bad machine port"); + String data = JSON.toJSONString( + apis.stream().map(r -> r.toApiDefinition()).collect(Collectors.toList())); + Map params = new HashMap<>(2); + params.put("data", data); + String result = executeCommand(app, ip, port, MODIFY_GATEWAY_API_PATH, params, true).get(); + logger.info("Modify gateway apis: {}", result); + return true; + } catch (Exception e) { + logger.warn("Error when modifying gateway apis", e); + return false; + } + } + + public CompletableFuture> fetchGatewayFlowRules(String app, String ip, int port) { + if (StringUtil.isBlank(ip) || port <= 0) { + return AsyncUtils.newFailedFuture(new IllegalArgumentException("Invalid parameter")); + } + + try { + return executeCommand(ip, port, FETCH_GATEWAY_FLOW_RULE_PATH, false) + .thenApply(r -> { + List gatewayFlowRules = JSON.parseArray(r, GatewayFlowRule.class); + List entities = gatewayFlowRules.stream().map(rule -> GatewayFlowRuleEntity.fromGatewayFlowRule(app, ip, port, rule)).collect(Collectors.toList()); + return entities; + }); + } catch (Exception ex) { + logger.warn("Error when fetching gateway flow rules", ex); + return AsyncUtils.newFailedFuture(ex); + } + } + + public boolean modifyGatewayFlowRules(String app, String ip, int port, List rules) { + if (rules == null) { + return true; + } + + try { + AssertUtil.notEmpty(app, "Bad app name"); + AssertUtil.notEmpty(ip, "Bad machine IP"); + AssertUtil.isTrue(port > 0, "Bad machine port"); + String data = JSON.toJSONString( + rules.stream().map(r -> r.toGatewayFlowRule()).collect(Collectors.toList())); + Map params = new HashMap<>(2); + params.put("data", data); + String result = executeCommand(app, ip, port, MODIFY_GATEWAY_FLOW_RULE_PATH, params, true).get(); + logger.info("Modify gateway flow rules: {}", result); + return true; + } catch (Exception e) { + logger.warn("Error when modifying gateway apis", e); + return false; + } + } } diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AppController.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AppController.java index 0e98bef9..63548966 100755 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AppController.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/AppController.java @@ -63,17 +63,7 @@ public class AppController { return Result.ofSuccess(null); } List list = new ArrayList<>(appInfo.getMachines()); - Collections.sort(list, (o1, o2) -> { - int t = o1.getApp().compareTo(o2.getApp()); - if (t != 0) { - return t; - } - t = o1.getIp().compareTo(o2.getIp()); - if (t != 0) { - return t; - } - return o1.getPort().compareTo(o2.getPort()); - }); + Collections.sort(list, Comparator.comparing(MachineInfo::getApp).thenComparing(MachineInfo::getIp).thenComparingInt(MachineInfo::getPort)); return Result.ofSuccess(MachineInfoVo.fromMachineInfoList(list)); } diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/MachineRegistryController.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/MachineRegistryController.java index f2e7fe2a..9a9a7f76 100755 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/MachineRegistryController.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/MachineRegistryController.java @@ -27,6 +27,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; @Controller @@ -40,7 +41,7 @@ public class MachineRegistryController { @ResponseBody @RequestMapping("/machine") - public Result receiveHeartBeat(String app, Long version, String v, String hostname, String ip, Integer port) { + public Result receiveHeartBeat(String app, @RequestParam(value = "app_type", required = false, defaultValue = "0") Integer appType, Long version, String v, String hostname, String ip, Integer port) { if (app == null) { app = MachineDiscovery.UNKNOWN_APP_NAME; } @@ -59,6 +60,7 @@ public class MachineRegistryController { try { MachineInfo machineInfo = new MachineInfo(); machineInfo.setApp(app); + machineInfo.setAppType(appType); machineInfo.setHostname(hostname); machineInfo.setIp(ip); machineInfo.setPort(port); diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayApiController.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayApiController.java new file mode 100644 index 00000000..378e3cc3 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayApiController.java @@ -0,0 +1,270 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.dashboard.controller.gateway; + +import com.alibaba.csp.sentinel.dashboard.auth.AuthService; +import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiPredicateItemEntity; +import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api.AddApiReqVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api.ApiPredicateItemVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api.UpdateApiReqVo; +import com.alibaba.csp.sentinel.dashboard.repository.gateway.InMemApiDefinitionStore; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.CollectionUtils; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.util.*; + +import static com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants.*; + +/** + * Gateway api Controller for manage gateway api definitions. + * + * @author cdfive + * @since 1.7.0 + */ +@RestController +@RequestMapping(value = "/gateway/api") +public class GatewayApiController { + + private final Logger logger = LoggerFactory.getLogger(GatewayApiController.class); + + @Autowired + private InMemApiDefinitionStore repository; + + @Autowired + private SentinelApiClient sentinelApiClient; + + @Autowired + private AuthService authService; + + @GetMapping("/list.json") + public Result> queryApis(HttpServletRequest request, String app, String ip, Integer port) { + AuthService.AuthUser authUser = authService.getAuthUser(request); + authUser.authTarget(app, AuthService.PrivilegeType.READ_RULE); + + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isEmpty(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + + try { + List apis = sentinelApiClient.fetchApis(app, ip, port).get(); + repository.saveAll(apis); + return Result.ofSuccess(apis); + } catch (Throwable throwable) { + logger.error("queryApis error:", throwable); + return Result.ofThrowable(-1, throwable); + } + } + + @PostMapping("/new.json") + public Result addApi(HttpServletRequest request, @RequestBody AddApiReqVo reqVo) { + AuthService.AuthUser authUser = authService.getAuthUser(request); + + String app = reqVo.getApp(); + if (StringUtil.isBlank(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + + authUser.authTarget(app, AuthService.PrivilegeType.WRITE_RULE); + + ApiDefinitionEntity entity = new ApiDefinitionEntity(); + entity.setApp(app.trim()); + + String ip = reqVo.getIp(); + if (StringUtil.isBlank(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + entity.setIp(ip.trim()); + + Integer port = reqVo.getPort(); + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + entity.setPort(port); + + // API名称 + String apiName = reqVo.getApiName(); + if (StringUtil.isBlank(apiName)) { + return Result.ofFail(-1, "apiName can't be null or empty"); + } + entity.setApiName(apiName.trim()); + + // 匹配规则列表 + List predicateItems = reqVo.getPredicateItems(); + if (CollectionUtils.isEmpty(predicateItems)) { + return Result.ofFail(-1, "predicateItems can't empty"); + } + + List predicateItemEntities = new ArrayList<>(); + for (ApiPredicateItemVo predicateItem : predicateItems) { + ApiPredicateItemEntity predicateItemEntity = new ApiPredicateItemEntity(); + + // 匹配模式 + Integer matchStrategy = predicateItem.getMatchStrategy(); + if (!Arrays.asList(URL_MATCH_STRATEGY_EXACT, URL_MATCH_STRATEGY_PREFIX, URL_MATCH_STRATEGY_REGEX).contains(matchStrategy)) { + return Result.ofFail(-1, "invalid matchStrategy: " + matchStrategy); + } + predicateItemEntity.setMatchStrategy(matchStrategy); + + // 匹配串 + String pattern = predicateItem.getPattern(); + if (StringUtil.isBlank(pattern)) { + return Result.ofFail(-1, "pattern can't be null or empty"); + } + predicateItemEntity.setPattern(pattern); + + predicateItemEntities.add(predicateItemEntity); + } + entity.setPredicateItems(new LinkedHashSet<>(predicateItemEntities)); + + // 检查API名称不能重复 + List allApis = repository.findAllByMachine(MachineInfo.of(app.trim(), ip.trim(), port)); + if (allApis.stream().map(o -> o.getApiName()).anyMatch(o -> o.equals(apiName.trim()))) { + return Result.ofFail(-1, "apiName exists: " + apiName); + } + + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + + try { + entity = repository.save(entity); + } catch (Throwable throwable) { + logger.error("add gateway api error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishApis(app, ip, port)) { + logger.warn("publish gateway apis fail after add"); + } + + return Result.ofSuccess(entity); + } + + @PostMapping("/save.json") + public Result updateApi(HttpServletRequest request, @RequestBody UpdateApiReqVo reqVo) { + AuthService.AuthUser authUser = authService.getAuthUser(request); + + String app = reqVo.getApp(); + if (StringUtil.isBlank(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + + authUser.authTarget(app, AuthService.PrivilegeType.WRITE_RULE); + + Long id = reqVo.getId(); + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + ApiDefinitionEntity entity = repository.findById(id); + if (entity == null) { + return Result.ofFail(-1, "api does not exist, id=" + id); + } + + // 匹配规则列表 + List predicateItems = reqVo.getPredicateItems(); + if (CollectionUtils.isEmpty(predicateItems)) { + return Result.ofFail(-1, "predicateItems can't empty"); + } + + List predicateItemEntities = new ArrayList<>(); + for (ApiPredicateItemVo predicateItem : predicateItems) { + ApiPredicateItemEntity predicateItemEntity = new ApiPredicateItemEntity(); + + // 匹配模式 + int matchStrategy = predicateItem.getMatchStrategy(); + if (!Arrays.asList(URL_MATCH_STRATEGY_EXACT, URL_MATCH_STRATEGY_PREFIX, URL_MATCH_STRATEGY_REGEX).contains(matchStrategy)) { + return Result.ofFail(-1, "Invalid matchStrategy: " + matchStrategy); + } + predicateItemEntity.setMatchStrategy(matchStrategy); + + // 匹配串 + String pattern = predicateItem.getPattern(); + if (StringUtil.isBlank(pattern)) { + return Result.ofFail(-1, "pattern can't be null or empty"); + } + predicateItemEntity.setPattern(pattern); + + predicateItemEntities.add(predicateItemEntity); + } + entity.setPredicateItems(new LinkedHashSet<>(predicateItemEntities)); + + Date date = new Date(); + entity.setGmtModified(date); + + try { + entity = repository.save(entity); + } catch (Throwable throwable) { + logger.error("update gateway api error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishApis(app, entity.getIp(), entity.getPort())) { + logger.warn("publish gateway apis fail after update"); + } + + return Result.ofSuccess(entity); + } + + @PostMapping("/delete.json") + public Result deleteApi(HttpServletRequest request, Long id) { + AuthService.AuthUser authUser = authService.getAuthUser(request); + + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + ApiDefinitionEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + + authUser.authTarget(oldEntity.getApp(), AuthService.PrivilegeType.DELETE_RULE); + + try { + repository.delete(id); + } catch (Throwable throwable) { + logger.error("delete gateway api error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishApis(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort())) { + logger.warn("publish gateway apis fail after delete"); + } + + return Result.ofSuccess(id); + } + + private boolean publishApis(String app, String ip, Integer port) { + List apis = repository.findAllByMachine(MachineInfo.of(app, ip, port)); + return sentinelApiClient.modifyApis(app, ip, port, apis); + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayFlowRuleController.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayFlowRuleController.java new file mode 100644 index 00000000..7d84262f --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayFlowRuleController.java @@ -0,0 +1,443 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.dashboard.controller.gateway; + + +import com.alibaba.csp.sentinel.dashboard.auth.AuthService; +import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayParamFlowItemEntity; +import com.alibaba.csp.sentinel.dashboard.discovery.MachineInfo; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule.AddFlowRuleReqVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule.GatewayParamFlowItemVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule.UpdateFlowRuleReqVo; +import com.alibaba.csp.sentinel.dashboard.repository.gateway.InMemGatewayFlowRuleStore; +import com.alibaba.csp.sentinel.util.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import javax.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.Date; +import java.util.List; + +import static com.alibaba.csp.sentinel.slots.block.RuleConstant.*; +import static com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants.*; +import static com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity.*; + +/** + * Gateway flow rule Controller for manage gateway flow rules. + * + * @author cdfive + * @since 1.7.0 + */ +@RestController +@RequestMapping(value = "/gateway/flow") +public class GatewayFlowRuleController { + + private final Logger logger = LoggerFactory.getLogger(GatewayFlowRuleController.class); + + @Autowired + private InMemGatewayFlowRuleStore repository; + + @Autowired + private SentinelApiClient sentinelApiClient; + + @Autowired + private AuthService authService; + + @GetMapping("/list.json") + public Result> queryFlowRules(HttpServletRequest request, String app, String ip, Integer port) { + AuthService.AuthUser authUser = authService.getAuthUser(request); + authUser.authTarget(app, AuthService.PrivilegeType.READ_RULE); + + if (StringUtil.isEmpty(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + if (StringUtil.isEmpty(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + + try { + List rules = sentinelApiClient.fetchGatewayFlowRules(app, ip, port).get(); + repository.saveAll(rules); + return Result.ofSuccess(rules); + } catch (Throwable throwable) { + logger.error("query gateway flow rules error:", throwable); + return Result.ofThrowable(-1, throwable); + } + } + + @PostMapping("/new.json") + public Result addFlowRule(HttpServletRequest request, @RequestBody AddFlowRuleReqVo reqVo) { + AuthService.AuthUser authUser = authService.getAuthUser(request); + + String app = reqVo.getApp(); + if (StringUtil.isBlank(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + + authUser.authTarget(app, AuthService.PrivilegeType.WRITE_RULE); + + GatewayFlowRuleEntity entity = new GatewayFlowRuleEntity(); + entity.setApp(app.trim()); + + String ip = reqVo.getIp(); + if (StringUtil.isBlank(ip)) { + return Result.ofFail(-1, "ip can't be null or empty"); + } + entity.setIp(ip.trim()); + + Integer port = reqVo.getPort(); + if (port == null) { + return Result.ofFail(-1, "port can't be null"); + } + entity.setPort(port); + + // API类型, Route ID或API分组 + Integer resourceMode = reqVo.getResourceMode(); + if (resourceMode == null) { + return Result.ofFail(-1, "resourceMode can't be null"); + } + if (!Arrays.asList(RESOURCE_MODE_ROUTE_ID, RESOURCE_MODE_CUSTOM_API_NAME).contains(resourceMode)) { + return Result.ofFail(-1, "invalid resourceMode: " + resourceMode); + } + entity.setResourceMode(resourceMode); + + // API名称 + String resource = reqVo.getResource(); + if (StringUtil.isBlank(resource)) { + return Result.ofFail(-1, "resource can't be null or empty"); + } + entity.setResource(resource.trim()); + + // 针对请求属性 + GatewayParamFlowItemVo paramItem = reqVo.getParamItem(); + if (paramItem != null) { + GatewayParamFlowItemEntity itemEntity = new GatewayParamFlowItemEntity(); + entity.setParamItem(itemEntity); + + // 参数属性 0-ClientIP 1-Remote Host 2-Header 3-URL参数 4-Cookie + Integer parseStrategy = paramItem.getParseStrategy(); + if (!Arrays.asList(PARAM_PARSE_STRATEGY_CLIENT_IP, PARAM_PARSE_STRATEGY_HOST, PARAM_PARSE_STRATEGY_HEADER + , PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) { + return Result.ofFail(-1, "invalid parseStrategy: " + parseStrategy); + } + itemEntity.setParseStrategy(paramItem.getParseStrategy()); + + // 当参数属性为2-Header 3-URL参数 4-Cookie时,参数名称必填 + if (Arrays.asList(PARAM_PARSE_STRATEGY_HEADER, PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) { + // 参数名称 + String fieldName = paramItem.getFieldName(); + if (StringUtil.isBlank(fieldName)) { + return Result.ofFail(-1, "fieldName can't be null or empty"); + } + itemEntity.setFieldName(paramItem.getFieldName()); + + String pattern = paramItem.getPattern(); + // 如果匹配串不为空,验证匹配模式 + if (StringUtil.isNotEmpty(pattern)) { + itemEntity.setPattern(pattern); + + Integer matchStrategy = paramItem.getMatchStrategy(); + if (!Arrays.asList(PARAM_MATCH_STRATEGY_EXACT, PARAM_MATCH_STRATEGY_CONTAINS, PARAM_MATCH_STRATEGY_REGEX).contains(matchStrategy)) { + return Result.ofFail(-1, "invalid matchStrategy: " + matchStrategy); + } + itemEntity.setMatchStrategy(matchStrategy); + } + } + } + + // 阈值类型 0-线程数 1-QPS + Integer grade = reqVo.getGrade(); + if (grade == null) { + return Result.ofFail(-1, "grade can't be null"); + } + if (!Arrays.asList(FLOW_GRADE_THREAD, FLOW_GRADE_QPS).contains(grade)) { + return Result.ofFail(-1, "invalid grade: " + grade); + } + entity.setGrade(grade); + + // QPS阈值 + Double count = reqVo.getCount(); + if (count == null) { + return Result.ofFail(-1, "count can't be null"); + } + if (count < 0) { + return Result.ofFail(-1, "count should be at lease zero"); + } + entity.setCount(count); + + // 间隔 + Long interval = reqVo.getInterval(); + if (interval == null) { + return Result.ofFail(-1, "interval can't be null"); + } + if (interval <= 0) { + return Result.ofFail(-1, "interval should be greater than zero"); + } + entity.setInterval(interval); + + // 间隔单位 + Integer intervalUnit = reqVo.getIntervalUnit(); + if (intervalUnit == null) { + return Result.ofFail(-1, "intervalUnit can't be null"); + } + if (!Arrays.asList(INTERVAL_UNIT_SECOND, INTERVAL_UNIT_MINUTE, INTERVAL_UNIT_HOUR, INTERVAL_UNIT_DAY).contains(intervalUnit)) { + return Result.ofFail(-1, "Invalid intervalUnit: " + intervalUnit); + } + entity.setIntervalUnit(intervalUnit); + + // 流控方式 0-快速失败 2-匀速排队 + Integer controlBehavior = reqVo.getControlBehavior(); + if (controlBehavior == null) { + return Result.ofFail(-1, "controlBehavior can't be null"); + } + if (!Arrays.asList(CONTROL_BEHAVIOR_DEFAULT, CONTROL_BEHAVIOR_RATE_LIMITER).contains(controlBehavior)) { + return Result.ofFail(-1, "invalid controlBehavior: " + controlBehavior); + } + entity.setControlBehavior(controlBehavior); + + if (CONTROL_BEHAVIOR_DEFAULT == controlBehavior) { + // 0-快速失败, 则Burst size必填 + Integer burst = reqVo.getBurst(); + if (burst == null) { + return Result.ofFail(-1, "burst can't be null"); + } + if (burst < 0) { + return Result.ofFail(-1, "invalid burst: " + burst); + } + entity.setBurst(burst); + } else if (CONTROL_BEHAVIOR_RATE_LIMITER == controlBehavior) { + // 1-匀速排队, 则超时时间必填 + Integer maxQueueingTimeoutMs = reqVo.getMaxQueueingTimeoutMs(); + if (maxQueueingTimeoutMs == null) { + return Result.ofFail(-1, "maxQueueingTimeoutMs can't be null"); + } + if (maxQueueingTimeoutMs < 0) { + return Result.ofFail(-1, "invalid maxQueueingTimeoutMs: " + maxQueueingTimeoutMs); + } + entity.setMaxQueueingTimeoutMs(maxQueueingTimeoutMs); + } + + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + + try { + entity = repository.save(entity); + } catch (Throwable throwable) { + logger.error("add gateway flow rule error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishRules(app, ip, port)) { + logger.warn("publish gateway flow rules fail after add"); + } + + return Result.ofSuccess(entity); + } + + @PostMapping("/save.json") + public Result updateFlowRule(HttpServletRequest request, @RequestBody UpdateFlowRuleReqVo reqVo) { + AuthService.AuthUser authUser = authService.getAuthUser(request); + + String app = reqVo.getApp(); + if (StringUtil.isBlank(app)) { + return Result.ofFail(-1, "app can't be null or empty"); + } + + authUser.authTarget(app, AuthService.PrivilegeType.WRITE_RULE); + + Long id = reqVo.getId(); + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + GatewayFlowRuleEntity entity = repository.findById(id); + if (entity == null) { + return Result.ofFail(-1, "gateway flow rule does not exist, id=" + id); + } + + // 针对请求属性 + GatewayParamFlowItemVo paramItem = reqVo.getParamItem(); + if (paramItem != null) { + GatewayParamFlowItemEntity itemEntity = new GatewayParamFlowItemEntity(); + entity.setParamItem(itemEntity); + + // 参数属性 0-ClientIP 1-Remote Host 2-Header 3-URL参数 4-Cookie + Integer parseStrategy = paramItem.getParseStrategy(); + if (!Arrays.asList(PARAM_PARSE_STRATEGY_CLIENT_IP, PARAM_PARSE_STRATEGY_HOST, PARAM_PARSE_STRATEGY_HEADER + , PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) { + return Result.ofFail(-1, "invalid parseStrategy: " + parseStrategy); + } + itemEntity.setParseStrategy(paramItem.getParseStrategy()); + + // 当参数属性为2-Header 3-URL参数 4-Cookie时,参数名称必填 + if (Arrays.asList(PARAM_PARSE_STRATEGY_HEADER, PARAM_PARSE_STRATEGY_URL_PARAM, PARAM_PARSE_STRATEGY_COOKIE).contains(parseStrategy)) { + // 参数名称 + String fieldName = paramItem.getFieldName(); + if (StringUtil.isBlank(fieldName)) { + return Result.ofFail(-1, "fieldName can't be null or empty"); + } + itemEntity.setFieldName(paramItem.getFieldName()); + + String pattern = paramItem.getPattern(); + // 如果匹配串不为空,验证匹配模式 + if (StringUtil.isNotEmpty(pattern)) { + itemEntity.setPattern(pattern); + + Integer matchStrategy = paramItem.getMatchStrategy(); + if (!Arrays.asList(PARAM_MATCH_STRATEGY_EXACT, PARAM_MATCH_STRATEGY_CONTAINS, PARAM_MATCH_STRATEGY_REGEX).contains(matchStrategy)) { + return Result.ofFail(-1, "invalid matchStrategy: " + matchStrategy); + } + itemEntity.setMatchStrategy(matchStrategy); + } + } + } else { + entity.setParamItem(null); + } + + // 阈值类型 0-线程数 1-QPS + Integer grade = reqVo.getGrade(); + if (grade == null) { + return Result.ofFail(-1, "grade can't be null"); + } + if (!Arrays.asList(FLOW_GRADE_THREAD, FLOW_GRADE_QPS).contains(grade)) { + return Result.ofFail(-1, "invalid grade: " + grade); + } + entity.setGrade(grade); + + // QPS阈值 + Double count = reqVo.getCount(); + if (count == null) { + return Result.ofFail(-1, "count can't be null"); + } + if (count < 0) { + return Result.ofFail(-1, "count should be at lease zero"); + } + entity.setCount(count); + + // 间隔 + Long interval = reqVo.getInterval(); + if (interval == null) { + return Result.ofFail(-1, "interval can't be null"); + } + if (interval <= 0) { + return Result.ofFail(-1, "interval should be greater than zero"); + } + entity.setInterval(interval); + + // 间隔单位 + Integer intervalUnit = reqVo.getIntervalUnit(); + if (intervalUnit == null) { + return Result.ofFail(-1, "intervalUnit can't be null"); + } + if (!Arrays.asList(INTERVAL_UNIT_SECOND, INTERVAL_UNIT_MINUTE, INTERVAL_UNIT_HOUR, INTERVAL_UNIT_DAY).contains(intervalUnit)) { + return Result.ofFail(-1, "Invalid intervalUnit: " + intervalUnit); + } + entity.setIntervalUnit(intervalUnit); + + // 流控方式 0-快速失败 2-匀速排队 + Integer controlBehavior = reqVo.getControlBehavior(); + if (controlBehavior == null) { + return Result.ofFail(-1, "controlBehavior can't be null"); + } + if (!Arrays.asList(CONTROL_BEHAVIOR_DEFAULT, CONTROL_BEHAVIOR_RATE_LIMITER).contains(controlBehavior)) { + return Result.ofFail(-1, "invalid controlBehavior: " + controlBehavior); + } + entity.setControlBehavior(controlBehavior); + + if (CONTROL_BEHAVIOR_DEFAULT == controlBehavior) { + // 0-快速失败, 则Burst size必填 + Integer burst = reqVo.getBurst(); + if (burst == null) { + return Result.ofFail(-1, "burst can't be null"); + } + if (burst < 0) { + return Result.ofFail(-1, "invalid burst: " + burst); + } + entity.setBurst(burst); + } else if (CONTROL_BEHAVIOR_RATE_LIMITER == controlBehavior) { + // 2-匀速排队, 则超时时间必填 + Integer maxQueueingTimeoutMs = reqVo.getMaxQueueingTimeoutMs(); + if (maxQueueingTimeoutMs == null) { + return Result.ofFail(-1, "maxQueueingTimeoutMs can't be null"); + } + if (maxQueueingTimeoutMs < 0) { + return Result.ofFail(-1, "invalid maxQueueingTimeoutMs: " + maxQueueingTimeoutMs); + } + entity.setMaxQueueingTimeoutMs(maxQueueingTimeoutMs); + } + + Date date = new Date(); + entity.setGmtModified(date); + + try { + entity = repository.save(entity); + } catch (Throwable throwable) { + logger.error("update gateway flow rule error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishRules(app, entity.getIp(), entity.getPort())) { + logger.warn("publish gateway flow rules fail after update"); + } + + return Result.ofSuccess(entity); + } + + + @PostMapping("/delete.json") + public Result deleteFlowRule(HttpServletRequest request, Long id) { + AuthService.AuthUser authUser = authService.getAuthUser(request); + + if (id == null) { + return Result.ofFail(-1, "id can't be null"); + } + + GatewayFlowRuleEntity oldEntity = repository.findById(id); + if (oldEntity == null) { + return Result.ofSuccess(null); + } + + authUser.authTarget(oldEntity.getApp(), AuthService.PrivilegeType.DELETE_RULE); + + try { + repository.delete(id); + } catch (Throwable throwable) { + logger.error("delete gateway flow rule error:", throwable); + return Result.ofThrowable(-1, throwable); + } + + if (!publishRules(oldEntity.getApp(), oldEntity.getIp(), oldEntity.getPort())) { + logger.warn("publish gateway flow rules fail after delete"); + } + + return Result.ofSuccess(id); + } + + private boolean publishRules(String app, String ip, Integer port) { + List rules = repository.findAllByMachine(MachineInfo.of(app, ip, port)); + return sentinelApiClient.modifyGatewayFlowRules(app, ip, port, rules); + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/ApplicationEntity.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/ApplicationEntity.java index 51dc229e..44c10daa 100755 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/ApplicationEntity.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/ApplicationEntity.java @@ -28,6 +28,7 @@ public class ApplicationEntity { private Date gmtCreate; private Date gmtModified; private String app; + private Integer appType; private String activeConsole; private Date lastFetch; @@ -63,6 +64,14 @@ public class ApplicationEntity { this.app = app; } + public Integer getAppType() { + return appType; + } + + public void setAppType(Integer appType) { + this.appType = appType; + } + public String getActiveConsole() { return activeConsole; } @@ -80,7 +89,7 @@ public class ApplicationEntity { } public AppInfo toAppInfo() { - return new AppInfo(app); + return new AppInfo(app, appType); } @Override diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/ApiDefinitionEntity.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/ApiDefinitionEntity.java new file mode 100644 index 00000000..b042e0ad --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/ApiDefinitionEntity.java @@ -0,0 +1,208 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway; + +import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiDefinition; +import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPathPredicateItem; +import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.RuleEntity; +import com.alibaba.csp.sentinel.slots.block.Rule; + +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.Objects; +import java.util.Set; + +/** + * Entity for {@link ApiDefinition}. + * + * @author cdfive + * @since 1.7.0 + */ +public class ApiDefinitionEntity implements RuleEntity { + + private Long id; + private String app; + private String ip; + private Integer port; + + private Date gmtCreate; + private Date gmtModified; + + private String apiName; + private Set predicateItems; + + public static ApiDefinitionEntity fromApiDefinition(String app, String ip, Integer port, ApiDefinition apiDefinition) { + ApiDefinitionEntity entity = new ApiDefinitionEntity(); + entity.setApp(app); + entity.setIp(ip); + entity.setPort(port); + entity.setApiName(apiDefinition.getApiName()); + + Set predicateItems = new LinkedHashSet<>(); + entity.setPredicateItems(predicateItems); + + Set apiPredicateItems = apiDefinition.getPredicateItems(); + if (apiPredicateItems != null) { + for (ApiPredicateItem apiPredicateItem : apiPredicateItems) { + ApiPredicateItemEntity itemEntity = new ApiPredicateItemEntity(); + predicateItems.add(itemEntity); + ApiPathPredicateItem pathPredicateItem = (ApiPathPredicateItem) apiPredicateItem; + itemEntity.setPattern(pathPredicateItem.getPattern()); + itemEntity.setMatchStrategy(pathPredicateItem.getMatchStrategy()); + } + } + + return entity; + } + + public ApiDefinition toApiDefinition() { + ApiDefinition apiDefinition = new ApiDefinition(); + apiDefinition.setApiName(apiName); + + Set apiPredicateItems = new LinkedHashSet<>(); + apiDefinition.setPredicateItems(apiPredicateItems); + + if (predicateItems != null) { + for (ApiPredicateItemEntity predicateItem : predicateItems) { + ApiPathPredicateItem apiPredicateItem = new ApiPathPredicateItem(); + apiPredicateItems.add(apiPredicateItem); + apiPredicateItem.setMatchStrategy(predicateItem.getMatchStrategy()); + apiPredicateItem.setPattern(predicateItem.getPattern()); + } + } + + return apiDefinition; + } + + public ApiDefinitionEntity() { + + } + + public ApiDefinitionEntity(String apiName, Set predicateItems) { + this.apiName = apiName; + this.predicateItems = predicateItems; + } + + public String getApiName() { + return apiName; + } + + public void setApiName(String apiName) { + this.apiName = apiName; + } + + public Set getPredicateItems() { + return predicateItems; + } + + public void setPredicateItems(Set predicateItems) { + this.predicateItems = predicateItems; + } + + @Override + public Long getId() { + return id; + } + + @Override + public void setId(Long id) { + this.id = id; + } + + @Override + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + @Override + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + @Override + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + @Override + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + public Date getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + } + + @Override + public Rule toRule() { + return null; + } + + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + ApiDefinitionEntity entity = (ApiDefinitionEntity) o; + return Objects.equals(id, entity.id) && + Objects.equals(app, entity.app) && + Objects.equals(ip, entity.ip) && + Objects.equals(port, entity.port) && + Objects.equals(gmtCreate, entity.gmtCreate) && + Objects.equals(gmtModified, entity.gmtModified) && + Objects.equals(apiName, entity.apiName) && + Objects.equals(predicateItems, entity.predicateItems); + } + + @Override + public int hashCode() { + return Objects.hash(id, app, ip, port, gmtCreate, gmtModified, apiName, predicateItems); + } + + @Override + public String toString() { + return "ApiDefinitionEntity{" + + "id=" + id + + ", app='" + app + '\'' + + ", ip='" + ip + '\'' + + ", port=" + port + + ", gmtCreate=" + gmtCreate + + ", gmtModified=" + gmtModified + + ", apiName='" + apiName + '\'' + + ", predicateItems=" + predicateItems + + '}'; + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/ApiPredicateItemEntity.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/ApiPredicateItemEntity.java new file mode 100644 index 00000000..1d6058cd --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/ApiPredicateItemEntity.java @@ -0,0 +1,79 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway; + +import com.alibaba.csp.sentinel.adapter.gateway.common.api.ApiPredicateItem; + +import java.util.Objects; + +/** + * Entity for {@link ApiPredicateItem}. + * + * @author cdfive + * @since 1.7.0 + */ +public class ApiPredicateItemEntity { + + private String pattern; + + private Integer matchStrategy; + + public ApiPredicateItemEntity() { + } + + public ApiPredicateItemEntity(String pattern, int matchStrategy) { + this.pattern = pattern; + this.matchStrategy = matchStrategy; + } + + public String getPattern() { + return pattern; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public Integer getMatchStrategy() { + return matchStrategy; + } + + public void setMatchStrategy(Integer matchStrategy) { + this.matchStrategy = matchStrategy; + } + + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + ApiPredicateItemEntity that = (ApiPredicateItemEntity) o; + return Objects.equals(pattern, that.pattern) && + Objects.equals(matchStrategy, that.matchStrategy); + } + + @Override + public int hashCode() { + return Objects.hash(pattern, matchStrategy); + } + + @Override + public String toString() { + return "ApiPredicateItemEntity{" + + "pattern='" + pattern + '\'' + + ", matchStrategy=" + matchStrategy + + '}'; + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/GatewayFlowRuleEntity.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/GatewayFlowRuleEntity.java new file mode 100644 index 00000000..391ea41f --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/GatewayFlowRuleEntity.java @@ -0,0 +1,354 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway; + +import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule; +import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayParamFlowItem; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.rule.RuleEntity; +import com.alibaba.csp.sentinel.slots.block.Rule; + +import java.util.Date; +import java.util.Objects; + +/** + * Entity for {@link GatewayFlowRule}. + * + * @author cdfive + * @since 1.7.0 + */ +public class GatewayFlowRuleEntity implements RuleEntity { + + /**间隔单位*/ + /**0-秒*/ + public static final int INTERVAL_UNIT_SECOND = 0; + /**1-分*/ + public static final int INTERVAL_UNIT_MINUTE = 1; + /**2-时*/ + public static final int INTERVAL_UNIT_HOUR = 2; + /**3-天*/ + public static final int INTERVAL_UNIT_DAY = 3; + + private Long id; + private String app; + private String ip; + private Integer port; + + private Date gmtCreate; + private Date gmtModified; + + private String resource; + private Integer resourceMode; + + private Integer grade; + private Double count; + private Long interval; + private Integer intervalUnit; + + private Integer controlBehavior; + private Integer burst; + + private Integer maxQueueingTimeoutMs; + + private GatewayParamFlowItemEntity paramItem; + + public static Long calIntervalSec(Long interval, Integer intervalUnit) { + switch (intervalUnit) { + case INTERVAL_UNIT_SECOND: + return interval; + case INTERVAL_UNIT_MINUTE: + return interval * 60; + case INTERVAL_UNIT_HOUR: + return interval * 60 * 60; + case INTERVAL_UNIT_DAY: + return interval * 60 * 60 * 24; + default: + break; + } + + throw new IllegalArgumentException("Invalid intervalUnit: " + intervalUnit); + } + + public static Object[] parseIntervalSec(Long intervalSec) { + if (intervalSec % (60 * 60 * 24) == 0) { + return new Object[] {intervalSec / (60 * 60 * 24), INTERVAL_UNIT_DAY}; + } + + if (intervalSec % (60 * 60 ) == 0) { + return new Object[] {intervalSec / (60 * 60), INTERVAL_UNIT_HOUR}; + } + + if (intervalSec % 60 == 0) { + return new Object[] {intervalSec / 60, INTERVAL_UNIT_MINUTE}; + } + + return new Object[] {intervalSec, INTERVAL_UNIT_SECOND}; + } + + public GatewayFlowRule toGatewayFlowRule() { + GatewayFlowRule rule = new GatewayFlowRule(); + rule.setResource(resource); + rule.setResourceMode(resourceMode); + + rule.setGrade(grade); + rule.setCount(count); + rule.setIntervalSec(calIntervalSec(interval, intervalUnit)); + + rule.setControlBehavior(controlBehavior); + + if (burst != null) { + rule.setBurst(burst); + } + + if (maxQueueingTimeoutMs != null) { + rule.setMaxQueueingTimeoutMs(maxQueueingTimeoutMs); + } + + if (paramItem != null) { + GatewayParamFlowItem ruleItem = new GatewayParamFlowItem(); + rule.setParamItem(ruleItem); + ruleItem.setParseStrategy(paramItem.getParseStrategy()); + ruleItem.setFieldName(paramItem.getFieldName()); + ruleItem.setPattern(paramItem.getPattern()); + + if (paramItem.getMatchStrategy() != null) { + ruleItem.setMatchStrategy(paramItem.getMatchStrategy()); + } + } + + return rule; + } + + public static GatewayFlowRuleEntity fromGatewayFlowRule(String app, String ip, Integer port, GatewayFlowRule rule) { + GatewayFlowRuleEntity entity = new GatewayFlowRuleEntity(); + entity.setApp(app); + entity.setIp(ip); + entity.setPort(port); + + entity.setResource(rule.getResource()); + entity.setResourceMode(rule.getResourceMode()); + + entity.setGrade(rule.getGrade()); + entity.setCount(rule.getCount()); + Object[] intervalSecResult = parseIntervalSec(rule.getIntervalSec()); + entity.setInterval((Long) intervalSecResult[0]); + entity.setIntervalUnit((Integer) intervalSecResult[1]); + + entity.setControlBehavior(rule.getControlBehavior()); + entity.setBurst(rule.getBurst()); + entity.setMaxQueueingTimeoutMs(rule.getMaxQueueingTimeoutMs()); + + GatewayParamFlowItem paramItem = rule.getParamItem(); + if (paramItem != null) { + GatewayParamFlowItemEntity itemEntity = new GatewayParamFlowItemEntity(); + entity.setParamItem(itemEntity); + itemEntity.setParseStrategy(paramItem.getParseStrategy()); + itemEntity.setFieldName(paramItem.getFieldName()); + itemEntity.setPattern(paramItem.getPattern()); + itemEntity.setMatchStrategy(paramItem.getMatchStrategy()); + } + + return entity; + } + + @Override + public Long getId() { + return id; + } + + @Override + public void setId(Long id) { + this.id = id; + } + + @Override + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + @Override + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + @Override + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + @Override + public Date getGmtCreate() { + return gmtCreate; + } + + public void setGmtCreate(Date gmtCreate) { + this.gmtCreate = gmtCreate; + } + + @Override + public Rule toRule() { + return null; + } + + public Date getGmtModified() { + return gmtModified; + } + + public void setGmtModified(Date gmtModified) { + this.gmtModified = gmtModified; + } + + public GatewayParamFlowItemEntity getParamItem() { + return paramItem; + } + + public void setParamItem(GatewayParamFlowItemEntity paramItem) { + this.paramItem = paramItem; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public Integer getResourceMode() { + return resourceMode; + } + + public void setResourceMode(Integer resourceMode) { + this.resourceMode = resourceMode; + } + + public Integer getGrade() { + return grade; + } + + public void setGrade(Integer grade) { + this.grade = grade; + } + + public Double getCount() { + return count; + } + + public void setCount(Double count) { + this.count = count; + } + + public Long getInterval() { + return interval; + } + + public void setInterval(Long interval) { + this.interval = interval; + } + + public Integer getIntervalUnit() { + return intervalUnit; + } + + public void setIntervalUnit(Integer intervalUnit) { + this.intervalUnit = intervalUnit; + } + + public Integer getControlBehavior() { + return controlBehavior; + } + + public void setControlBehavior(Integer controlBehavior) { + this.controlBehavior = controlBehavior; + } + + public Integer getBurst() { + return burst; + } + + public void setBurst(Integer burst) { + this.burst = burst; + } + + public Integer getMaxQueueingTimeoutMs() { + return maxQueueingTimeoutMs; + } + + public void setMaxQueueingTimeoutMs(Integer maxQueueingTimeoutMs) { + this.maxQueueingTimeoutMs = maxQueueingTimeoutMs; + } + + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + GatewayFlowRuleEntity that = (GatewayFlowRuleEntity) o; + return Objects.equals(id, that.id) && + Objects.equals(app, that.app) && + Objects.equals(ip, that.ip) && + Objects.equals(port, that.port) && + Objects.equals(gmtCreate, that.gmtCreate) && + Objects.equals(gmtModified, that.gmtModified) && + Objects.equals(resource, that.resource) && + Objects.equals(resourceMode, that.resourceMode) && + Objects.equals(grade, that.grade) && + Objects.equals(count, that.count) && + Objects.equals(interval, that.interval) && + Objects.equals(intervalUnit, that.intervalUnit) && + Objects.equals(controlBehavior, that.controlBehavior) && + Objects.equals(burst, that.burst) && + Objects.equals(maxQueueingTimeoutMs, that.maxQueueingTimeoutMs) && + Objects.equals(paramItem, that.paramItem); + } + + @Override + public int hashCode() { + return Objects.hash(id, app, ip, port, gmtCreate, gmtModified, resource, resourceMode, grade, count, interval, intervalUnit, controlBehavior, burst, maxQueueingTimeoutMs, paramItem); + } + + @Override + public String toString() { + return "GatewayFlowRuleEntity{" + + "id=" + id + + ", app='" + app + '\'' + + ", ip='" + ip + '\'' + + ", port=" + port + + ", gmtCreate=" + gmtCreate + + ", gmtModified=" + gmtModified + + ", resource='" + resource + '\'' + + ", resourceMode=" + resourceMode + + ", grade=" + grade + + ", count=" + count + + ", interval=" + interval + + ", intervalUnit=" + intervalUnit + + ", controlBehavior=" + controlBehavior + + ", burst=" + burst + + ", maxQueueingTimeoutMs=" + maxQueueingTimeoutMs + + ", paramItem=" + paramItem + + '}'; + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/GatewayParamFlowItemEntity.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/GatewayParamFlowItemEntity.java new file mode 100644 index 00000000..4da71c8e --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/datasource/entity/gateway/GatewayParamFlowItemEntity.java @@ -0,0 +1,95 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway; + +import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayParamFlowItem; + +import java.util.Objects; + +/** + * Entity for {@link GatewayParamFlowItem}. + * + * @author cdfive + * @since 1.7.0 + */ +public class GatewayParamFlowItemEntity { + + private Integer parseStrategy; + + private String fieldName; + + private String pattern; + + private Integer matchStrategy; + + public Integer getParseStrategy() { + return parseStrategy; + } + + public void setParseStrategy(Integer parseStrategy) { + this.parseStrategy = parseStrategy; + } + + public String getFieldName() { + return fieldName; + } + + public void setFieldName(String fieldName) { + this.fieldName = fieldName; + } + + public String getPattern() { + return pattern; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public Integer getMatchStrategy() { + return matchStrategy; + } + + public void setMatchStrategy(Integer matchStrategy) { + this.matchStrategy = matchStrategy; + } + + @Override + public boolean equals(Object o) { + if (this == o) { return true; } + if (o == null || getClass() != o.getClass()) { return false; } + GatewayParamFlowItemEntity that = (GatewayParamFlowItemEntity) o; + return Objects.equals(parseStrategy, that.parseStrategy) && + Objects.equals(fieldName, that.fieldName) && + Objects.equals(pattern, that.pattern) && + Objects.equals(matchStrategy, that.matchStrategy); + } + + @Override + public int hashCode() { + return Objects.hash(parseStrategy, fieldName, pattern, matchStrategy); + } + + @Override + public String toString() { + return "GatewayParamFlowItemEntity{" + + "parseStrategy=" + parseStrategy + + ", fieldName='" + fieldName + '\'' + + ", pattern='" + pattern + '\'' + + ", matchStrategy=" + matchStrategy + + '}'; + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/AppInfo.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/AppInfo.java index 01d3183f..f7697698 100755 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/AppInfo.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/AppInfo.java @@ -28,6 +28,8 @@ public class AppInfo { private String app = ""; + private Integer appType = 0; + private Set machines = ConcurrentHashMap.newKeySet(); public AppInfo() {} @@ -36,6 +38,11 @@ public class AppInfo { this.app = app; } + public AppInfo(String app, Integer appType) { + this.app = app; + this.appType = appType; + } + public String getApp() { return app; } @@ -44,6 +51,14 @@ public class AppInfo { this.app = app; } + public Integer getAppType() { + return appType; + } + + public void setAppType(Integer appType) { + this.appType = appType; + } + /** * Get the current machines. * diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/MachineInfo.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/MachineInfo.java index 9c65f365..afcc8124 100755 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/MachineInfo.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/MachineInfo.java @@ -23,6 +23,7 @@ import com.alibaba.csp.sentinel.util.StringUtil; public class MachineInfo implements Comparable { private String app = ""; + private Integer appType = 0; private String hostname = ""; private String ip = ""; private Integer port = -1; @@ -62,6 +63,14 @@ public class MachineInfo implements Comparable { this.app = app; } + public Integer getAppType() { + return appType; + } + + public void setAppType(Integer appType) { + this.appType = appType; + } + public String getHostname() { return hostname; } @@ -139,6 +148,7 @@ public class MachineInfo implements Comparable { public String toString() { return new StringBuilder("MachineInfo {") .append("app='").append(app).append('\'') + .append(",appType='").append(appType).append('\'') .append(", hostname='").append(hostname).append('\'') .append(", ip='").append(ip).append('\'') .append(", port=").append(port) diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/SimpleMachineDiscovery.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/SimpleMachineDiscovery.java index 23ce943d..a4ab83ac 100755 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/SimpleMachineDiscovery.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/discovery/SimpleMachineDiscovery.java @@ -37,7 +37,7 @@ public class SimpleMachineDiscovery implements MachineDiscovery { @Override public long addMachine(MachineInfo machineInfo) { AssertUtil.notNull(machineInfo, "machineInfo cannot be null"); - AppInfo appInfo = apps.computeIfAbsent(machineInfo.getApp(), AppInfo::new); + AppInfo appInfo = apps.computeIfAbsent(machineInfo.getApp(), o -> new AppInfo(machineInfo.getApp(), machineInfo.getAppType())); appInfo.addMachine(machineInfo); return 1; } diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/AddApiReqVo.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/AddApiReqVo.java new file mode 100644 index 00000000..445f445c --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/AddApiReqVo.java @@ -0,0 +1,78 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api; + +import java.util.List; + +/** + * Value Object for add gateway api. + * + * @author cdfive + * @since 1.7.0 + */ +public class AddApiReqVo { + + private String app; + + private String ip; + + private Integer port; + + private String apiName; + + private List predicateItems; + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public String getApiName() { + return apiName; + } + + public void setApiName(String apiName) { + this.apiName = apiName; + } + + public List getPredicateItems() { + return predicateItems; + } + + public void setPredicateItems(List predicateItems) { + this.predicateItems = predicateItems; + } +} + diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/ApiPredicateItemVo.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/ApiPredicateItemVo.java new file mode 100644 index 00000000..d621ddf5 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/ApiPredicateItemVo.java @@ -0,0 +1,45 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api; + +/** + * Value Object for add or update gateway api. + * + * @author cdfive + * @since 1.7.0 + */ +public class ApiPredicateItemVo { + + private String pattern; + + private Integer matchStrategy; + + public String getPattern() { + return pattern; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public Integer getMatchStrategy() { + return matchStrategy; + } + + public void setMatchStrategy(Integer matchStrategy) { + this.matchStrategy = matchStrategy; + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/UpdateApiReqVo.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/UpdateApiReqVo.java new file mode 100644 index 00000000..3dfd96a2 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/api/UpdateApiReqVo.java @@ -0,0 +1,57 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.api; + +import java.util.List; + +/** + * Value Object for update gateway api. + * + * @author cdfive + * @since 1.7.0 + */ +public class UpdateApiReqVo { + + private Long id; + + private String app; + + private List predicateItems; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public List getPredicateItems() { + return predicateItems; + } + + public void setPredicateItems(List predicateItems) { + this.predicateItems = predicateItems; + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/AddFlowRuleReqVo.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/AddFlowRuleReqVo.java new file mode 100644 index 00000000..57cc9e2a --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/AddFlowRuleReqVo.java @@ -0,0 +1,155 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule; + +/** + * Value Object for add gateway flow rule. + * + * @author cdfive + * @since 1.7.0 + */ +public class AddFlowRuleReqVo { + + private String app; + + private String ip; + + private Integer port; + + private String resource; + + private Integer resourceMode; + + private Integer grade; + + private Double count; + + private Long interval; + + private Integer intervalUnit; + + private Integer controlBehavior; + + private Integer burst; + + private Integer maxQueueingTimeoutMs; + + private GatewayParamFlowItemVo paramItem; + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public String getIp() { + return ip; + } + + public void setIp(String ip) { + this.ip = ip; + } + + public Integer getPort() { + return port; + } + + public void setPort(Integer port) { + this.port = port; + } + + public String getResource() { + return resource; + } + + public void setResource(String resource) { + this.resource = resource; + } + + public Integer getResourceMode() { + return resourceMode; + } + + public void setResourceMode(Integer resourceMode) { + this.resourceMode = resourceMode; + } + + public Integer getGrade() { + return grade; + } + + public void setGrade(Integer grade) { + this.grade = grade; + } + + public Double getCount() { + return count; + } + + public void setCount(Double count) { + this.count = count; + } + + public Long getInterval() { + return interval; + } + + public void setInterval(Long interval) { + this.interval = interval; + } + + public Integer getIntervalUnit() { + return intervalUnit; + } + + public void setIntervalUnit(Integer intervalUnit) { + this.intervalUnit = intervalUnit; + } + + public Integer getControlBehavior() { + return controlBehavior; + } + + public void setControlBehavior(Integer controlBehavior) { + this.controlBehavior = controlBehavior; + } + + public Integer getBurst() { + return burst; + } + + public void setBurst(Integer burst) { + this.burst = burst; + } + + public Integer getMaxQueueingTimeoutMs() { + return maxQueueingTimeoutMs; + } + + public void setMaxQueueingTimeoutMs(Integer maxQueueingTimeoutMs) { + this.maxQueueingTimeoutMs = maxQueueingTimeoutMs; + } + + public GatewayParamFlowItemVo getParamItem() { + return paramItem; + } + + public void setParamItem(GatewayParamFlowItemVo paramItem) { + this.paramItem = paramItem; + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/GatewayParamFlowItemVo.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/GatewayParamFlowItemVo.java new file mode 100644 index 00000000..af24fed9 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/GatewayParamFlowItemVo.java @@ -0,0 +1,65 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule; + +/** + * Value Object for add or update gateway flow rule. + * + * @author cdfive + * @since 1.7.0 + */ +public class GatewayParamFlowItemVo { + + private Integer parseStrategy; + + private String fieldName; + + private String pattern; + + private Integer matchStrategy; + + public Integer getParseStrategy() { + return parseStrategy; + } + + public void setParseStrategy(Integer parseStrategy) { + this.parseStrategy = parseStrategy; + } + + public String getFieldName() { + return fieldName; + } + + public void setFieldName(String fieldName) { + this.fieldName = fieldName; + } + + public String getPattern() { + return pattern; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public Integer getMatchStrategy() { + return matchStrategy; + } + + public void setMatchStrategy(Integer matchStrategy) { + this.matchStrategy = matchStrategy; + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/UpdateFlowRuleReqVo.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/UpdateFlowRuleReqVo.java new file mode 100644 index 00000000..8d1988fe --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/domain/vo/gateway/rule/UpdateFlowRuleReqVo.java @@ -0,0 +1,125 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule; + +/** + * Value Object for update gateway flow rule. + * + * @author cdfive + * @since 1.7.0 + */ +public class UpdateFlowRuleReqVo { + + private Long id; + + private String app; + + private Integer grade; + + private Double count; + + private Long interval; + + private Integer intervalUnit; + + private Integer controlBehavior; + + private Integer burst; + + private Integer maxQueueingTimeoutMs; + + private GatewayParamFlowItemVo paramItem; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getApp() { + return app; + } + + public void setApp(String app) { + this.app = app; + } + + public Integer getGrade() { + return grade; + } + + public void setGrade(Integer grade) { + this.grade = grade; + } + + public Double getCount() { + return count; + } + + public void setCount(Double count) { + this.count = count; + } + + public Long getInterval() { + return interval; + } + + public void setInterval(Long interval) { + this.interval = interval; + } + + public Integer getIntervalUnit() { + return intervalUnit; + } + + public void setIntervalUnit(Integer intervalUnit) { + this.intervalUnit = intervalUnit; + } + + public Integer getControlBehavior() { + return controlBehavior; + } + + public void setControlBehavior(Integer controlBehavior) { + this.controlBehavior = controlBehavior; + } + + public Integer getBurst() { + return burst; + } + + public void setBurst(Integer burst) { + this.burst = burst; + } + + public Integer getMaxQueueingTimeoutMs() { + return maxQueueingTimeoutMs; + } + + public void setMaxQueueingTimeoutMs(Integer maxQueueingTimeoutMs) { + this.maxQueueingTimeoutMs = maxQueueingTimeoutMs; + } + + public GatewayParamFlowItemVo getParamItem() { + return paramItem; + } + + public void setParamItem(GatewayParamFlowItemVo paramItem) { + this.paramItem = paramItem; + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/gateway/InMemApiDefinitionStore.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/gateway/InMemApiDefinitionStore.java new file mode 100644 index 00000000..844acbd5 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/gateway/InMemApiDefinitionStore.java @@ -0,0 +1,39 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.dashboard.repository.gateway; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.ApiDefinitionEntity; +import com.alibaba.csp.sentinel.dashboard.repository.rule.InMemoryRuleRepositoryAdapter; +import org.springframework.stereotype.Component; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * Store {@link ApiDefinitionEntity} in memory. + * + * @author cdfive + * @since 1.7.0 + */ +@Component +public class InMemApiDefinitionStore extends InMemoryRuleRepositoryAdapter { + + private static AtomicLong ids = new AtomicLong(0); + + @Override + protected long nextId() { + return ids.incrementAndGet(); + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/gateway/InMemGatewayFlowRuleStore.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/gateway/InMemGatewayFlowRuleStore.java new file mode 100644 index 00000000..f1b63c42 --- /dev/null +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/gateway/InMemGatewayFlowRuleStore.java @@ -0,0 +1,39 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.dashboard.repository.gateway; + +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.repository.rule.InMemoryRuleRepositoryAdapter; +import org.springframework.stereotype.Component; + +import java.util.concurrent.atomic.AtomicLong; + +/** + * Store {@link GatewayFlowRuleEntity} in memory. + * + * @author cdfive + * @since 1.7.0 + */ +@Component +public class InMemGatewayFlowRuleStore extends InMemoryRuleRepositoryAdapter { + + private static AtomicLong ids = new AtomicLong(0); + + @Override + protected long nextId() { + return ids.incrementAndGet(); + } +} diff --git a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemoryRuleRepositoryAdapter.java b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemoryRuleRepositoryAdapter.java index c6fdeed3..08d623cc 100755 --- a/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemoryRuleRepositoryAdapter.java +++ b/sentinel-dashboard/src/main/java/com/alibaba/csp/sentinel/dashboard/repository/rule/InMemoryRuleRepositoryAdapter.java @@ -110,6 +110,12 @@ public abstract class InMemoryRuleRepositoryAdapter implem return new ArrayList<>(entities.values()); } + public void clearAll() { + allRules.clear(); + machineRules.clear(); + appRules.clear(); + } + protected T preProcess(T entity) { return entity; } diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/app.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/app.js index fcc61610..bc3747af 100755 --- a/sentinel-dashboard/src/main/webapp/resources/app/scripts/app.js +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/app.js @@ -26,9 +26,9 @@ angular .factory('AuthInterceptor', ['$window', '$state', function ($window, $state) { var authInterceptor = { 'responseError' : function(response) { - if (response.status == 401) { + if (response.status === 401) { // If not auth, clear session in localStorage and jump to the login page - $window.localStorage.removeItem("session_sentinel_admin"); + $window.localStorage.removeItem('session_sentinel_admin'); $state.go('login'); } @@ -123,21 +123,21 @@ angular } }) - .state('dashboard.flow', { - templateUrl: 'app/views/flow_v2.html', - url: '/v2/flow/:app', - controller: 'FlowControllerV2', - resolve: { - loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { - return $ocLazyLoad.load({ - name: 'sentinelDashboardApp', - files: [ - 'app/scripts/controllers/flow_v2.js', - ] - }); - }] - } - }) + .state('dashboard.flow', { + templateUrl: 'app/views/flow_v2.html', + url: '/v2/flow/:app', + controller: 'FlowControllerV2', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/flow_v2.js', + ] + }); + }] + } + }) .state('dashboard.paramFlow', { templateUrl: 'app/views/param_flow.html', @@ -155,69 +155,69 @@ angular } }) - .state('dashboard.clusterAppAssignManage', { - templateUrl: 'app/views/cluster_app_assign_manage.html', - url: '/cluster/assign_manage/:app', - controller: 'SentinelClusterAppAssignManageController', - resolve: { - loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { - return $ocLazyLoad.load({ - name: 'sentinelDashboardApp', - files: [ - 'app/scripts/controllers/cluster_app_assign_manage.js', - ] - }); - }] - } - }) + .state('dashboard.clusterAppAssignManage', { + templateUrl: 'app/views/cluster_app_assign_manage.html', + url: '/cluster/assign_manage/:app', + controller: 'SentinelClusterAppAssignManageController', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/cluster_app_assign_manage.js', + ] + }); + }] + } + }) - .state('dashboard.clusterAppServerList', { - templateUrl: 'app/views/cluster_app_server_list.html', - url: '/cluster/server/:app', - controller: 'SentinelClusterAppServerListController', - resolve: { - loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { - return $ocLazyLoad.load({ - name: 'sentinelDashboardApp', - files: [ - 'app/scripts/controllers/cluster_app_server_list.js', - ] - }); - }] - } - }) + .state('dashboard.clusterAppServerList', { + templateUrl: 'app/views/cluster_app_server_list.html', + url: '/cluster/server/:app', + controller: 'SentinelClusterAppServerListController', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/cluster_app_server_list.js', + ] + }); + }] + } + }) - .state('dashboard.clusterAppClientList', { - templateUrl: 'app/views/cluster_app_client_list.html', - url: '/cluster/client/:app', - controller: 'SentinelClusterAppTokenClientListController', - resolve: { - loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { - return $ocLazyLoad.load({ - name: 'sentinelDashboardApp', - files: [ - 'app/scripts/controllers/cluster_app_token_client_list.js', - ] - }); - }] - } - }) + .state('dashboard.clusterAppClientList', { + templateUrl: 'app/views/cluster_app_client_list.html', + url: '/cluster/client/:app', + controller: 'SentinelClusterAppTokenClientListController', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/cluster_app_token_client_list.js', + ] + }); + }] + } + }) - .state('dashboard.clusterSingle', { - templateUrl: 'app/views/cluster_single_config.html', - url: '/cluster/single/:app', - controller: 'SentinelClusterSingleController', - resolve: { - loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { - return $ocLazyLoad.load({ - name: 'sentinelDashboardApp', - files: [ - 'app/scripts/controllers/cluster_single.js', - ] - }); - }] - } - }) + .state('dashboard.clusterSingle', { + templateUrl: 'app/views/cluster_single_config.html', + url: '/cluster/single/:app', + controller: 'SentinelClusterSingleController', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/cluster_single.js', + ] + }); + }] + } + }) .state('dashboard.authority', { templateUrl: 'app/views/authority.html', @@ -298,6 +298,23 @@ angular }] } }) + + .state('dashboard.gatewayIdentity', { + templateUrl: 'app/views/gateway/identity.html', + url: '/gateway/identity/:app', + controller: 'GatewayIdentityCtl', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/gateway/identity.js', + ] + }); + }] + } + }) + .state('dashboard.metric', { templateUrl: 'app/views/metric.html', url: '/metric/:app', @@ -312,5 +329,37 @@ angular }); }] } + }) + + .state('dashboard.gatewayApi', { + templateUrl: 'app/views/gateway/api.html', + url: '/gateway/api/:app', + controller: 'GatewayApiCtl', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/gateway/api.js', + ] + }); + }] + } + }) + + .state('dashboard.gatewayFlow', { + templateUrl: 'app/views/gateway/flow.html', + url: '/gateway/flow/:app', + controller: 'GatewayFlowCtl', + resolve: { + loadMyFiles: ['$ocLazyLoad', function ($ocLazyLoad) { + return $ocLazyLoad.load({ + name: 'sentinelDashboardApp', + files: [ + 'app/scripts/controllers/gateway/flow.js', + ] + }); + }] + } }); }]); \ No newline at end of file diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/gateway/api.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/gateway/api.js new file mode 100644 index 00000000..8e66b113 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/gateway/api.js @@ -0,0 +1,245 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('GatewayApiCtl', ['$scope', '$stateParams', 'GatewayApiService', 'ngDialog', 'MachineService', + function ($scope, $stateParams, GatewayApiService, ngDialog, MachineService) { + $scope.app = $stateParams.app; + + $scope.apisPageConfig = { + pageSize: 10, + currentPageIndex: 1, + totalPage: 1, + totalCount: 0, + }; + + $scope.macsInputConfig = { + searchField: ['text', 'value'], + persist: true, + create: false, + maxItems: 1, + render: { + item: function (data, escape) { + return '
' + escape(data.text) + '
'; + } + }, + onChange: function (value, oldValue) { + $scope.macInputModel = value; + } + }; + + getApis(); + function getApis() { + if (!$scope.macInputModel) { + return; + } + + var mac = $scope.macInputModel.split(':'); + GatewayApiService.queryApis($scope.app, mac[0], mac[1]).success( + function (data) { + if (data.code == 0 && data.data) { + // To merge rows for api who has more than one predicateItems, here we build data manually + $scope.apis = []; + + data.data.forEach(function(api) { + api["predicateItems"].forEach(function (item, index) { + var newItem = {}; + newItem["id"] = api["id"]; + newItem["app"] = api["app"]; + newItem["ip"] = api["ip"]; + newItem["port"] = api["port"]; + newItem["apiName"] = api["apiName"]; + newItem["pattern"] = item["pattern"]; + newItem["matchStrategy"] = item["matchStrategy"]; + // The itemSize indicates how many rows to merge, by using rowspan="{{api.itemSize}}" in tag + newItem["itemSize"] = api["predicateItems"].length; + // Mark the flag of first item to zero, indicates the start row to merge + newItem["firstFlag"] = index == 0 ? 0 : 1; + // Still hold the data of predicateItems, in order to bind data in edit dialog html + newItem["predicateItems"] = api["predicateItems"]; + $scope.apis.push(newItem); + }); + }); + + $scope.apisPageConfig.totalCount = data.data.length; + } else { + $scope.apis = []; + $scope.apisPageConfig.totalCount = 0; + } + }); + }; + $scope.getApis = getApis; + + var gatewayApiDialog; + $scope.editApi = function (api) { + $scope.currentApi = angular.copy(api); + $scope.gatewayApiDialog = { + title: '编辑自定义API', + type: 'edit', + confirmBtnText: '保存' + }; + gatewayApiDialog = ngDialog.open({ + template: '/app/views/dialog/gateway/api-dialog.html', + width: 840, + overlay: true, + scope: $scope + }); + }; + + $scope.addNewApi = function () { + var mac = $scope.macInputModel.split(':'); + $scope.currentApi = { + grade: 0, + app: $scope.app, + ip: mac[0], + port: mac[1], + predicateItems: [{matchStrategy: 0, pattern: ''}] + }; + $scope.gatewayApiDialog = { + title: '新增自定义API', + type: 'add', + confirmBtnText: '新增' + }; + gatewayApiDialog = ngDialog.open({ + template: '/app/views/dialog/gateway/api-dialog.html', + width: 840, + overlay: true, + scope: $scope + }); + }; + + $scope.saveApi = function () { + var apiNames = []; + if ($scope.gatewayApiDialog.type === 'add') { + apiNames = $scope.apis.map(function (item, index, array) { + return item["apiName"]; + }).filter(function (item, index, array) { + return array.indexOf(item) === index; + }); + } + + if (!GatewayApiService.checkApiValid($scope.currentApi, apiNames)) { + return; + } + + if ($scope.gatewayApiDialog.type === 'add') { + addNewApi($scope.currentApi); + } else if ($scope.gatewayApiDialog.type === 'edit') { + saveApi($scope.currentApi, true); + } + }; + + function addNewApi(api) { + GatewayApiService.newApi(api).success(function (data) { + if (data.code == 0) { + getApis(); + gatewayApiDialog.close(); + } else { + alert('新增自定义API失败!' + data.msg); + } + }); + }; + + function saveApi(api, edit) { + GatewayApiService.saveApi(api).success(function (data) { + if (data.code == 0) { + getApis(); + if (edit) { + gatewayApiDialog.close(); + } else { + confirmDialog.close(); + } + } else { + alert('修改自定义API失败!' + data.msg); + } + }); + }; + + var confirmDialog; + $scope.deleteApi = function (api) { + $scope.currentApi = api; + $scope.confirmDialog = { + title: '删除自定义API', + type: 'delete_api', + attentionTitle: '请确认是否删除如下自定义API', + attention: 'API名称: ' + api.apiName, + confirmBtnText: '删除', + }; + confirmDialog = ngDialog.open({ + template: '/app/views/dialog/confirm-dialog.html', + scope: $scope, + overlay: true + }); + }; + + $scope.confirm = function () { + if ($scope.confirmDialog.type == 'delete_api') { + deleteApi($scope.currentApi); + } else { + console.error('error'); + } + }; + + function deleteApi(api) { + GatewayApiService.deleteApi(api).success(function (data) { + if (data.code == 0) { + getApis(); + confirmDialog.close(); + } else { + alert('删除自定义API失败!' + data.msg); + } + }); + }; + + $scope.addNewMatchPattern = function() { + var total; + if ($scope.currentApi.predicateItems == null) { + $scope.currentApi.predicateItems = []; + total = 0; + } else { + total = $scope.currentApi.predicateItems.length; + } + $scope.currentApi.predicateItems.splice(total + 1, 0, {matchStrategy: 0, pattern: ''}); + }; + + $scope.removeMatchPattern = function($index) { + if ($scope.currentApi.predicateItems.length <= 1) { + // Should never happen since no remove button will display when only one predicateItem. + alert('至少有一个匹配规则'); + return; + } + $scope.currentApi.predicateItems.splice($index, 1); + }; + + queryAppMachines(); + function queryAppMachines() { + MachineService.getAppMachines($scope.app).success( + function (data) { + if (data.code == 0) { + // $scope.machines = data.data; + if (data.data) { + $scope.machines = []; + $scope.macsInputOptions = []; + data.data.forEach(function (item) { + if (item.healthy) { + $scope.macsInputOptions.push({ + text: item.ip + ':' + item.port, + value: item.ip + ':' + item.port + }); + } + }); + } + if ($scope.macsInputOptions.length > 0) { + $scope.macInputModel = $scope.macsInputOptions[0].value; + } + } else { + $scope.macsInputOptions = []; + } + } + ); + }; + $scope.$watch('macInputModel', function () { + if ($scope.macInputModel) { + getApis(); + } + }); + }] +); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/gateway/flow.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/gateway/flow.js new file mode 100644 index 00000000..c492cf9c --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/gateway/flow.js @@ -0,0 +1,251 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('GatewayFlowCtl', ['$scope', '$stateParams', 'GatewayFlowService', 'GatewayApiService', 'ngDialog', 'MachineService', + function ($scope, $stateParams, GatewayFlowService, GatewayApiService, ngDialog, MachineService) { + $scope.app = $stateParams.app; + + $scope.rulesPageConfig = { + pageSize: 10, + currentPageIndex: 1, + totalPage: 1, + totalCount: 0, + }; + + $scope.macsInputConfig = { + searchField: ['text', 'value'], + persist: true, + create: false, + maxItems: 1, + render: { + item: function (data, escape) { + return '
' + escape(data.text) + '
'; + } + }, + onChange: function (value, oldValue) { + $scope.macInputModel = value; + } + }; + + getMachineRules(); + function getMachineRules() { + if (!$scope.macInputModel) { + return; + } + + var mac = $scope.macInputModel.split(':'); + GatewayFlowService.queryRules($scope.app, mac[0], mac[1]).success( + function (data) { + if (data.code == 0 && data.data) { + $scope.rules = data.data; + $scope.rulesPageConfig.totalCount = $scope.rules.length; + } else { + $scope.rules = []; + $scope.rulesPageConfig.totalCount = 0; + } + }); + }; + $scope.getMachineRules = getMachineRules; + + getApiNames(); + function getApiNames() { + if (!$scope.macInputModel) { + return; + } + + var mac = $scope.macInputModel.split(':'); + GatewayApiService.queryApis($scope.app, mac[0], mac[1]).success( + function (data) { + if (data.code == 0 && data.data) { + $scope.apiNames = []; + + data.data.forEach(function (api) { + $scope.apiNames.push(api["apiName"]); + }); + } + }); + } + + $scope.intervalUnits = [{val: 0, desc: '秒'}, {val: 1, desc: '分'}, {val: 2, desc: '时'}, {val: 3, desc: '天'}]; + + var gatewayFlowRuleDialog; + $scope.editRule = function (rule) { + $scope.currentRule = angular.copy(rule); + $scope.gatewayFlowRuleDialog = { + title: '编辑网关流控规则', + type: 'edit', + confirmBtnText: '保存' + }; + gatewayFlowRuleDialog = ngDialog.open({ + template: '/app/views/dialog/gateway/flow-rule-dialog.html', + width: 780, + overlay: true, + scope: $scope + }); + }; + + $scope.addNewRule = function () { + var mac = $scope.macInputModel.split(':'); + $scope.currentRule = { + grade: 1, + app: $scope.app, + ip: mac[0], + port: mac[1], + resourceMode: 0, + interval: 1, + intervalUnit: 0, + controlBehavior: 0, + burst: 0, + maxQueueingTimeoutMs: 0 + }; + + $scope.gatewayFlowRuleDialog = { + title: '新增网关流控规则', + type: 'add', + confirmBtnText: '新增' + }; + + gatewayFlowRuleDialog = ngDialog.open({ + template: '/app/views/dialog/gateway/flow-rule-dialog.html', + width: 780, + overlay: true, + scope: $scope + }); + }; + + $scope.saveRule = function () { + if (!GatewayFlowService.checkRuleValid($scope.currentRule)) { + return; + } + if ($scope.gatewayFlowRuleDialog.type === 'add') { + addNewRule($scope.currentRule); + } else if ($scope.gatewayFlowRuleDialog.type === 'edit') { + saveRule($scope.currentRule, true); + } + }; + + $scope.useRouteID = function() { + $scope.currentRule.resource = ''; + }; + + $scope.useCustormAPI = function() { + $scope.currentRule.resource = ''; + }; + + $scope.useParamItem = function () { + $scope.currentRule.paramItem = { + parseStrategy: 0, + matchStrategy: 0 + }; + }; + + $scope.notUseParamItem = function () { + $scope.currentRule.paramItem = null; + }; + + $scope.useParamItemVal = function() { + $scope.currentRule.paramItem.pattern = ""; + $scope.currentRule.paramItem.matchStrategy = 0; + }; + + $scope.notUseParamItemVal = function() { + $scope.currentRule.paramItem.pattern = null; + $scope.currentRule.paramItem.matchStrategy = null; + }; + + function addNewRule(rule) { + GatewayFlowService.newRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + gatewayFlowRuleDialog.close(); + } else { + alert('新增网关流控规则失败!' + data.msg); + } + }); + }; + + function saveRule(rule, edit) { + GatewayFlowService.saveRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + if (edit) { + gatewayFlowRuleDialog.close(); + } else { + confirmDialog.close(); + } + } else { + alert('修改网关流控规则失败!' + data.msg); + } + }); + }; + + var confirmDialog; + $scope.deleteRule = function (rule) { + $scope.currentRule = rule; + $scope.confirmDialog = { + title: '删除网关流控规则', + type: 'delete_rule', + attentionTitle: '请确认是否删除如下规则', + attention: 'API名称: ' + rule.resource + ', ' + (rule.grade == 1 ? 'QPS阈值' : '线程数') + ': ' + rule.count, + confirmBtnText: '删除', + }; + confirmDialog = ngDialog.open({ + template: '/app/views/dialog/confirm-dialog.html', + scope: $scope, + overlay: true + }); + }; + + $scope.confirm = function () { + if ($scope.confirmDialog.type == 'delete_rule') { + deleteRule($scope.currentRule); + } else { + console.error('error'); + } + }; + + function deleteRule(rule) { + GatewayFlowService.deleteRule(rule).success(function (data) { + if (data.code == 0) { + getMachineRules(); + confirmDialog.close(); + } else { + alert('删除网关流控规则失败!' + data.msg); + } + }); + }; + + queryAppMachines(); + + function queryAppMachines() { + MachineService.getAppMachines($scope.app).success( + function (data) { + if (data.code == 0) { + if (data.data) { + $scope.machines = []; + $scope.macsInputOptions = []; + data.data.forEach(function (item) { + if (item.healthy) { + $scope.macsInputOptions.push({ + text: item.ip + ':' + item.port, + value: item.ip + ':' + item.port + }); + } + }); + } + if ($scope.macsInputOptions.length > 0) { + $scope.macInputModel = $scope.macsInputOptions[0].value; + } + } else { + $scope.macsInputOptions = []; + } + } + ); + }; + $scope.$watch('macInputModel', function () { + if ($scope.macInputModel) { + getMachineRules(); + getApiNames(); + } + }); + }] +); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/gateway/identity.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/gateway/identity.js new file mode 100644 index 00000000..52871b4a --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/gateway/identity.js @@ -0,0 +1,299 @@ +var app = angular.module('sentinelDashboardApp'); + +app.controller('GatewayIdentityCtl', ['$scope', '$stateParams', 'IdentityService', + 'ngDialog', 'GatewayFlowService', 'GatewayApiService', 'DegradeService', 'MachineService', + '$interval', '$location', '$timeout', + function ($scope, $stateParams, IdentityService, ngDialog, + GatewayFlowService, GatewayApiService, DegradeService, MachineService, $interval, $location, $timeout) { + + $scope.app = $stateParams.app; + + $scope.currentPage = 1; + $scope.pageSize = 16; + $scope.totalPage = 1; + $scope.totalCount = 0; + $scope.identities = []; + + $scope.searchKey = ''; + + $scope.macsInputConfig = { + searchField: ['text', 'value'], + persist: true, + create: false, + maxItems: 1, + render: { + item: function (data, escape) { + return '
' + escape(data.text) + '
'; + } + }, + onChange: function (value, oldValue) { + $scope.macInputModel = value; + } + }; + $scope.table = null; + + getApiNames(); + function getApiNames() { + if (!$scope.macInputModel) { + return; + } + + var mac = $scope.macInputModel.split(':'); + GatewayApiService.queryApis($scope.app, mac[0], mac[1]).success( + function (data) { + if (data.code == 0 && data.data) { + $scope.apiNames = []; + + data.data.forEach(function (api) { + $scope.apiNames.push(api["apiName"]); + }); + } + }); + } + + var gatewayFlowRuleDialog; + var gatewayFlowRuleDialogScope; + $scope.addNewGatewayFlowRule = function (resource) { + if (!$scope.macInputModel) { + return; + } + var mac = $scope.macInputModel.split(':'); + gatewayFlowRuleDialogScope = $scope.$new(true); + + gatewayFlowRuleDialogScope.apiNames = $scope.apiNames; + + gatewayFlowRuleDialogScope.intervalUnits = [{val: 0, desc: '秒'}, {val: 1, desc: '分'}, {val: 2, desc: '时'}, {val: 3, desc: '天'}]; + + gatewayFlowRuleDialogScope.currentRule = { + grade: 1, + app: $scope.app, + ip: mac[0], + port: mac[1], + resourceMode: gatewayFlowRuleDialogScope.apiNames.indexOf(resource) == -1 ? 0 : 1, + resource: resource, + interval: 1, + intervalUnit: 0, + controlBehavior: 0, + burst: 0, + maxQueueingTimeoutMs: 0 + }; + + gatewayFlowRuleDialogScope.gatewayFlowRuleDialog = { + title: '新增网关流控规则', + type: 'add', + confirmBtnText: '新增', + saveAndContinueBtnText: '新增并继续添加', + showAdvanceButton: true + }; + + gatewayFlowRuleDialogScope.useRouteID = function() { + gatewayFlowRuleDialogScope.currentRule.resource = ''; + }; + + gatewayFlowRuleDialogScope.useCustormAPI = function() { + gatewayFlowRuleDialogScope.currentRule.resource = ''; + }; + + gatewayFlowRuleDialogScope.useParamItem = function () { + gatewayFlowRuleDialogScope.currentRule.paramItem = { + parseStrategy: 0, + matchStrategy: 0 + }; + }; + + gatewayFlowRuleDialogScope.notUseParamItem = function () { + gatewayFlowRuleDialogScope.currentRule.paramItem = null; + }; + + gatewayFlowRuleDialogScope.useParamItemVal = function() { + gatewayFlowRuleDialogScope.currentRule.paramItem.pattern = ""; + }; + + gatewayFlowRuleDialogScope.notUseParamItemVal = function() { + gatewayFlowRuleDialogScope.currentRule.paramItem.pattern = null; + }; + + gatewayFlowRuleDialogScope.saveRule = saveGatewayFlowRule; + gatewayFlowRuleDialogScope.saveRuleAndContinue = saveGatewayFlowRuleAndContinue; + gatewayFlowRuleDialogScope.onOpenAdvanceClick = function () { + gatewayFlowRuleDialogScope.gatewayFlowRuleDialog.showAdvanceButton = false; + }; + gatewayFlowRuleDialogScope.onCloseAdvanceClick = function () { + gatewayFlowRuleDialogScope.gatewayFlowRuleDialog.showAdvanceButton = true; + }; + + gatewayFlowRuleDialog = ngDialog.open({ + template: '/app/views/dialog/gateway/flow-rule-dialog.html', + width: 780, + overlay: true, + scope: gatewayFlowRuleDialogScope + }); + }; + + function saveGatewayFlowRule() { + if (!GatewayFlowService.checkRuleValid(gatewayFlowRuleDialogScope.currentRule)) { + return; + } + GatewayFlowService.newRule(gatewayFlowRuleDialogScope.currentRule).success(function (data) { + if (data.code === 0) { + gatewayFlowRuleDialog.close(); + let url = '/dashboard/gateway/flow/' + $scope.app; + $location.path(url); + } else { + alert('失败!'); + } + }).error((data, header, config, status) => { + alert('未知错误'); + }); + } + + function saveGatewayFlowRuleAndContinue() { + if (!GatewayFlowService.checkRuleValid(gatewayFlowRuleDialogScope.currentRule)) { + return; + } + GatewayFlowService.newRule(gatewayFlowRuleDialogScope.currentRule).success(function (data) { + if (data.code == 0) { + gatewayFlowRuleDialog.close(); + } else { + alert('失败!'); + } + }); + } + + var degradeRuleDialog; + $scope.addNewDegradeRule = function (resource) { + if (!$scope.macInputModel) { + return; + } + var mac = $scope.macInputModel.split(':'); + degradeRuleDialogScope = $scope.$new(true); + degradeRuleDialogScope.currentRule = { + enable: false, + grade: 0, + strategy: 0, + resource: resource, + limitApp: 'default', + app: $scope.app, + ip: mac[0], + port: mac[1] + }; + + degradeRuleDialogScope.degradeRuleDialog = { + title: '新增降级规则', + type: 'add', + confirmBtnText: '新增', + saveAndContinueBtnText: '新增并继续添加' + }; + degradeRuleDialogScope.saveRule = saveDegradeRule; + degradeRuleDialogScope.saveRuleAndContinue = saveDegradeRuleAndContinue; + + degradeRuleDialog = ngDialog.open({ + template: '/app/views/dialog/degrade-rule-dialog.html', + width: 680, + overlay: true, + scope: degradeRuleDialogScope + }); + }; + + function saveDegradeRule() { + if (!DegradeService.checkRuleValid(degradeRuleDialogScope.currentRule)) { + return; + } + DegradeService.newRule(degradeRuleDialogScope.currentRule).success(function (data) { + if (data.code == 0) { + degradeRuleDialog.close(); + var url = '/dashboard/degrade/' + $scope.app; + $location.path(url); + } else { + alert('失败!'); + } + }); + } + + function saveDegradeRuleAndContinue() { + if (!DegradeService.checkRuleValid(degradeRuleDialogScope.currentRule)) { + return; + } + DegradeService.newRule(degradeRuleDialogScope.currentRule).success(function (data) { + if (data.code == 0) { + degradeRuleDialog.close(); + } else { + alert('失败!'); + } + }); + } + + var searchHandler; + $scope.searchChange = function (searchKey) { + $timeout.cancel(searchHandler); + searchHandler = $timeout(function () { + $scope.searchKey = searchKey; + reInitIdentityDatas(); + }, 600); + }; + + function queryAppMachines() { + MachineService.getAppMachines($scope.app).success( + function (data) { + if (data.code === 0) { + if (data.data) { + $scope.machines = []; + $scope.macsInputOptions = []; + data.data.forEach(function (item) { + if (item.healthy) { + $scope.macsInputOptions.push({ + text: item.ip + ':' + item.port, + value: item.ip + ':' + item.port + }); + } + }); + } + if ($scope.macsInputOptions.length > 0) { + $scope.macInputModel = $scope.macsInputOptions[0].value; + } + } else { + $scope.macsInputOptions = []; + } + } + ); + } + + // Fetch all machines by current app name. + queryAppMachines(); + + $scope.$watch('macInputModel', function () { + if ($scope.macInputModel) { + reInitIdentityDatas(); + } + }); + + $scope.$on('$destroy', function () { + $interval.cancel(intervalId); + }); + + var intervalId; + function reInitIdentityDatas() { + getApiNames(); + queryIdentities(); + }; + + function queryIdentities() { + var mac = $scope.macInputModel.split(':'); + if (mac == null || mac.length < 2) { + return; + } + + IdentityService.fetchClusterNodeOfMachine(mac[0], mac[1], $scope.searchKey).success( + function (data) { + if (data.code == 0 && data.data) { + $scope.identities = data.data; + $scope.totalCount = $scope.identities.length; + } else { + $scope.identities = []; + $scope.totalCount = 0; + } + } + ); + }; + $scope.queryIdentities = queryIdentities; + }]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/login.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/login.js index 70b6ffd3..1230129c 100644 --- a/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/login.js +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/controllers/login.js @@ -1,8 +1,8 @@ var app = angular.module('sentinelDashboardApp'); app.controller('LoginCtl', ['$scope', '$state', '$window', 'AuthService', - function ($scope, $state, $window, LoginService) { - // If auth, jump to the index page directly + function ($scope, $state, $window, AuthService) { + // If auth passed, jump to the index page directly if ($window.localStorage.getItem('session_sentinel_admin')) { $state.go('dashboard'); } @@ -20,7 +20,7 @@ app.controller('LoginCtl', ['$scope', '$state', '$window', 'AuthService', var param = {"username": $scope.username, "password": $scope.password}; - LoginService.login(param).success(function (data) { + AuthService.login(param).success(function (data) { if (data.code == 0) { $window.localStorage.setItem('session_sentinel_admin', { username: data.data diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.html b/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.html index 0f5b82c6..623e889a 100755 --- a/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.html +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.html @@ -28,24 +28,41 @@   实时监控 -
  • + +
  •   簇点链路
  • -
  • - -   流控规则 +
  • + +   请求链路
  • - + + + +
  • + +    API管理 +
  • +
  • + +   流控规则 +
  • + +
  • + +   流控规则 +
  • +
  •   降级规则
  • -
  • +
  •   热点规则
  • @@ -53,19 +70,19 @@   系统规则 -
  • +
  •   授权规则
  • -
  • +
  •   集群流控
  • +
  •   机器列表
  • - diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.js index bfa6dd75..64ed684b 100755 --- a/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.js +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/directives/sidebar/sidebar.js @@ -20,7 +20,8 @@ angular.module('sentinelDashboardApp') AppService.getApps().success( function (data) { if (data.code === 0) { - let initHashApp = $location.path().split('/')[3]; + var path = $location.path().split('/'); + let initHashApp = path[path.length - 1]; $scope.apps = data.data; $scope.apps = $scope.apps.map(function (item) { if (item.app === initHashApp) { diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/auth_service.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/auth_service.js index 0e3c04b4..337221c1 100644 --- a/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/auth_service.js +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/auth_service.js @@ -6,13 +6,13 @@ app.service('AuthService', ['$http', function ($http) { url: '/auth/login', params: param, method: 'POST' - }) - } + }); + }; this.logout = function () { return $http({ url: '/auth/logout', method: 'POST' - }) - } + }); + }; }]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/gateway/api_service.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/gateway/api_service.js new file mode 100644 index 00000000..373f71db --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/gateway/api_service.js @@ -0,0 +1,73 @@ +var app = angular.module('sentinelDashboardApp'); + +app.service('GatewayApiService', ['$http', function ($http) { + this.queryApis = function (app, ip, port) { + var param = { + app: app, + ip: ip, + port: port + }; + return $http({ + url: '/gateway/api/list.json', + params: param, + method: 'GET' + }); + }; + + this.newApi = function (api) { + return $http({ + url: '/gateway/api/new.json', + data: api, + method: 'POST' + }); + }; + + this.saveApi = function (api) { + return $http({ + url: '/gateway/api/save.json', + data: api, + method: 'POST' + }); + }; + + this.deleteApi = function (api) { + var param = { + id: api.id, + app: api.app + }; + return $http({ + url: '/gateway/api/delete.json', + params: param, + method: 'POST' + }); + }; + + this.checkApiValid = function (api, apiNames) { + if (api.apiName === undefined || api.apiName === '') { + alert('API名称不能为空'); + return false; + } + + if (api.predicateItems == null || api.predicateItems.length === 0) { + // Should never happen since no remove button will display when only one predicateItem. + alert('至少有一个匹配规则'); + return false; + } + + for (var i = 0; i < api.predicateItems.length; i++) { + var predicateItem = api.predicateItems[i]; + var pattern = predicateItem.pattern; + if (pattern === undefined || pattern === '') { + alert('匹配串不能为空,请检查'); + return false; + } + } + + if (apiNames.indexOf(api.apiName) !== -1) { + alert('API名称(' + api.apiName + ')已存在'); + return false; + } + + return true; + }; +}]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/gateway/flow_service.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/gateway/flow_service.js new file mode 100644 index 00000000..b026b32f --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/gateway/flow_service.js @@ -0,0 +1,76 @@ +var app = angular.module('sentinelDashboardApp'); + +app.service('GatewayFlowService', ['$http', function ($http) { + this.queryRules = function (app, ip, port) { + var param = { + app: app, + ip: ip, + port: port + }; + + return $http({ + url: '/gateway/flow/list.json', + params: param, + method: 'GET' + }); + }; + + this.newRule = function (rule) { + return $http({ + url: '/gateway/flow/new.json', + data: rule, + method: 'POST' + }); + }; + + this.saveRule = function (rule) { + return $http({ + url: '/gateway/flow/save.json', + data: rule, + method: 'POST' + }); + }; + + this.deleteRule = function (rule) { + var param = { + id: rule.id, + app: rule.app + }; + + return $http({ + url: '/gateway/flow/delete.json', + params: param, + method: 'POST' + }); + }; + + this.checkRuleValid = function (rule) { + if (rule.resource === undefined || rule.resource === '') { + alert('API名称不能为空'); + return false; + } + + if (rule.paramItem != null) { + if (rule.paramItem.parseStrategy == 2 || + rule.paramItem.parseStrategy == 3 || + rule.paramItem.parseStrategy == 4) { + if (rule.paramItem.fieldName === undefined || rule.paramItem.fieldName === '') { + alert('当参数属性为Header、URL参数、Cookie时,参数名称不能为空'); + return false; + } + + if (rule.paramItem.pattern === '') { + alert('匹配串不能为空'); + return false; + } + } + } + + if (rule.count === undefined || rule.count < 0) { + alert((rule.grade === 1 ? 'QPS阈值' : '线程数') + '必须大于等于 0'); + return false; + } + + return true; + }; +}]); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/machineservice.js b/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/machineservice.js index 64531d65..2d3b5e8b 100755 --- a/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/machineservice.js +++ b/sentinel-dashboard/src/main/webapp/resources/app/scripts/services/machineservice.js @@ -1,23 +1,25 @@ var app = angular.module('sentinelDashboardApp'); -app.service('MachineService', ['$http', '$httpParamSerializerJQLike', function ($http, $httpParamSerializerJQLike) { - this.getAppMachines = function (app) { - return $http({ - url: 'app/' + app + '/machines.json', - method: 'GET' - }); - }; - this.removeAppMachine = function (app, ip, port) { - return $http({ - url: 'app/' + app + '/machine/remove.json', - method: 'POST', - headers: { - "Content-type": 'application/x-www-form-urlencoded; charset=UTF-8' - }, - data: $httpParamSerializerJQLike({ - ip: ip, - port: port - }) - }); - }; -}]); +app.service('MachineService', ['$http', '$httpParamSerializerJQLike', + function ($http, $httpParamSerializerJQLike) { + this.getAppMachines = function (app) { + return $http({ + url: 'app/' + app + '/machines.json', + method: 'GET' + }); + }; + this.removeAppMachine = function (app, ip, port) { + return $http({ + url: 'app/' + app + '/machine/remove.json', + method: 'POST', + headers: { + 'Content-type': 'application/x-www-form-urlencoded; charset=UTF-8' + }, + data: $httpParamSerializerJQLike({ + ip: ip, + port: port + }) + }); + }; + }] +); diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/gateway/api-dialog.html b/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/gateway/api-dialog.html new file mode 100644 index 00000000..d58f48f9 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/gateway/api-dialog.html @@ -0,0 +1,52 @@ +
    + {{gatewayApiDialog.title}} +
    +
    +
    +
    +
    + +
    + + +
    +
    + +
    + +
    +
    +  精确   +  前缀   +  正则   +
    +
    + +
    + +
    +
    + +
    +
    + +
    + +
    +
    +
    +
    +
    + + + +
    +
    +
    +
    diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/gateway/flow-rule-dialog.html b/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/gateway/flow-rule-dialog.html new file mode 100644 index 00000000..bbaeab48 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/dialog/gateway/flow-rule-dialog.html @@ -0,0 +1,172 @@ +
    + {{gatewayFlowRuleDialog.title}} +
    +
    +
    +
    +
    + +
    +
    +  Route ID   +  Route ID   +  API分组   +  API分组   +
    +
    +
    + +
    + +
    + + + + + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    +
    +  Client IP   +  Remote Host   +  Header   +  URL参数   +  Cookie   +
    +
    +
    + +
    + +
    + +
    +
    + +
    + +
    + +
    +
    + +
    + +
    +
    +  精确   +  子串   +  正则   +
    +
    + +
    + +
    +
    + +
    + +
    +
    +  QPS   +  线程数   +
    +
    +
    + +
    +
    + +
    + +
    +
    +
    + +
    +
    + +
    + +
    +
    + +
    +
    +
    + +
    + +
    +
    +  快速失败   +  匀速排队   +
    +
    +
    + +
    +
    + +
    + +
    +
    +
    + +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    + + + +
    +
    +
    +
    diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/gateway/api.html b/sentinel-dashboard/src/main/webapp/resources/app/views/gateway/api.html new file mode 100644 index 00000000..0316e689 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/gateway/api.html @@ -0,0 +1,87 @@ +
    +
    + {{app}} +
    +
    + +
    +
    + +
    + +
    +
    +
    +
    +
    + API管理 + + +
    + +
    +
    + + +
    + + + + + + + + + + + + + + + + + +
    + API名称 + + 匹配模式 + + 匹配串 + + 操作 +
    {{api.apiName}} + 精确 + 前缀 + 正则 + {{api.pattern}} + + +
    +
    + + + +
    + +
    + +
    + +
    + diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/gateway/flow.html b/sentinel-dashboard/src/main/webapp/resources/app/views/gateway/flow.html new file mode 100644 index 00000000..9bd4c232 --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/gateway/flow.html @@ -0,0 +1,94 @@ +
    +
    + {{app}} +
    +
    + +
    +
    + +
    + +
    +
    +
    +
    +
    + 网关流控规则 + + +
    + +
    +
    + + +
    + + + + + + + + + + + + + + + + + + + + +
    + API名称 + + API类型 + + 阈值类型 + + 单机阈值 + + 操作 +
    {{rule.resource}} + Route ID + API分组 + + QPS + 线程数 + {{rule.count}} + + +
    +
    + + + +
    + +
    + +
    + +
    + diff --git a/sentinel-dashboard/src/main/webapp/resources/app/views/gateway/identity.html b/sentinel-dashboard/src/main/webapp/resources/app/views/gateway/identity.html new file mode 100644 index 00000000..a36c39ec --- /dev/null +++ b/sentinel-dashboard/src/main/webapp/resources/app/views/gateway/identity.html @@ -0,0 +1,98 @@ +
    +
    + {{app}} +
    +
    + +
    + +
    +
    +
    +
    +
    + 请求链路 + + +
    + +
    +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + 资源名 + + 资源类型 + 通过QPS拒绝QPS线程数平均RT分钟通过分钟拒绝操作
    + {{resource.resource}} + + Route ID + 自定义API + {{resource.passQps}}{{resource.blockQps}}{{resource.threadNum}}{{resource.averageRt}}{{resource.oneMinutePass}} + {{resource.oneMinuteBlock}} {{resource.oneMinuteBlock}} +
    + + +
    +
    +
    + + + +
    + +
    + +
    + +
    + \ No newline at end of file diff --git a/sentinel-dashboard/src/main/webapp/resources/dist/js/app.js b/sentinel-dashboard/src/main/webapp/resources/dist/js/app.js index 98fbd02f..7b992400 100755 --- a/sentinel-dashboard/src/main/webapp/resources/dist/js/app.js +++ b/sentinel-dashboard/src/main/webapp/resources/dist/js/app.js @@ -1 +1 @@ -"use strict";var app;angular.module("sentinelDashboardApp",["oc.lazyLoad","ui.router","ui.bootstrap","angular-loading-bar","ngDialog","ui.bootstrap.datetimepicker","ui-notification","rzTable","angular-clipboard","selectize","angularUtils.directives.dirPagination"]).factory("AuthInterceptor",["$window","$state",function(r,t){return{responseError:function(e){return 401==e.status&&(r.localStorage.removeItem("session_sentinel_admin"),t.go("login")),e},response:function(e){return e},request:function(e){return e},requestError:function(e){return e}}}]).config(["$stateProvider","$urlRouterProvider","$ocLazyLoadProvider","$httpProvider",function(e,r,t,a){a.interceptors.push("AuthInterceptor"),t.config({debug:!1,events:!0}),r.otherwise("/dashboard/home"),e.state("login",{url:"/login",templateUrl:"app/views/login.html",controller:"LoginCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/login.js"]})}]}}).state("dashboard",{url:"/dashboard",templateUrl:"app/views/dashboard/main.html",resolve:{loadMyDirectives:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/directives/header/header.js","app/scripts/directives/sidebar/sidebar.js","app/scripts/directives/sidebar/sidebar-search/sidebar-search.js"]})}]}}).state("dashboard.home",{url:"/home",templateUrl:"app/views/dashboard/home.html",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/main.js"]})}]}}).state("dashboard.flowV1",{templateUrl:"app/views/flow_v1.html",url:"/flow/:app",controller:"FlowControllerV1",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/flow_v1.js"]})}]}}).state("dashboard.flow",{templateUrl:"app/views/flow_v2.html",url:"/v2/flow/:app",controller:"FlowControllerV2",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/flow_v2.js"]})}]}}).state("dashboard.paramFlow",{templateUrl:"app/views/param_flow.html",url:"/paramFlow/:app",controller:"ParamFlowController",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/param_flow.js"]})}]}}).state("dashboard.clusterAppAssignManage",{templateUrl:"app/views/cluster_app_assign_manage.html",url:"/cluster/assign_manage/:app",controller:"SentinelClusterAppAssignManageController",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/cluster_app_assign_manage.js"]})}]}}).state("dashboard.clusterAppServerList",{templateUrl:"app/views/cluster_app_server_list.html",url:"/cluster/server/:app",controller:"SentinelClusterAppServerListController",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/cluster_app_server_list.js"]})}]}}).state("dashboard.clusterAppClientList",{templateUrl:"app/views/cluster_app_client_list.html",url:"/cluster/client/:app",controller:"SentinelClusterAppTokenClientListController",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/cluster_app_token_client_list.js"]})}]}}).state("dashboard.clusterSingle",{templateUrl:"app/views/cluster_single_config.html",url:"/cluster/single/:app",controller:"SentinelClusterSingleController",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/cluster_single.js"]})}]}}).state("dashboard.authority",{templateUrl:"app/views/authority.html",url:"/authority/:app",controller:"AuthorityRuleController",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/authority.js"]})}]}}).state("dashboard.degrade",{templateUrl:"app/views/degrade.html",url:"/degrade/:app",controller:"DegradeCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/degrade.js"]})}]}}).state("dashboard.system",{templateUrl:"app/views/system.html",url:"/system/:app",controller:"SystemCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/system.js"]})}]}}).state("dashboard.machine",{templateUrl:"app/views/machine.html",url:"/app/:app",controller:"MachineCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/machine.js"]})}]}}).state("dashboard.identity",{templateUrl:"app/views/identity.html",url:"/identity/:app",controller:"IdentityCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/identity.js"]})}]}}).state("dashboard.metric",{templateUrl:"app/views/metric.html",url:"/metric/:app",controller:"MetricCtl",resolve:{loadMyFiles:["$ocLazyLoad",function(e){return e.load({name:"sentinelDashboardApp",files:["app/scripts/controllers/metric.js"]})}]}})}]),(app=angular.module("sentinelDashboardApp")).filter("range",[function(){return function(e,r){if(isNaN(r)||r<=0)return[];e=[];for(var t=1;t<=r;t++)e.push(t);return e}}]),(app=angular.module("sentinelDashboardApp")).service("AuthService",["$http",function(r){this.login=function(e){return r({url:"/auth/login",params:e,method:"POST"})},this.logout=function(){return r({url:"/auth/logout",method:"POST"})}}]),(app=angular.module("sentinelDashboardApp")).service("AppService",["$http",function(e){this.getApps=function(){return e({url:"app/briefinfos.json",method:"GET"})}}]),(app=angular.module("sentinelDashboardApp")).service("FlowServiceV1",["$http",function(a){function r(e){return void 0===e||""===e||isNaN(e)||e<=0}this.queryMachineRules=function(e,r,t){return a({url:"/v1/flow/rules",params:{app:e,ip:r,port:t},method:"GET"})},this.newRule=function(e){e.resource,e.limitApp,e.grade,e.count,e.strategy,e.refResource,e.controlBehavior,e.warmUpPeriodSec,e.maxQueueingTimeMs,e.app,e.ip,e.port;return a({url:"/v1/flow/rule",data:e,method:"POST"})},this.saveRule=function(e){var r={id:e.id,resource:e.resource,limitApp:e.limitApp,grade:e.grade,count:e.count,strategy:e.strategy,refResource:e.refResource,controlBehavior:e.controlBehavior,warmUpPeriodSec:e.warmUpPeriodSec,maxQueueingTimeMs:e.maxQueueingTimeMs};return a({url:"/v1/flow/save.json",params:r,method:"PUT"})},this.deleteRule=function(e){var r={id:e.id,app:e.app};return a({url:"/v1/flow/delete.json",params:r,method:"DELETE"})},this.checkRuleValid=function(e){return void 0===e.resource||""===e.resource?(alert("资源名称不能为空"),!1):void 0===e.count||e.count<0?(alert("限流阈值必须大于等于 0"),!1):void 0===e.strategy||e.strategy<0?(alert("无效的流控模式"),!1):1!=e.strategy&&2!=e.strategy||void 0!==e.refResource&&""!=e.refResource?void 0===e.controlBehavior||e.controlBehavior<0?(alert("无效的流控整形方式"),!1):1==e.controlBehavior&&r(e.warmUpPeriodSec)?(alert("预热时长必须大于 0"),!1):2==e.controlBehavior&&r(e.maxQueueingTimeMs)?(alert("排队超时时间必须大于 0"),!1):!e.clusterMode||void 0!==e.clusterConfig&&void 0!==e.clusterConfig.thresholdType||(alert("集群限流配置不正确"),!1):(alert("请填写关联资源或入口"),!1)}}]),(app=angular.module("sentinelDashboardApp")).service("FlowServiceV2",["$http",function(a){function r(e){return void 0===e||""===e||isNaN(e)||e<=0}this.queryMachineRules=function(e,r,t){return a({url:"/v2/flow/rules",params:{app:e,ip:r,port:t},method:"GET"})},this.newRule=function(e){return a({url:"/v2/flow/rule",data:e,method:"POST"})},this.saveRule=function(e){return a({url:"/v2/flow/rule/"+e.id,data:e,method:"PUT"})},this.deleteRule=function(e){return a({url:"/v2/flow/rule/"+e.id,method:"DELETE"})},this.checkRuleValid=function(e){return void 0===e.resource||""===e.resource?(alert("资源名称不能为空"),!1):void 0===e.count||e.count<0?(alert("限流阈值必须大于等于 0"),!1):void 0===e.strategy||e.strategy<0?(alert("无效的流控模式"),!1):1!=e.strategy&&2!=e.strategy||void 0!==e.refResource&&""!=e.refResource?void 0===e.controlBehavior||e.controlBehavior<0?(alert("无效的流控整形方式"),!1):1==e.controlBehavior&&r(e.warmUpPeriodSec)?(alert("预热时长必须大于 0"),!1):2==e.controlBehavior&&r(e.maxQueueingTimeMs)?(alert("排队超时时间必须大于 0"),!1):!e.clusterMode||void 0!==e.clusterConfig&&void 0!==e.clusterConfig.thresholdType||(alert("集群限流配置不正确"),!1):(alert("请填写关联资源或入口"),!1)}}]),(app=angular.module("sentinelDashboardApp")).service("DegradeService",["$http",function(a){this.queryMachineRules=function(e,r,t){return a({url:"degrade/rules.json",params:{app:e,ip:r,port:t},method:"GET"})},this.newRule=function(e){var r={id:e.id,resource:e.resource,limitApp:e.limitApp,count:e.count,timeWindow:e.timeWindow,grade:e.grade,app:e.app,ip:e.ip,port:e.port};return a({url:"/degrade/new.json",params:r,method:"GET"})},this.saveRule=function(e){var r={id:e.id,resource:e.resource,limitApp:e.limitApp,grade:e.grade,count:e.count,timeWindow:e.timeWindow};return a({url:"/degrade/save.json",params:r,method:"GET"})},this.deleteRule=function(e){var r={id:e.id,app:e.app};return a({url:"/degrade/delete.json",params:r,method:"GET"})},this.checkRuleValid=function(e){return void 0===e.resource||""===e.resource?(alert("资源名称不能为空"),!1):void 0===e.grade||e.grade<0?(alert("未知的降级策略"),!1):void 0===e.count||""===e.count||e.count<0?(alert("降级阈值不能为空或小于 0"),!1):void 0===e.timeWindow||""===e.timeWindow||e.timeWindow<=0?(alert("降级时间窗口必须大于 0"),!1):!(1==e.grade&&1 entities = new ArrayList<>(); + + // Mock two entities + ApiDefinitionEntity entity = new ApiDefinitionEntity(); + entity.setId(1L); + entity.setApp(TEST_APP); + entity.setIp(TEST_IP); + entity.setPort(TEST_PORT); + entity.setApiName("foo"); + Date date = new Date(); + entity.setGmtCreate(date); + entity.setGmtModified(date); + + Set itemEntities = new LinkedHashSet<>(); + entity.setPredicateItems(itemEntities); + ApiPredicateItemEntity itemEntity = new ApiPredicateItemEntity(); + itemEntity.setPattern("/aaa"); + itemEntity.setMatchStrategy(URL_MATCH_STRATEGY_EXACT); + + itemEntities.add(itemEntity); + entities.add(entity); + + ApiDefinitionEntity entity2 = new ApiDefinitionEntity(); + entity2.setId(2L); + entity2.setApp(TEST_APP); + entity2.setIp(TEST_IP); + entity2.setPort(TEST_PORT); + entity2.setApiName("biz"); + entity.setGmtCreate(date); + entity.setGmtModified(date); + + Set itemEntities2 = new LinkedHashSet<>(); + entity2.setPredicateItems(itemEntities2); + ApiPredicateItemEntity itemEntity2 = new ApiPredicateItemEntity(); + itemEntity2.setPattern("/bbb"); + itemEntity2.setMatchStrategy(URL_MATCH_STRATEGY_PREFIX); + + itemEntities2.add(itemEntity2); + entities.add(entity2); + + CompletableFuture> completableFuture = mock(CompletableFuture.class); + given(completableFuture.get()).willReturn(entities); + given(sentinelApiClient.fetchApis(TEST_APP, TEST_IP, TEST_PORT)).willReturn(completableFuture); + + MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get(path); + requestBuilder.param("app", TEST_APP); + requestBuilder.param("ip", TEST_IP); + requestBuilder.param("port", String.valueOf(TEST_PORT)); + + // Do controller logic + MvcResult mvcResult = mockMvc.perform(requestBuilder) + .andExpect(MockMvcResultMatchers.status().isOk()).andDo(MockMvcResultHandlers.print()).andReturn(); + + // Verify the fetchApis method has been called + verify(sentinelApiClient).fetchApis(TEST_APP, TEST_IP, TEST_PORT); + + // Verify if two same entities are got + Result> result = JSONObject.parseObject(mvcResult.getResponse().getContentAsString(), new TypeReference>>(){}); + assertTrue(result.isSuccess()); + + List data = result.getData(); + assertEquals(2, data.size()); + assertEquals(entities, data); + + // Verify the entities are add into memory repository + List entitiesInMem = repository.findAllByApp(TEST_APP); + assertEquals(2, entitiesInMem.size()); + assertEquals(entities, entitiesInMem); + } + + @Test + public void testAddApi() throws Exception { + String path = "/gateway/api/new.json"; + + AddApiReqVo reqVo = new AddApiReqVo(); + reqVo.setApp(TEST_APP); + reqVo.setIp(TEST_IP); + reqVo.setPort(TEST_PORT); + + reqVo.setApiName("customized_api"); + + List itemVos = new ArrayList<>(); + ApiPredicateItemVo itemVo = new ApiPredicateItemVo(); + itemVo.setMatchStrategy(URL_MATCH_STRATEGY_EXACT); + itemVo.setPattern("/product"); + itemVos.add(itemVo); + reqVo.setPredicateItems(itemVos); + + given(sentinelApiClient.modifyApis(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any())).willReturn(true); + + MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.post(path); + requestBuilder.content(JSON.toJSONString(reqVo)).contentType(MediaType.APPLICATION_JSON); + + // Do controller logic + MvcResult mvcResult = mockMvc.perform(requestBuilder) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcResultHandlers.print()).andReturn(); + + // Verify the modifyApis method has been called + verify(sentinelApiClient).modifyApis(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any()); + + Result result = JSONObject.parseObject(mvcResult.getResponse().getContentAsString(), new TypeReference>() {}); + assertTrue(result.isSuccess()); + + // Verify the result + ApiDefinitionEntity entity = result.getData(); + assertNotNull(entity); + assertEquals(TEST_APP, entity.getApp()); + assertEquals(TEST_IP, entity.getIp()); + assertEquals(TEST_PORT, entity.getPort()); + assertEquals("customized_api", entity.getApiName()); + assertNotNull(entity.getId()); + assertNotNull(entity.getGmtCreate()); + assertNotNull(entity.getGmtModified()); + + Set predicateItemEntities = entity.getPredicateItems(); + assertEquals(1, predicateItemEntities.size()); + ApiPredicateItemEntity predicateItemEntity = predicateItemEntities.iterator().next(); + assertEquals(URL_MATCH_STRATEGY_EXACT, predicateItemEntity.getMatchStrategy().intValue()); + assertEquals("/product", predicateItemEntity.getPattern()); + + // Verify the entity which is add in memory repository + List entitiesInMem = repository.findAllByApp(TEST_APP); + assertEquals(1, entitiesInMem.size()); + assertEquals(entity, entitiesInMem.get(0)); + } + + @Test + public void testUpdateApi() throws Exception { + String path = "/gateway/api/save.json"; + + // Add one entity to memory repository for update + ApiDefinitionEntity addEntity = new ApiDefinitionEntity(); + addEntity.setApp(TEST_APP); + addEntity.setIp(TEST_IP); + addEntity.setPort(TEST_PORT); + addEntity.setApiName("bbb"); + Date date = new Date(); + // To make the gmtModified different when do update + date = DateUtils.addSeconds(date, -1); + addEntity.setGmtCreate(date); + addEntity.setGmtModified(date); + Set addRedicateItemEntities = new HashSet<>(); + addEntity.setPredicateItems(addRedicateItemEntities); + ApiPredicateItemEntity addPredicateItemEntity = new ApiPredicateItemEntity(); + addPredicateItemEntity.setMatchStrategy(URL_MATCH_STRATEGY_EXACT); + addPredicateItemEntity.setPattern("/order"); + addEntity = repository.save(addEntity); + + UpdateApiReqVo reqVo = new UpdateApiReqVo(); + reqVo.setId(addEntity.getId()); + reqVo.setApp(TEST_APP); + List itemVos = new ArrayList<>(); + ApiPredicateItemVo itemVo = new ApiPredicateItemVo(); + itemVo.setMatchStrategy(URL_MATCH_STRATEGY_PREFIX); + itemVo.setPattern("/my_order"); + itemVos.add(itemVo); + reqVo.setPredicateItems(itemVos); + + given(sentinelApiClient.modifyApis(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any())).willReturn(true); + + MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.post(path); + requestBuilder.content(JSON.toJSONString(reqVo)).contentType(MediaType.APPLICATION_JSON); + + // Do controller logic + MvcResult mvcResult = mockMvc.perform(requestBuilder) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcResultHandlers.print()).andReturn(); + + // Verify the modifyApis method has been called + verify(sentinelApiClient).modifyApis(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any()); + + Result result = JSONObject.parseObject(mvcResult.getResponse().getContentAsString(), new TypeReference>() {}); + assertTrue(result.isSuccess()); + + ApiDefinitionEntity entity = result.getData(); + assertNotNull(entity); + assertEquals("bbb", entity.getApiName()); + assertEquals(date, entity.getGmtCreate()); + // To make sure gmtModified has been set and it's different from gmtCreate + assertNotNull(entity.getGmtModified()); + assertNotEquals(entity.getGmtCreate(), entity.getGmtModified()); + + Set predicateItemEntities = entity.getPredicateItems(); + assertEquals(1, predicateItemEntities.size()); + ApiPredicateItemEntity predicateItemEntity = predicateItemEntities.iterator().next(); + assertEquals(URL_MATCH_STRATEGY_PREFIX, predicateItemEntity.getMatchStrategy().intValue()); + assertEquals("/my_order", predicateItemEntity.getPattern()); + + // Verify the entity which is update in memory repository + List entitiesInMem = repository.findAllByApp(TEST_APP); + assertEquals(1, entitiesInMem.size()); + assertEquals(entity, entitiesInMem.get(0)); + } + + @Test + public void testDeleteApi() throws Exception { + String path = "/gateway/api/delete.json"; + + // Add one entity into memory repository for delete + ApiDefinitionEntity addEntity = new ApiDefinitionEntity(); + addEntity.setApp(TEST_APP); + addEntity.setIp(TEST_IP); + addEntity.setPort(TEST_PORT); + addEntity.setApiName("ccc"); + Date date = new Date(); + addEntity.setGmtCreate(date); + addEntity.setGmtModified(date); + Set addRedicateItemEntities = new HashSet<>(); + addEntity.setPredicateItems(addRedicateItemEntities); + ApiPredicateItemEntity addPredicateItemEntity = new ApiPredicateItemEntity(); + addPredicateItemEntity.setMatchStrategy(URL_MATCH_STRATEGY_EXACT); + addPredicateItemEntity.setPattern("/user/add"); + addEntity = repository.save(addEntity); + + given(sentinelApiClient.modifyApis(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any())).willReturn(true); + + MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.post(path); + requestBuilder.param("id", String.valueOf(addEntity.getId())); + + // Do controller logic + MvcResult mvcResult = mockMvc.perform(requestBuilder) + .andExpect(MockMvcResultMatchers.status().isOk()).andDo(MockMvcResultHandlers.print()).andReturn(); + + // Verify the modifyApis method has been called + verify(sentinelApiClient).modifyApis(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any()); + + // Verify the result + Result result = JSONObject.parseObject(mvcResult.getResponse().getContentAsString(), new TypeReference>() {}); + assertTrue(result.isSuccess()); + + assertEquals(addEntity.getId(), result.getData()); + + // Now no entities in memory + List entitiesInMem = repository.findAllByApp(TEST_APP); + assertEquals(0, entitiesInMem.size()); + } +} diff --git a/sentinel-dashboard/src/test/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayFlowRuleControllerTest.java b/sentinel-dashboard/src/test/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayFlowRuleControllerTest.java new file mode 100644 index 00000000..74b9bc6b --- /dev/null +++ b/sentinel-dashboard/src/test/java/com/alibaba/csp/sentinel/dashboard/controller/gateway/GatewayFlowRuleControllerTest.java @@ -0,0 +1,355 @@ +/* + * Copyright 1999-2018 Alibaba Group Holding Ltd. + * + * 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 com.alibaba.csp.sentinel.dashboard.controller.gateway; + +import com.alibaba.csp.sentinel.dashboard.auth.FakeAuthServiceImpl; +import com.alibaba.csp.sentinel.dashboard.client.SentinelApiClient; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayFlowRuleEntity; +import com.alibaba.csp.sentinel.dashboard.datasource.entity.gateway.GatewayParamFlowItemEntity; +import com.alibaba.csp.sentinel.dashboard.discovery.AppManagement; +import com.alibaba.csp.sentinel.dashboard.discovery.SimpleMachineDiscovery; +import com.alibaba.csp.sentinel.dashboard.domain.Result; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule.AddFlowRuleReqVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule.GatewayParamFlowItemVo; +import com.alibaba.csp.sentinel.dashboard.domain.vo.gateway.rule.UpdateFlowRuleReqVo; +import com.alibaba.csp.sentinel.dashboard.repository.gateway.InMemGatewayFlowRuleStore; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.alibaba.fastjson.TypeReference; +import org.apache.commons.lang3.time.DateUtils; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.http.MediaType; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import static com.alibaba.csp.sentinel.slots.block.RuleConstant.*; +import static com.alibaba.csp.sentinel.adapter.gateway.common.SentinelGatewayConstants.*; +import static org.junit.Assert.*; +import static org.mockito.BDDMockito.*; + +/** + * Test cases for {@link GatewayFlowRuleController}. + * + * @author cdfive + */ +@RunWith(SpringRunner.class) +@WebMvcTest(GatewayFlowRuleController.class) +@Import({FakeAuthServiceImpl.class, InMemGatewayFlowRuleStore.class, AppManagement.class, SimpleMachineDiscovery.class}) +public class GatewayFlowRuleControllerTest { + + private static final String TEST_APP = "test_app"; + + private static final String TEST_IP = "localhost"; + + private static final Integer TEST_PORT = 8719; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private InMemGatewayFlowRuleStore repository; + + @MockBean + private SentinelApiClient sentinelApiClient; + + @Before + public void before() { + repository.clearAll(); + } + + @Test + public void testQueryFlowRules() throws Exception { + String path = "/gateway/flow/list.json"; + + List entities = new ArrayList<>(); + + // Mock two entities + GatewayFlowRuleEntity entity = new GatewayFlowRuleEntity(); + entity.setId(1L); + entity.setApp(TEST_APP); + entity.setIp(TEST_IP); + entity.setPort(TEST_PORT); + entity.setResource("httpbin_route"); + entity.setResourceMode(RESOURCE_MODE_ROUTE_ID); + entity.setGrade(FLOW_GRADE_QPS); + entity.setCount(5D); + entity.setInterval(30L); + entity.setIntervalUnit(GatewayFlowRuleEntity.INTERVAL_UNIT_SECOND); + entity.setControlBehavior(CONTROL_BEHAVIOR_DEFAULT); + entity.setBurst(0); + entity.setMaxQueueingTimeoutMs(0); + + GatewayParamFlowItemEntity itemEntity = new GatewayParamFlowItemEntity(); + entity.setParamItem(itemEntity); + itemEntity.setParseStrategy(PARAM_PARSE_STRATEGY_CLIENT_IP); + entities.add(entity); + + GatewayFlowRuleEntity entity2 = new GatewayFlowRuleEntity(); + entity2.setId(2L); + entity2.setApp(TEST_APP); + entity2.setIp(TEST_IP); + entity2.setPort(TEST_PORT); + entity2.setResource("some_customized_api"); + entity2.setResourceMode(RESOURCE_MODE_CUSTOM_API_NAME); + entity2.setCount(30D); + entity2.setInterval(2L); + entity2.setIntervalUnit(GatewayFlowRuleEntity.INTERVAL_UNIT_MINUTE); + entity2.setControlBehavior(CONTROL_BEHAVIOR_DEFAULT); + entity2.setBurst(0); + entity2.setMaxQueueingTimeoutMs(0); + + GatewayParamFlowItemEntity itemEntity2 = new GatewayParamFlowItemEntity(); + entity2.setParamItem(itemEntity2); + itemEntity2.setParseStrategy(PARAM_PARSE_STRATEGY_CLIENT_IP); + entities.add(entity2); + + CompletableFuture> completableFuture = mock(CompletableFuture.class); + given(completableFuture.get()).willReturn(entities); + given(sentinelApiClient.fetchGatewayFlowRules(TEST_APP, TEST_IP, TEST_PORT)).willReturn(completableFuture); + + MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.get(path); + requestBuilder.param("app", TEST_APP); + requestBuilder.param("ip", TEST_IP); + requestBuilder.param("port", String.valueOf(TEST_PORT)); + + // Do controller logic + MvcResult mvcResult = mockMvc.perform(requestBuilder) + .andExpect(MockMvcResultMatchers.status().isOk()).andDo(MockMvcResultHandlers.print()).andReturn(); + + // Verify the fetchGatewayFlowRules method has been called + verify(sentinelApiClient).fetchGatewayFlowRules(TEST_APP, TEST_IP, TEST_PORT); + + // Verify if two same entities are got + Result> result = JSONObject.parseObject(mvcResult.getResponse().getContentAsString(), new TypeReference>>(){}); + assertTrue(result.isSuccess()); + + List data = result.getData(); + assertEquals(2, data.size()); + assertEquals(entities, data); + + // Verify the entities are add into memory repository + List entitiesInMem = repository.findAllByApp(TEST_APP); + assertEquals(2, entitiesInMem.size()); + assertEquals(entities, entitiesInMem); + } + + @Test + public void testAddFlowRule() throws Exception { + String path = "/gateway/flow/new.json"; + + AddFlowRuleReqVo reqVo = new AddFlowRuleReqVo(); + reqVo.setApp(TEST_APP); + reqVo.setIp(TEST_IP); + reqVo.setPort(TEST_PORT); + + reqVo.setResourceMode(RESOURCE_MODE_ROUTE_ID); + reqVo.setResource("httpbin_route"); + + reqVo.setGrade(FLOW_GRADE_QPS); + reqVo.setCount(5D); + reqVo.setInterval(30L); + reqVo.setIntervalUnit(GatewayFlowRuleEntity.INTERVAL_UNIT_SECOND); + reqVo.setControlBehavior(CONTROL_BEHAVIOR_DEFAULT); + reqVo.setBurst(0); + reqVo.setMaxQueueingTimeoutMs(0); + + given(sentinelApiClient.modifyGatewayFlowRules(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any())).willReturn(true); + + MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.post(path); + requestBuilder.content(JSON.toJSONString(reqVo)).contentType(MediaType.APPLICATION_JSON); + + // Do controller logic + MvcResult mvcResult = mockMvc.perform(requestBuilder) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcResultHandlers.print()).andReturn(); + + // Verify the modifyGatewayFlowRules method has been called + verify(sentinelApiClient).modifyGatewayFlowRules(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any()); + + Result result = JSONObject.parseObject(mvcResult.getResponse().getContentAsString(), new TypeReference>() {}); + assertTrue(result.isSuccess()); + + // Verify the result + GatewayFlowRuleEntity entity = result.getData(); + assertNotNull(entity); + assertEquals(TEST_APP, entity.getApp()); + assertEquals(TEST_IP, entity.getIp()); + assertEquals(TEST_PORT, entity.getPort()); + assertEquals(RESOURCE_MODE_ROUTE_ID, entity.getResourceMode().intValue()); + assertEquals("httpbin_route", entity.getResource()); + assertNotNull(entity.getId()); + assertNotNull(entity.getGmtCreate()); + assertNotNull(entity.getGmtModified()); + + // Verify the entity which is add in memory repository + List entitiesInMem = repository.findAllByApp(TEST_APP); + assertEquals(1, entitiesInMem.size()); + assertEquals(entity, entitiesInMem.get(0)); + } + + @Test + public void testUpdateFlowRule() throws Exception { + String path = "/gateway/flow/save.json"; + + // Add one entity into memory repository for update + GatewayFlowRuleEntity addEntity = new GatewayFlowRuleEntity(); + addEntity.setId(1L); + addEntity.setApp(TEST_APP); + addEntity.setIp(TEST_IP); + addEntity.setPort(TEST_PORT); + addEntity.setResource("httpbin_route"); + addEntity.setResourceMode(RESOURCE_MODE_ROUTE_ID); + addEntity.setGrade(FLOW_GRADE_QPS); + addEntity.setCount(5D); + addEntity.setInterval(30L); + addEntity.setIntervalUnit(GatewayFlowRuleEntity.INTERVAL_UNIT_SECOND); + addEntity.setControlBehavior(CONTROL_BEHAVIOR_DEFAULT); + addEntity.setBurst(0); + addEntity.setMaxQueueingTimeoutMs(0); + Date date = new Date(); + // To make the gmtModified different when do update + date = DateUtils.addSeconds(date, -1); + addEntity.setGmtCreate(date); + addEntity.setGmtModified(date); + + GatewayParamFlowItemEntity addItemEntity = new GatewayParamFlowItemEntity(); + addEntity.setParamItem(addItemEntity); + addItemEntity.setParseStrategy(PARAM_PARSE_STRATEGY_CLIENT_IP); + + repository.save(addEntity); + + UpdateFlowRuleReqVo reqVo = new UpdateFlowRuleReqVo(); + reqVo.setId(addEntity.getId()); + reqVo.setApp(TEST_APP); + reqVo.setGrade(FLOW_GRADE_QPS); + reqVo.setCount(6D); + reqVo.setInterval(2L); + reqVo.setIntervalUnit(GatewayFlowRuleEntity.INTERVAL_UNIT_MINUTE); + reqVo.setControlBehavior(CONTROL_BEHAVIOR_RATE_LIMITER); + reqVo.setMaxQueueingTimeoutMs(500); + + GatewayParamFlowItemVo itemVo = new GatewayParamFlowItemVo(); + reqVo.setParamItem(itemVo); + itemVo.setParseStrategy(PARAM_PARSE_STRATEGY_URL_PARAM); + itemVo.setFieldName("pa"); + + given(sentinelApiClient.modifyGatewayFlowRules(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any())).willReturn(true); + + MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.post(path); + requestBuilder.content(JSON.toJSONString(reqVo)).contentType(MediaType.APPLICATION_JSON); + + // Do controller logic + MvcResult mvcResult = mockMvc.perform(requestBuilder) + .andExpect(MockMvcResultMatchers.status().isOk()) + .andDo(MockMvcResultHandlers.print()).andReturn(); + + // Verify the modifyGatewayFlowRules method has been called + verify(sentinelApiClient).modifyGatewayFlowRules(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any()); + + Result result = JSONObject.parseObject(mvcResult.getResponse().getContentAsString(), new TypeReference>() { + }); + assertTrue(result.isSuccess()); + + GatewayFlowRuleEntity entity = result.getData(); + assertNotNull(entity); + assertEquals(RESOURCE_MODE_ROUTE_ID, entity.getResourceMode().intValue()); + assertEquals("httpbin_route", entity.getResource()); + assertEquals(6D, entity.getCount().doubleValue(), 0); + assertEquals(2L, entity.getInterval().longValue()); + assertEquals(GatewayFlowRuleEntity.INTERVAL_UNIT_MINUTE, entity.getIntervalUnit().intValue()); + assertEquals(CONTROL_BEHAVIOR_RATE_LIMITER, entity.getControlBehavior().intValue()); + assertEquals(0, entity.getBurst().intValue()); + assertEquals(500, entity.getMaxQueueingTimeoutMs().intValue()); + assertEquals(date, entity.getGmtCreate()); + // To make sure gmtModified has been set and it's different from gmtCreate + assertNotNull(entity.getGmtModified()); + assertNotEquals(entity.getGmtCreate(), entity.getGmtModified()); + + // Verify the entity which is update in memory repository + GatewayParamFlowItemEntity itemEntity = entity.getParamItem(); + assertEquals(PARAM_PARSE_STRATEGY_URL_PARAM, itemEntity.getParseStrategy().intValue()); + assertEquals("pa", itemEntity.getFieldName()); + } + + @Test + public void testDeleteFlowRule() throws Exception { + String path = "/gateway/flow/delete.json"; + + // Add one entity into memory repository for delete + GatewayFlowRuleEntity addEntity = new GatewayFlowRuleEntity(); + addEntity.setId(1L); + addEntity.setApp(TEST_APP); + addEntity.setIp(TEST_IP); + addEntity.setPort(TEST_PORT); + addEntity.setResource("httpbin_route"); + addEntity.setResourceMode(RESOURCE_MODE_ROUTE_ID); + addEntity.setGrade(FLOW_GRADE_QPS); + addEntity.setCount(5D); + addEntity.setInterval(30L); + addEntity.setIntervalUnit(GatewayFlowRuleEntity.INTERVAL_UNIT_SECOND); + addEntity.setControlBehavior(CONTROL_BEHAVIOR_DEFAULT); + addEntity.setBurst(0); + addEntity.setMaxQueueingTimeoutMs(0); + Date date = new Date(); + date = DateUtils.addSeconds(date, -1); + addEntity.setGmtCreate(date); + addEntity.setGmtModified(date); + + GatewayParamFlowItemEntity addItemEntity = new GatewayParamFlowItemEntity(); + addEntity.setParamItem(addItemEntity); + addItemEntity.setParseStrategy(PARAM_PARSE_STRATEGY_CLIENT_IP); + + repository.save(addEntity); + + given(sentinelApiClient.modifyGatewayFlowRules(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any())).willReturn(true); + + MockHttpServletRequestBuilder requestBuilder = MockMvcRequestBuilders.post(path); + requestBuilder.param("id", String.valueOf(addEntity.getId())); + + // Do controller logic + MvcResult mvcResult = mockMvc.perform(requestBuilder) + .andExpect(MockMvcResultMatchers.status().isOk()).andDo(MockMvcResultHandlers.print()).andReturn(); + + // Verify the modifyGatewayFlowRules method has been called + verify(sentinelApiClient).modifyGatewayFlowRules(eq(TEST_APP), eq(TEST_IP), eq(TEST_PORT), any()); + + // Verify the result + Result result = JSONObject.parseObject(mvcResult.getResponse().getContentAsString(), new TypeReference>() {}); + assertTrue(result.isSuccess()); + + assertEquals(addEntity.getId(), result.getData()); + + // Now no entities in memory + List entitiesInMem = repository.findAllByApp(TEST_APP); + assertEquals(0, entitiesInMem.size()); + } +}