Ver Fonte

canal-admin完善 (#2121)

* admin端口调整完成

* server端适配调整

* 调整部分功能

* 单机/集群admin调试完成

* 重新发布前端页面
rewerma há 6 anos atrás
pai
commit
89055f7405
60 ficheiros alterados com 1869 adições e 379 exclusões
  1. 15 0
      canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/common/DaemonThreadFactory.java
  2. 1 1
      canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/connector/SimpleAdminConnector.java
  3. 89 0
      canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/controller/CanalClusterController.java
  4. 4 3
      canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/controller/CanalConfigController.java
  5. 31 10
      canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/controller/CanalInstanceController.java
  6. 4 2
      canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/controller/NodeServerController.java
  7. 17 69
      canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/controller/PollingConfigController.java
  8. 1 1
      canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/controller/UserController.java
  9. 70 0
      canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/model/CanalCluster.java
  10. 28 1
      canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/model/CanalConfig.java
  11. 73 20
      canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/model/CanalInstanceConfig.java
  12. 30 11
      canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/model/NodeServer.java
  13. 81 0
      canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/model/Pager.java
  14. 18 0
      canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/service/CanalClusterServic.java
  15. 1 1
      canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/service/CanalConfigService.java
  16. 6 1
      canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/service/CanalInstanceService.java
  17. 4 1
      canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/service/NodeServerService.java
  18. 13 0
      canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/service/PollingConfigServer.java
  19. 47 0
      canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/service/impl/CanalClusterServiceImpl.java
  20. 37 14
      canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/service/impl/CanalConfigServiceImpl.java
  21. 217 32
      canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/service/impl/CanalInstanceServiceImpl.java
  22. 62 19
      canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/service/impl/NodeServerServiceImpl.java
  23. 96 0
      canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/service/impl/PollingConfigServiceImpl.java
  24. 9 9
      canal-admin/canal-admin-server/src/main/resources/application.yml
  25. 27 6
      canal-admin/canal-admin-server/src/main/resources/canal_manager.sql
  26. 6 0
      canal-admin/canal-admin-server/src/main/resources/logback.xml
  27. 0 0
      canal-admin/canal-admin-server/src/main/resources/public/index.html
  28. 1 0
      canal-admin/canal-admin-server/src/main/resources/public/static/css/chunk-14b5f7a4.f3e06673.css
  29. 1 0
      canal-admin/canal-admin-server/src/main/resources/public/static/css/chunk-22553be3.f3e06673.css
  30. 1 0
      canal-admin/canal-admin-server/src/main/resources/public/static/css/chunk-2301924a.160e7b4a.css
  31. 0 0
      canal-admin/canal-admin-server/src/main/resources/public/static/css/chunk-49959c8b.e8e2beee.css
  32. 1 0
      canal-admin/canal-admin-server/src/main/resources/public/static/css/chunk-98f505d0.5280f88f.css
  33. 1 0
      canal-admin/canal-admin-server/src/main/resources/public/static/css/chunk-bd1d44ee.1528199a.css
  34. 0 0
      canal-admin/canal-admin-server/src/main/resources/public/static/js/app.cae6e777.js
  35. 0 0
      canal-admin/canal-admin-server/src/main/resources/public/static/js/chunk-0dca2f22.a2bc28b8.js
  36. 0 0
      canal-admin/canal-admin-server/src/main/resources/public/static/js/chunk-14b5f7a4.d0531bb5.js
  37. 0 0
      canal-admin/canal-admin-server/src/main/resources/public/static/js/chunk-22553be3.21abed4f.js
  38. 0 0
      canal-admin/canal-admin-server/src/main/resources/public/static/js/chunk-2301924a.b0e97fa6.js
  39. 1 0
      canal-admin/canal-admin-server/src/main/resources/public/static/js/chunk-49959c8b.6d226f70.js
  40. 0 0
      canal-admin/canal-admin-server/src/main/resources/public/static/js/chunk-55380ff2.681c71c9.js
  41. 0 0
      canal-admin/canal-admin-server/src/main/resources/public/static/js/chunk-7ec889b7.295b8aad.js
  42. 0 0
      canal-admin/canal-admin-server/src/main/resources/public/static/js/chunk-98f505d0.aade2e3b.js
  43. 0 0
      canal-admin/canal-admin-server/src/main/resources/public/static/js/chunk-bd1d44ee.c2f6b1a9.js
  44. 46 0
      canal-admin/canal-admin-ui/src/api/canalCluster.js
  45. 2 2
      canal-admin/canal-admin-ui/src/api/canalConfig.js
  46. 14 0
      canal-admin/canal-admin-ui/src/api/canalInstance.js
  47. 100 0
      canal-admin/canal-admin-ui/src/components/Pagination/index.vue
  48. 12 5
      canal-admin/canal-admin-ui/src/router/index.js
  49. 50 0
      canal-admin/canal-admin-ui/src/utils/scrollTo.js
  50. 204 0
      canal-admin/canal-admin-ui/src/views/canalServer/CanalCluster.vue
  51. 28 6
      canal-admin/canal-admin-ui/src/views/canalServer/CanalConfig.vue
  52. 85 65
      canal-admin/canal-admin-ui/src/views/canalServer/CanalInstance.vue
  53. 26 2
      canal-admin/canal-admin-ui/src/views/canalServer/CanalInstanceAdd.vue
  54. 13 1
      canal-admin/canal-admin-ui/src/views/canalServer/CanalInstanceUpdate.vue
  55. 170 6
      canal-admin/canal-admin-ui/src/views/canalServer/NodeServer.vue
  56. 73 70
      deployer/src/main/java/com/alibaba/otter/canal/deployer/CanalController.java
  57. 12 4
      deployer/src/main/java/com/alibaba/otter/canal/deployer/CanalLauncher.java
  58. 11 9
      deployer/src/main/java/com/alibaba/otter/canal/deployer/monitor/ManagerInstanceConfigMonitor.java
  59. 20 5
      instance/manager/src/main/java/com/alibaba/otter/canal/instance/manager/plain/PlainCanalConfigClient.java
  60. 10 3
      instance/manager/src/test/java/com/alibaba/otter/canal/instance/manager/PlainCanalConfigClientIntegration.java

+ 15 - 0
canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/common/DaemonThreadFactory.java

@@ -0,0 +1,15 @@
+package com.alibaba.otter.canal.admin.common;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+public class DaemonThreadFactory implements ThreadFactory {
+
+    public static final ThreadFactory daemonThreadFactory = new DaemonThreadFactory();
+
+    public Thread newThread(Runnable r) {
+        Thread t = Executors.defaultThreadFactory().newThread(r);
+        t.setDaemon(true);
+        return t;
+    }
+}

+ 1 - 1
canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/connector/SimpleAdminConnector.java

@@ -31,7 +31,7 @@ import com.google.protobuf.ByteString;
 
 /**
  * 基于netty实现的admin控制
- * 
+ *
  * @author agapple 2019年8月26日 上午10:23:44
  * @since 1.1.4
  */

+ 89 - 0
canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/controller/CanalClusterController.java

@@ -0,0 +1,89 @@
+package com.alibaba.otter.canal.admin.controller;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.alibaba.otter.canal.admin.model.BaseModel;
+import com.alibaba.otter.canal.admin.model.CanalCluster;
+import com.alibaba.otter.canal.admin.model.NodeServer;
+import com.alibaba.otter.canal.admin.service.CanalClusterServic;
+import com.alibaba.otter.canal.admin.service.NodeServerService;
+
+@RestController
+@RequestMapping("/api/{env}/canal")
+public class CanalClusterController {
+
+    @Autowired
+    CanalClusterServic canalClusterServic;
+
+    @Autowired
+    NodeServerService  nodeServerService;
+
+    @GetMapping(value = "/clusters")
+    public BaseModel<List<CanalCluster>> clusters(CanalCluster canalCluster, @PathVariable String env) {
+        return BaseModel.getInstance(canalClusterServic.findList(canalCluster));
+    }
+
+    @PostMapping(value = "/cluster")
+    public BaseModel<String> save(@RequestBody CanalCluster canalCluster, @PathVariable String env) {
+        canalClusterServic.save(canalCluster);
+        return BaseModel.getInstance("success");
+    }
+
+    @GetMapping(value = "/cluster/{id}")
+    public BaseModel<CanalCluster> detail(@PathVariable Long id, @PathVariable String env) {
+        return BaseModel.getInstance(canalClusterServic.detail(id));
+    }
+
+    @PutMapping(value = "/cluster")
+    public BaseModel<String> update(@RequestBody CanalCluster canalCluster, @PathVariable String env) {
+        canalClusterServic.update(canalCluster);
+        return BaseModel.getInstance("success");
+    }
+
+    @DeleteMapping(value = "/cluster/{id}")
+    public BaseModel<String> delete(@PathVariable Long id, @PathVariable String env) {
+        canalClusterServic.delete(id);
+        return BaseModel.getInstance("success");
+    }
+
+    @GetMapping(value = "/clustersAndServers")
+    public BaseModel<List<?>> clustersAndServers(@PathVariable String env) {
+        List<CanalCluster> clusters = canalClusterServic.findList(new CanalCluster());
+        JSONObject group = new JSONObject();
+        group.put("label", "集群");
+        JSONArray jsonArray = new JSONArray();
+        clusters.forEach(cluster -> {
+            JSONObject item = new JSONObject();
+            item.put("label", cluster.getName());
+            item.put("value", "cluster:" + cluster.getId());
+            jsonArray.add(item);
+        });
+        group.put("options", jsonArray);
+
+        NodeServer param = new NodeServer();
+        param.setClusterId(-1L);
+        List<NodeServer> servers = nodeServerService.findAll(param); // 取所有standalone的节点
+        JSONObject group2 = new JSONObject();
+        group2.put("label", "单机主机");
+        JSONArray jsonArray2 = new JSONArray();
+        servers.forEach(server -> {
+            JSONObject item = new JSONObject();
+            item.put("label", server.getName());
+            item.put("value", "server:" + server.getId());
+            jsonArray2.add(item);
+        });
+        group2.put("options", jsonArray2);
+
+        List<JSONObject> result = new ArrayList<>();
+        result.add(group);
+        result.add(group2);
+        return BaseModel.getInstance(result);
+    }
+
+}

+ 4 - 3
canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/controller/CanalConfigController.java

@@ -31,9 +31,10 @@ public class CanalConfigController {
      * @param env 环境变量
      * @return 配置信息
      */
-    @GetMapping(value = "/config")
-    public BaseModel<CanalConfig> canalConfig(@PathVariable String env) {
-        return BaseModel.getInstance(canalConfigService.getCanalConfig());
+    @GetMapping(value = "/config/{clusterId}/{serverId}")
+    public BaseModel<CanalConfig> canalConfig(@PathVariable Long clusterId, @PathVariable Long serverId,
+                                              @PathVariable String env) {
+        return BaseModel.getInstance(canalConfigService.getCanalConfig(clusterId, serverId));
     }
 
     /**

+ 31 - 10
canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/controller/CanalInstanceController.java

@@ -3,15 +3,9 @@ package com.alibaba.otter.canal.admin.controller;
 import java.util.List;
 import java.util.Map;
 
+import com.alibaba.otter.canal.admin.model.Pager;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.web.bind.annotation.DeleteMapping;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.PostMapping;
-import org.springframework.web.bind.annotation.PutMapping;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 import com.alibaba.otter.canal.admin.model.BaseModel;
 import com.alibaba.otter.canal.admin.model.CanalInstanceConfig;
@@ -38,8 +32,9 @@ public class CanalInstanceController {
      * @return 实例列表
      */
     @GetMapping(value = "/instances")
-    public BaseModel<List<CanalInstanceConfig>> list(CanalInstanceConfig canalInstanceConfig, @PathVariable String env) {
-        return BaseModel.getInstance(canalInstanceConfigService.findList(canalInstanceConfig));
+    public BaseModel<Pager<CanalInstanceConfig>> list(CanalInstanceConfig canalInstanceConfig,
+                                                      Pager<CanalInstanceConfig> pager, @PathVariable String env) {
+        return BaseModel.getInstance(canalInstanceConfigService.findList(canalInstanceConfig, pager));
     }
 
     /**
@@ -130,6 +125,20 @@ public class CanalInstanceController {
         return BaseModel.getInstance(canalInstanceConfigService.remoteOperation(id, nodeId, "stop"));
     }
 
+    /**
+     * 通过操作instance状态启动/停止远程instance
+     *
+     * @param id 实例配置id
+     * @param option 操作类型: start/stop
+     * @param env 环境变量
+     * @return 是否成功
+     */
+    @PutMapping(value = "/instance/status/{id}")
+    public BaseModel<Boolean> instanceStart(@PathVariable Long id, @RequestParam String option,
+                                            @PathVariable String env) {
+        return BaseModel.getInstance(canalInstanceConfigService.instanceOperation(id, option));
+    }
+
     /**
      * 获取远程实例运行日志
      *
@@ -143,4 +152,16 @@ public class CanalInstanceController {
                                                       @PathVariable String env) {
         return BaseModel.getInstance(canalInstanceConfigService.remoteInstanceLog(id, nodeId));
     }
+
+    /**
+     * 通过Server id获取所有活动的Instance
+     *
+     * @param serverId 节点id
+     * @param env 环境变量
+     * @return 实例列表
+     */
+    @GetMapping(value = "/active/instances/{serverId}")
+    public BaseModel<List<CanalInstanceConfig>> activeInstances(@PathVariable Long serverId, @PathVariable String env) {
+        return BaseModel.getInstance(canalInstanceConfigService.findActiveInstaceByServerId(serverId));
+    }
 }

+ 4 - 2
canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/controller/NodeServerController.java

@@ -7,6 +7,7 @@ import org.springframework.web.bind.annotation.*;
 
 import com.alibaba.otter.canal.admin.model.BaseModel;
 import com.alibaba.otter.canal.admin.model.NodeServer;
+import com.alibaba.otter.canal.admin.model.Pager;
 import com.alibaba.otter.canal.admin.service.NodeServerService;
 
 /**
@@ -30,8 +31,9 @@ public class NodeServerController {
      * @return 节点信息列表
      */
     @GetMapping(value = "/nodeServers")
-    public BaseModel<List<NodeServer>> nodeServers(NodeServer nodeServer, @PathVariable String env) {
-        return BaseModel.getInstance(nodeServerService.findList(nodeServer));
+    public BaseModel<Pager<NodeServer>> nodeServers(NodeServer nodeServer, Pager<NodeServer> pager,
+                                                    @PathVariable String env) {
+        return BaseModel.getInstance(nodeServerService.findList(nodeServer, pager));
     }
 
     /**

+ 17 - 69
canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/controller/PollingConfigController.java

@@ -1,26 +1,17 @@
 package com.alibaba.otter.canal.admin.controller;
 
 import java.security.NoSuchAlgorithmException;
-import java.util.List;
-import java.util.stream.Collectors;
 
 import org.apache.commons.lang.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestHeader;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.bind.annotation.*;
 
 import com.alibaba.otter.canal.admin.model.BaseModel;
 import com.alibaba.otter.canal.admin.model.CanalConfig;
 import com.alibaba.otter.canal.admin.model.CanalInstanceConfig;
-import com.alibaba.otter.canal.admin.service.CanalConfigService;
-import com.alibaba.otter.canal.admin.service.CanalInstanceService;
+import com.alibaba.otter.canal.admin.service.PollingConfigServer;
 import com.alibaba.otter.canal.protocol.SecurityUtil;
-import com.google.common.base.Joiner;
 
 /**
  * Canal Instance配置管理控制层
@@ -34,43 +25,28 @@ public class PollingConfigController {
 
     private static final byte[] seeds = "canal is best!".getBytes();
 
-    @Autowired
-    CanalInstanceService        canalInstanceConfigService;
-
-    @Autowired
-    CanalConfigService          canalConfigService;
-
     @Value(value = "${canal.adminUser}")
     String                      user;
 
     @Value(value = "${canal.adminPasswd}")
     String                      passwd;
 
+    @Autowired
+    PollingConfigServer         pollingConfigServer;
+
     /**
      * 获取server全局配置
      */
     @GetMapping(value = "/server_polling")
     public BaseModel<CanalConfig> canalConfigPoll(@RequestHeader String user, @RequestHeader String passwd,
-                                                  @PathVariable String env, @RequestParam String md5) {
+                                                  @RequestParam String ip, @RequestParam Integer port,
+                                                  @RequestParam String md5, @PathVariable String env) {
         if (!auth(user, passwd)) {
             throw new RuntimeException("auth :" + user + " is failed");
         }
 
-        CanalConfig config = canalConfigService.getCanalConfig();
-        if (StringUtils.isEmpty(md5)) {
-            return BaseModel.getInstance(config);
-        } else {
-
-            try {
-                String newMd5 = SecurityUtil.md5String(config.getContent());
-                if (StringUtils.equals(md5, newMd5)) {
-                    config.setContent(null);
-                }
-            } catch (NoSuchAlgorithmException e) {
-            }
-
-            return BaseModel.getInstance(config);
-        }
+        CanalConfig canalConfig = pollingConfigServer.getChangedConfig(ip, port, md5);
+        return BaseModel.getInstance(canalConfig);
     }
 
     /**
@@ -78,26 +54,14 @@ public class PollingConfigController {
      */
     @GetMapping(value = "/instance_polling/{destination}")
     public BaseModel<CanalInstanceConfig> instanceConfigPoll(@RequestHeader String user, @RequestHeader String passwd,
-                                                             @PathVariable String env,
-                                                             @PathVariable String destination, @RequestParam String md5) {
+                                                             @PathVariable String env, @PathVariable String destination,
+                                                             @RequestParam String md5) {
         if (!auth(user, passwd)) {
             throw new RuntimeException("auth :" + user + " is failed");
         }
 
-        CanalInstanceConfig config = canalInstanceConfigService.findOne(destination);
-        if (StringUtils.isEmpty(md5)) {
-            return BaseModel.getInstance(config);
-        } else {
-            try {
-                String newMd5 = SecurityUtil.md5String(config.getContent());
-                if (StringUtils.equals(md5, newMd5)) {
-                    config.setContent(null);
-                }
-            } catch (NoSuchAlgorithmException e) {
-            }
-
-            return BaseModel.getInstance(config);
-        }
+        CanalInstanceConfig canalInstanceConfig = pollingConfigServer.getInstanceConfig(destination, md5);
+        return BaseModel.getInstance(canalInstanceConfig);
     }
 
     /**
@@ -105,30 +69,14 @@ public class PollingConfigController {
      */
     @GetMapping(value = "/instances_polling")
     public BaseModel<CanalInstanceConfig> instancesPoll(@RequestHeader String user, @RequestHeader String passwd,
-                                                        @PathVariable String env, @RequestParam String ip,
-                                                        @RequestParam String port, @RequestParam String md5) {
+                                                        @RequestParam String ip, @RequestParam Integer port,
+                                                        @RequestParam String md5, @PathVariable String env) {
         if (!auth(user, passwd)) {
             throw new RuntimeException("auth :" + user + " is failed");
         }
 
-        CanalInstanceConfig canalInstanceConfig = new CanalInstanceConfig();
-        List<CanalInstanceConfig> configs = canalInstanceConfigService.findList(canalInstanceConfig);
-        List<String> instances = configs.stream().map(CanalInstanceConfig::getName).collect(Collectors.toList());
-        String data = Joiner.on(',').join(instances);
-        canalInstanceConfig.setContent(data);
-        if (StringUtils.isEmpty(md5)) {
-            return BaseModel.getInstance(canalInstanceConfig);
-        } else {
-            try {
-                String newMd5 = SecurityUtil.md5String(canalInstanceConfig.getContent());
-                if (StringUtils.equals(md5, newMd5)) {
-                    canalInstanceConfig.setContent(null);
-                }
-            } catch (NoSuchAlgorithmException e) {
-            }
-
-            return BaseModel.getInstance(canalInstanceConfig);
-        }
+        CanalInstanceConfig canalInstanceConfig = pollingConfigServer.getInstancesConfig(ip, port, md5);
+        return BaseModel.getInstance(canalInstanceConfig);
     }
 
     private boolean auth(String user, String passwd) {

+ 1 - 1
canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/controller/UserController.java

@@ -27,7 +27,7 @@ public class UserController {
 
     public static final LoadingCache<String, User> loginUsers = Caffeine.newBuilder()
         .maximumSize(10_000)
-        .expireAfterAccess(10, TimeUnit.MINUTES)
+        .expireAfterAccess(30, TimeUnit.MINUTES)
         .build(key -> null);                                                         // 用户登录信息缓存
 
     @Autowired

+ 70 - 0
canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/model/CanalCluster.java

@@ -0,0 +1,70 @@
+package com.alibaba.otter.canal.admin.model;
+
+import io.ebean.Finder;
+
+import javax.persistence.Entity;
+import javax.persistence.Id;
+import javax.persistence.Table;
+import java.util.Date;
+
+/**
+ * Canal集群信息实体类
+ *
+ * @author rewerma 2019-07-13 下午05:12:16
+ * @version 1.0.0
+ */
+@Entity
+@Table(name = "canal_cluster")
+public class CanalCluster extends Model {
+
+    public static final CanalClusterFinder find = new CanalClusterFinder();
+
+    public static class CanalClusterFinder extends Finder<Long, CanalCluster> {
+
+        /**
+         * Construct using the default EbeanServer.
+         */
+        public CanalClusterFinder(){
+            super(CanalCluster.class);
+        }
+
+    }
+
+    @Id
+    private Long   id;
+    private String name;
+    private String zkHosts;
+    private Date   modifiedTime;
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getZkHosts() {
+        return zkHosts;
+    }
+
+    public void setZkHosts(String zkHosts) {
+        this.zkHosts = zkHosts;
+    }
+
+    public Date getModifiedTime() {
+        return modifiedTime;
+    }
+
+    public void setModifiedTime(Date modifiedTime) {
+        this.modifiedTime = modifiedTime;
+    }
+}

+ 28 - 1
canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/model/CanalConfig.java

@@ -8,7 +8,7 @@ import javax.persistence.Entity;
 import javax.persistence.Id;
 
 /**
- * Canal配置实体类
+ * Canal配置实体类
  *
  * @author rewerma 2019-07-13 下午05:12:16
  * @version 1.0.0
@@ -31,8 +31,11 @@ public class CanalConfig extends Model {
 
     @Id
     private Long   id;
+    private Long   clusterId;
+    private Long   serverId;
     private String name;
     private String content;
+    private String contentMd5;
     private String status;
     private Date   modifiedTime;
 
@@ -44,6 +47,22 @@ public class CanalConfig extends Model {
         this.id = id;
     }
 
+    public Long getClusterId() {
+        return clusterId;
+    }
+
+    public void setClusterId(Long clusterId) {
+        this.clusterId = clusterId;
+    }
+
+    public Long getServerId() {
+        return serverId;
+    }
+
+    public void setServerId(Long serverId) {
+        this.serverId = serverId;
+    }
+
     public String getName() {
         return name;
     }
@@ -60,6 +79,14 @@ public class CanalConfig extends Model {
         this.content = content;
     }
 
+    public String getContentMd5() {
+        return contentMd5;
+    }
+
+    public void setContentMd5(String contentMd5) {
+        this.contentMd5 = contentMd5;
+    }
+
     public Date getModifiedTime() {
         return modifiedTime;
     }

+ 73 - 20
canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/model/CanalInstanceConfig.java

@@ -1,12 +1,10 @@
 package com.alibaba.otter.canal.admin.model;
 
-import io.ebean.Finder;
-
 import java.util.Date;
 
-import javax.persistence.Entity;
-import javax.persistence.Id;
-import javax.persistence.Transient;
+import javax.persistence.*;
+
+import io.ebean.Finder;
 
 /**
  * Canal实例配置信息实体类
@@ -31,16 +29,31 @@ public class CanalInstanceConfig extends Model {
     }
 
     @Id
-    private Long   id;
-    private String name;
-    private String content;
-    private String status;
-    private Date   modifiedTime;
+    private Long         id;
+    @Column(name = "cluster_id")
+    private Long         clusterId;
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "cluster_id", updatable = false, insertable = false)
+    private CanalCluster canalCluster;
+    @Column(name = "server_id")
+    private Long         serverId;
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "server_id", updatable = false, insertable = false)
+    private NodeServer   nodeServer;
+    private String       name;
+    private String       content;
+    private String       contentMd5;
+    private String       status;         // 1: 正常 0: 停止
+    private Date         modifiedTime;
 
     @Transient
-    private Long   nodeId;
+    private String       clusterServerId;
     @Transient
-    private String nodeIp;
+    private String       runningStatus = "0";  // 1: 运行中 0: 停止
+
+    public void init() {
+        status = "1";
+    }
 
     public Long getId() {
         return id;
@@ -50,6 +63,38 @@ public class CanalInstanceConfig extends Model {
         this.id = id;
     }
 
+    public Long getClusterId() {
+        return clusterId;
+    }
+
+    public void setClusterId(Long clusterId) {
+        this.clusterId = clusterId;
+    }
+
+    public CanalCluster getCanalCluster() {
+        return canalCluster;
+    }
+
+    public void setCanalCluster(CanalCluster canalCluster) {
+        this.canalCluster = canalCluster;
+    }
+
+    public Long getServerId() {
+        return serverId;
+    }
+
+    public void setServerId(Long serverId) {
+        this.serverId = serverId;
+    }
+
+    public NodeServer getNodeServer() {
+        return nodeServer;
+    }
+
+    public void setNodeServer(NodeServer nodeServer) {
+        this.nodeServer = nodeServer;
+    }
+
     public String getName() {
         return name;
     }
@@ -66,6 +111,14 @@ public class CanalInstanceConfig extends Model {
         this.content = content;
     }
 
+    public String getContentMd5() {
+        return contentMd5;
+    }
+
+    public void setContentMd5(String contentMd5) {
+        this.contentMd5 = contentMd5;
+    }
+
     public String getStatus() {
         return status;
     }
@@ -82,19 +135,19 @@ public class CanalInstanceConfig extends Model {
         this.modifiedTime = modifiedTime;
     }
 
-    public Long getNodeId() {
-        return nodeId;
+    public String getClusterServerId() {
+        return clusterServerId;
     }
 
-    public void setNodeId(Long nodeId) {
-        this.nodeId = nodeId;
+    public void setClusterServerId(String clusterServerId) {
+        this.clusterServerId = clusterServerId;
     }
 
-    public String getNodeIp() {
-        return nodeIp;
+    public String getRunningStatus() {
+        return runningStatus;
     }
 
-    public void setNodeIp(String nodeIp) {
-        this.nodeIp = nodeIp;
+    public void setRunningStatus(String runningStatus) {
+        this.runningStatus = runningStatus;
     }
 }

+ 30 - 11
canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/model/NodeServer.java

@@ -4,9 +4,7 @@ import io.ebean.Finder;
 
 import java.util.Date;
 
-import javax.persistence.Entity;
-import javax.persistence.Id;
-import javax.persistence.Table;
+import javax.persistence.*;
 
 /**
  * 节点信息实体类
@@ -32,14 +30,19 @@ public class NodeServer extends Model {
     }
 
     @Id
-    private Long    id;
-    private String  name;
-    private String  ip;
-    private Integer adminPort;
-    private Integer metricPort;
-    private Integer tcpPort;
-    private String  status;
-    private Date    modifiedTime;
+    private Long         id;
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "cluster_id", updatable = false, insertable = false)
+    private CanalCluster canalCluster;
+    @Column(name = "cluster_id")
+    private Long         clusterId;
+    private String       name;
+    private String       ip;
+    private Integer      adminPort;
+    private Integer      metricPort;
+    private Integer      tcpPort;
+    private String       status;
+    private Date         modifiedTime;
 
     public void init() {
         status = "-1";
@@ -53,6 +56,22 @@ public class NodeServer extends Model {
         this.id = id;
     }
 
+    public CanalCluster getCanalCluster() {
+        return canalCluster;
+    }
+
+    public void setCanalCluster(CanalCluster canalCluster) {
+        this.canalCluster = canalCluster;
+    }
+
+    public Long getClusterId() {
+        return clusterId;
+    }
+
+    public void setClusterId(Long clusterId) {
+        this.clusterId = clusterId;
+    }
+
     public String getName() {
         return name;
     }

+ 81 - 0
canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/model/Pager.java

@@ -0,0 +1,81 @@
+package com.alibaba.otter.canal.admin.model;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.List;
+
+public class Pager<T> implements Serializable {
+
+    private static final long serialVersionUID = -986577815091763517L;
+
+    private Long              count            = 0L;
+    private List<T>           items            = new ArrayList<T>();
+    private Integer           page             = 1;
+    private Integer           size             = 20;
+    private Long              offset           = 0L;
+
+    public Pager(){
+
+    }
+
+    public Pager(Integer page, Integer size){
+        this.page = page;
+        this.size = size;
+    }
+
+    public Pager(Long count, List<T> items){
+        this.count = count;
+        this.items = items;
+    }
+
+    public String toString() {
+        return "PageResult[count=" + this.count + ", items=" + this.items + "]";
+    }
+
+    public Long getCount() {
+        return this.count;
+    }
+
+    public void setCount(Long count) {
+        this.count = count;
+    }
+
+    public List<T> getItems() {
+        return items;
+    }
+
+    public void setItems(List<T> items) {
+        this.items = items;
+    }
+
+    public Integer getPage() {
+        if (page == null) {
+            page = 1;
+        }
+        return page;
+    }
+
+    public void setPage(Integer page) {
+        this.page = page;
+    }
+
+    public Integer getSize() {
+        if (size == null) {
+            size = 20;
+        }
+        return size;
+    }
+
+    public void setSize(Integer size) {
+        this.size = size;
+    }
+
+    public Long getOffset() {
+        offset = (long) (getPage() - 1) * (long) getSize();
+        return offset;
+    }
+
+    public void setOffset(Long offset) {
+        this.offset = offset;
+    }
+}

+ 18 - 0
canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/service/CanalClusterServic.java

@@ -0,0 +1,18 @@
+package com.alibaba.otter.canal.admin.service;
+
+import java.util.List;
+
+import com.alibaba.otter.canal.admin.model.CanalCluster;
+
+public interface CanalClusterServic {
+
+    void save(CanalCluster canalCluster);
+
+    CanalCluster detail(Long id);
+
+    void update(CanalCluster canalCluster);
+
+    void delete(Long id);
+
+    List<CanalCluster> findList(CanalCluster canalCluster);
+}

+ 1 - 1
canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/service/CanalConfigService.java

@@ -4,7 +4,7 @@ import com.alibaba.otter.canal.admin.model.CanalConfig;
 
 public interface CanalConfigService {
 
-    CanalConfig getCanalConfig();
+    CanalConfig getCanalConfig(Long clusterId, Long serverId);
 
     CanalConfig getCanalConfigSummary();
 

+ 6 - 1
canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/service/CanalInstanceService.java

@@ -4,6 +4,7 @@ import java.util.List;
 import java.util.Map;
 
 import com.alibaba.otter.canal.admin.model.CanalInstanceConfig;
+import com.alibaba.otter.canal.admin.model.Pager;
 
 /**
  * Canal实例配置信息业务层接口
@@ -13,7 +14,7 @@ import com.alibaba.otter.canal.admin.model.CanalInstanceConfig;
  */
 public interface CanalInstanceService {
 
-    List<CanalInstanceConfig> findList(CanalInstanceConfig canalInstanceConfig);
+    Pager<CanalInstanceConfig> findList(CanalInstanceConfig canalInstanceConfig, Pager<CanalInstanceConfig> pager);
 
     void save(CanalInstanceConfig canalInstanceConfig);
 
@@ -28,4 +29,8 @@ public interface CanalInstanceService {
     Map<String, String> remoteInstanceLog(Long id, Long nodeId);
 
     boolean remoteOperation(Long id, Long nodeId, String option);
+
+    boolean instanceOperation(Long id, String option);
+
+    List<CanalInstanceConfig> findActiveInstaceByServerId(Long serverId);
 }

+ 4 - 1
canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/service/NodeServerService.java

@@ -1,6 +1,7 @@
 package com.alibaba.otter.canal.admin.service;
 
 import com.alibaba.otter.canal.admin.model.NodeServer;
+import com.alibaba.otter.canal.admin.model.Pager;
 
 import java.util.List;
 
@@ -14,7 +15,9 @@ public interface NodeServerService {
 
     void delete(Long id);
 
-    List<NodeServer> findList(NodeServer nodeServer);
+    List<NodeServer> findAll(NodeServer nodeServer);
+
+    Pager<NodeServer> findList(NodeServer nodeServer, Pager<NodeServer> pager);
 
     int remoteNodeStatus(String ip, Integer port);
 

+ 13 - 0
canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/service/PollingConfigServer.java

@@ -0,0 +1,13 @@
+package com.alibaba.otter.canal.admin.service;
+
+import com.alibaba.otter.canal.admin.model.CanalConfig;
+import com.alibaba.otter.canal.admin.model.CanalInstanceConfig;
+
+public interface PollingConfigServer {
+
+    CanalConfig getChangedConfig(String ip, Integer port, String md5);
+
+    CanalInstanceConfig getInstancesConfig(String ip, Integer port, String md5);
+
+    CanalInstanceConfig getInstanceConfig(String destination, String md5);
+}

+ 47 - 0
canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/service/impl/CanalClusterServiceImpl.java

@@ -0,0 +1,47 @@
+package com.alibaba.otter.canal.admin.service.impl;
+
+import java.util.List;
+
+import com.alibaba.otter.canal.admin.common.exception.ServiceException;
+import com.alibaba.otter.canal.admin.model.NodeServer;
+import org.springframework.stereotype.Service;
+
+import com.alibaba.otter.canal.admin.model.CanalCluster;
+import com.alibaba.otter.canal.admin.service.CanalClusterServic;
+
+import io.ebean.Query;
+
+@Service
+public class CanalClusterServiceImpl implements CanalClusterServic {
+
+    public void save(CanalCluster canalCluster) {
+        canalCluster.save();
+    }
+
+    public CanalCluster detail(Long id) {
+        return CanalCluster.find.byId(id);
+    }
+
+    public void update(CanalCluster canalCluster) {
+        canalCluster.update("name", "zkHosts");
+    }
+
+    public void delete(Long id) {
+        // 判断集群下是否存在server信息
+        int serverCnt = NodeServer.find.query().where().eq("clusterId", id).findCount();
+        if (serverCnt > 0) {
+            throw new ServiceException("Servers exist, delete failed");
+        }
+
+        CanalCluster canalCluster = CanalCluster.find.byId(id);
+        if (canalCluster != null) {
+            canalCluster.delete();
+        }
+    }
+
+    public List<CanalCluster> findList(CanalCluster canalCluster) {
+        Query<CanalCluster> query = CanalCluster.find.query();
+        query.order().asc("id");
+        return query.findList();
+    }
+}

+ 37 - 14
canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/service/impl/CanalConfigServiceImpl.java

@@ -2,8 +2,14 @@ package com.alibaba.otter.canal.admin.service.impl;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.security.NoSuchAlgorithmException;
 import java.util.Date;
 
+import com.alibaba.otter.canal.admin.common.exception.ServiceException;
+import com.alibaba.otter.canal.admin.model.CanalCluster;
+import com.alibaba.otter.canal.admin.model.NodeServer;
+import com.alibaba.otter.canal.protocol.SecurityUtil;
+import io.ebean.Query;
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang.StringUtils;
 import org.slf4j.Logger;
@@ -27,20 +33,27 @@ public class CanalConfigServiceImpl implements CanalConfigService {
     private static final String CANAL_GLOBAL_CONFIG  = "canal.properties";
     private static final String CANAL_ADAPTER_CONFIG = "application.yml";
 
-    public CanalConfig getCanalConfig() {
-        long id = 1L;
-        CanalConfig config = CanalConfig.find.byId(id);
-        if (config == null) {
-            String context = loadDefaultConf(CANAL_GLOBAL_CONFIG);
-            if (context == null) {
-                return null;
+    public CanalConfig getCanalConfig(Long clusterId, Long serverId) {
+        CanalConfig config = null;
+        if (clusterId != null && clusterId != 0) {
+            config = CanalConfig.find.query().where().eq("clusterId", clusterId).findOne();
+        } else if (serverId != null && serverId != 0) {
+            config = CanalConfig.find.query().where().eq("serverId", serverId).findOne();
+            if (config == null) {
+                NodeServer nodeServer = NodeServer.find.byId(serverId);
+                if (nodeServer != null) {
+                    Long cid = nodeServer.getClusterId();
+                    if (cid != null) {
+                        config = CanalConfig.find.query().where().eq("clusterId", cid).findOne();
+                    }
+                }
             }
-
+        } else {
+            throw new ServiceException("clusterId and serverId are all empty");
+        }
+        if (config == null) {
             config = new CanalConfig();
-            config.setId(id);
             config.setName(CANAL_GLOBAL_CONFIG);
-            config.setModifiedTime(new Date());
-            config.setContent(context);
             return config;
         }
 
@@ -78,9 +91,19 @@ public class CanalConfigServiceImpl implements CanalConfigService {
 
     public void updateContent(CanalConfig canalConfig) {
         try {
-            canalConfig.insert();
-        } catch (Throwable e) {
-            canalConfig.update();
+            String contentMd5 = SecurityUtil.md5String(canalConfig.getContent());
+            canalConfig.setContentMd5(contentMd5);
+        } catch (NoSuchAlgorithmException e) {
+            // ignore
+        }
+        if (canalConfig.getId() != null) {
+            CanalConfig canalConfigTmp = CanalConfig.find.byId(canalConfig.getId());
+            if (canalConfigTmp != null && canalConfigTmp.getClusterId() != null) {
+                canalConfig.setServerId(null);
+            }
+            canalConfig.update("serverId", "content", "contentMd5");
+        } else {
+            canalConfig.save();
         }
     }
 

+ 217 - 32
canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/service/impl/CanalInstanceServiceImpl.java

@@ -1,19 +1,23 @@
 package com.alibaba.otter.canal.admin.service.impl;
 
-import io.ebean.Query;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.security.NoSuchAlgorithmException;
+import java.util.*;
+import java.util.concurrent.*;
 
+import com.alibaba.otter.canal.admin.model.Pager;
 import org.apache.commons.lang.StringUtils;
 import org.springframework.stereotype.Service;
 
+import com.alibaba.otter.canal.admin.common.DaemonThreadFactory;
+import com.alibaba.otter.canal.admin.common.exception.ServiceException;
 import com.alibaba.otter.canal.admin.connector.AdminConnector;
 import com.alibaba.otter.canal.admin.connector.SimpleAdminConnectors;
 import com.alibaba.otter.canal.admin.model.CanalInstanceConfig;
 import com.alibaba.otter.canal.admin.model.NodeServer;
 import com.alibaba.otter.canal.admin.service.CanalInstanceService;
+import com.alibaba.otter.canal.protocol.SecurityUtil;
+
+import io.ebean.Query;
 
 /**
  * Canal实例配置信息业务层
@@ -24,52 +28,196 @@ import com.alibaba.otter.canal.admin.service.CanalInstanceService;
 @Service
 public class CanalInstanceServiceImpl implements CanalInstanceService {
 
-    public List<CanalInstanceConfig> findList(CanalInstanceConfig canalInstanceConfig) {
+    public Pager<CanalInstanceConfig> findList(CanalInstanceConfig canalInstanceConfig,
+                                               Pager<CanalInstanceConfig> pager) {
         Query<CanalInstanceConfig> query = CanalInstanceConfig.find.query()
             .setDisableLazyLoading(true)
-            .select("name, modifiedTime");
+            .select("clusterId, serverId, name, modifiedTime")
+            .fetch("canalCluster", "name")
+            .fetch("nodeServer", "name,ip,adminPort");
         if (canalInstanceConfig != null) {
             if (StringUtils.isNotEmpty(canalInstanceConfig.getName())) {
                 query.where().like("name", "%" + canalInstanceConfig.getName() + "%");
             }
+            if (StringUtils.isNotEmpty(canalInstanceConfig.getClusterServerId())) {
+                if (canalInstanceConfig.getClusterServerId().startsWith("cluster:")) {
+                    query.where()
+                        .eq("clusterId", Long.parseLong(canalInstanceConfig.getClusterServerId().substring(8)));
+                } else if (canalInstanceConfig.getClusterServerId().startsWith("server:")) {
+                    query.where().eq("serverId", Long.parseLong(canalInstanceConfig.getClusterServerId().substring(7)));
+                }
+            }
         }
-        query.order().asc("id");
+
+        Query<CanalInstanceConfig> queryCnt = query.copy();
+        int count = queryCnt.findCount();
+        pager.setCount((long) count);
+
+        query.setFirstRow(pager.getOffset().intValue()).setMaxRows(pager.getSize()).order().asc("id");
         List<CanalInstanceConfig> canalInstanceConfigs = query.findList();
+        pager.setItems(canalInstanceConfigs);
+
+        if (canalInstanceConfigs.isEmpty()) {
+            return pager;
+        }
 
         // check all canal instances running status
-        List<NodeServer> nodeServers = NodeServer.find.query().findList();
-        for (NodeServer nodeServer : nodeServers) {
-            String runningInstances = SimpleAdminConnectors.execute(nodeServer.getIp(),
-                nodeServer.getAdminPort(),
-                AdminConnector::getRunningInstances);
-            if (runningInstances == null) {
-                continue;
-            }
-            String[] instances = runningInstances.split(",");
-            for (String instance : instances) {
-                for (CanalInstanceConfig cig : canalInstanceConfigs) {
-                    if (instance.equals(cig.getName())) {
-                        cig.setNodeId(nodeServer.getId());
-                        cig.setNodeIp(nodeServer.getIp());
-                        break;
+        ExecutorService executorService = Executors.newFixedThreadPool(canalInstanceConfigs.size(),
+            DaemonThreadFactory.daemonThreadFactory);
+        List<Future<Void>> futures = new ArrayList<>(canalInstanceConfigs.size());
+
+        for (CanalInstanceConfig canalInstanceConfig1 : canalInstanceConfigs) {
+            futures.add(executorService.submit(() -> {
+                List<NodeServer> nodeServers;
+                if (canalInstanceConfig1.getClusterId() != null) { // 集群模式
+                    nodeServers = NodeServer.find.query()
+                        .where()
+                        .eq("clusterId", canalInstanceConfig1.getClusterId())
+                        .findList();
+                } else if (canalInstanceConfig1.getServerId() != null) { // 单机模式
+                    nodeServers = Collections.singletonList(canalInstanceConfig1.getNodeServer());
+                } else {
+                    return null;
+                }
+
+                for (NodeServer nodeServer : nodeServers) {
+                    String runningInstances = SimpleAdminConnectors
+                        .execute(nodeServer.getIp(), nodeServer.getAdminPort(), AdminConnector::getRunningInstances);
+                    if (runningInstances == null) {
+                        continue;
+                    }
+                    String[] instances = runningInstances.split(",");
+                    for (String instance : instances) {
+                        if (instance.equals(canalInstanceConfig1.getName())) {
+                            if (canalInstanceConfig1.getNodeServer() == null) { // 集群模式下 server 对象为空
+                                canalInstanceConfig1.setNodeServer(nodeServer);
+                            }
+                            canalInstanceConfig1.setRunningStatus("1");
+                            break;
+                        }
                     }
                 }
+
+                return null;
+            }));
+        }
+
+        futures.forEach(f -> {
+            try {
+                f.get(3, TimeUnit.SECONDS);
+            } catch (TimeoutException | InterruptedException | ExecutionException e) {
+                // ignore
             }
+        });
+        executorService.shutdownNow();
+
+        return pager;
+    }
+
+    /**
+     * 通过Server id获取当前Server下所有运行的Instance
+     *
+     * @param serverId server id
+     */
+    public List<CanalInstanceConfig> findActiveInstaceByServerId(Long serverId) {
+        NodeServer nodeServer = NodeServer.find.byId(serverId);
+        if (nodeServer == null) {
+            return null;
+        }
+        String runningInstances = SimpleAdminConnectors
+            .execute(nodeServer.getIp(), nodeServer.getAdminPort(), AdminConnector::getRunningInstances);
+        if (runningInstances == null) {
+            return null;
         }
 
-        return canalInstanceConfigs;
+        String[] instances = runningInstances.split(",");
+
+        // 单机模式和集群模式区分处理
+        if (nodeServer.getClusterId() != null) { // 集群模式
+            List<CanalInstanceConfig> list = CanalInstanceConfig.find.query()
+                .setDisableLazyLoading(true)
+                .select("clusterId, serverId, name, modifiedTime")
+                .where()
+                // 暂停的实例也显示 .eq("status", "1")
+                .in("name", instances)
+                .findList();
+            list.forEach(config -> config.setRunningStatus("1"));
+            return list; // 集群模式直接返回当前运行的Instances
+        } else { // 单机模式
+            // 当前Server所配置的所有Instance
+            List<CanalInstanceConfig> list = CanalInstanceConfig.find.query()
+                .setDisableLazyLoading(true)
+                .select("clusterId, serverId, name, modifiedTime")
+                .where()
+                // 暂停的实例也显示 .eq("status", "1")
+                .eq("serverId", serverId)
+                .findList();
+            List<String> instanceList = Arrays.asList(instances);
+            list.forEach(config -> {
+                if (instanceList.contains(config.getName())) {
+                    config.setRunningStatus("1");
+                }
+            });
+            return list;
+        }
     }
 
     public void save(CanalInstanceConfig canalInstanceConfig) {
+        if (StringUtils.isEmpty(canalInstanceConfig.getClusterServerId())) {
+            throw new ServiceException("empty cluster or server id");
+        }
+        if (canalInstanceConfig.getClusterServerId().startsWith("cluster:")) {
+            Long clusterId = Long.parseLong(canalInstanceConfig.getClusterServerId().substring(8));
+            canalInstanceConfig.setClusterId(clusterId);
+        } else if (canalInstanceConfig.getClusterServerId().startsWith("server:")) {
+            Long serverId = Long.parseLong(canalInstanceConfig.getClusterServerId().substring(7));
+            canalInstanceConfig.setServerId(serverId);
+        }
+
+        try {
+            String contentMd5 = SecurityUtil.md5String(canalInstanceConfig.getContent());
+            canalInstanceConfig.setContentMd5(contentMd5);
+        } catch (NoSuchAlgorithmException e) {
+            // ignore
+        }
+
         canalInstanceConfig.insert();
     }
 
     public CanalInstanceConfig detail(Long id) {
-        return CanalInstanceConfig.find.byId(id);
+        CanalInstanceConfig canalInstanceConfig = CanalInstanceConfig.find.byId(id);
+        if (canalInstanceConfig != null) {
+            if (canalInstanceConfig.getClusterId() != null) {
+                canalInstanceConfig.setClusterServerId("cluster:" + canalInstanceConfig.getClusterId());
+            } else if (canalInstanceConfig.getServerId() != null) {
+                canalInstanceConfig.setClusterServerId("server:" + canalInstanceConfig.getServerId());
+            }
+        }
+        return canalInstanceConfig;
     }
 
     public void updateContent(CanalInstanceConfig canalInstanceConfig) {
-        canalInstanceConfig.update("content");
+        if (StringUtils.isEmpty(canalInstanceConfig.getClusterServerId())) {
+            throw new ServiceException("empty cluster or server id");
+        }
+        if (canalInstanceConfig.getClusterServerId().startsWith("cluster:")) {
+            Long clusterId = Long.parseLong(canalInstanceConfig.getClusterServerId().substring(8));
+            canalInstanceConfig.setClusterId(clusterId);
+            canalInstanceConfig.setServerId(null);
+        } else if (canalInstanceConfig.getClusterServerId().startsWith("server:")) {
+            Long serverId = Long.parseLong(canalInstanceConfig.getClusterServerId().substring(7));
+            canalInstanceConfig.setServerId(serverId);
+            canalInstanceConfig.setClusterId(null);
+        }
+
+        try {
+            String contentMd5 = SecurityUtil.md5String(canalInstanceConfig.getContent());
+            canalInstanceConfig.setContentMd5(contentMd5);
+        } catch (NoSuchAlgorithmException e) {
+            // ignore
+        }
+
+        canalInstanceConfig.update("content", "contentMd5", "clusterId", "serverId");
     }
 
     public void delete(Long id) {
@@ -133,13 +281,34 @@ public class CanalInstanceServiceImpl implements CanalInstanceService {
         }
         Boolean resutl = null;
         if ("start".equals(option)) {
-            resutl = SimpleAdminConnectors.execute(nodeServer.getIp(),
-                nodeServer.getAdminPort(),
-                adminConnector -> adminConnector.startInstance(canalInstanceConfig.getName()));
+            if (nodeServer.getClusterId() == null) { // 非集群模式
+                return instanceOperation(id, "start");
+                // resutl = SimpleAdminConnectors.execute(nodeServer.getIp(),
+                // nodeServer.getAdminPort(),
+                // adminConnector ->
+                // adminConnector.startInstance(canalInstanceConfig.getName()));
+            }
         } else if ("stop".equals(option)) {
-            resutl = SimpleAdminConnectors.execute(nodeServer.getIp(),
-                nodeServer.getAdminPort(),
-                adminConnector -> adminConnector.stopInstance(canalInstanceConfig.getName()));
+            if (nodeServer.getClusterId() != null) {
+                resutl = SimpleAdminConnectors.execute(nodeServer.getIp(),
+                    nodeServer.getAdminPort(),
+                    adminConnector -> adminConnector.stopInstance(canalInstanceConfig.getName()));
+
+                NodeServer nodeServerTmp = nodeServer;
+                // 集群模式下停止实例后过五秒钟再次启动进行启动抢占
+                Thread thread = new Thread(() -> {
+                    try {
+                        Thread.sleep(5000);
+                        SimpleAdminConnectors.execute(nodeServerTmp.getIp(),
+                            nodeServerTmp.getAdminPort(),
+                            adminConnector -> adminConnector.startInstance(canalInstanceConfig.getName()));
+                    } catch (Throwable e) {
+                    }
+                });
+                thread.start();
+            } else { // 非集群模式下直接将状态置为0
+                return instanceOperation(id, "stop");
+            }
         } else {
             return false;
         }
@@ -150,4 +319,20 @@ public class CanalInstanceServiceImpl implements CanalInstanceService {
         return resutl;
     }
 
+    public boolean instanceOperation(Long id, String option) {
+        CanalInstanceConfig canalInstanceConfig = CanalInstanceConfig.find.byId(id);
+        if (canalInstanceConfig == null) {
+            return false;
+        }
+        if ("stop".equals(option)) {
+            canalInstanceConfig.setStatus("0");
+            canalInstanceConfig.update("status");
+        } else if ("start".equals(option)) {
+            canalInstanceConfig.setStatus("1");
+            canalInstanceConfig.update("status");
+        } else {
+            return false;
+        }
+        return true;
+    }
 }

+ 62 - 19
canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/service/impl/NodeServerServiceImpl.java

@@ -1,13 +1,12 @@
 package com.alibaba.otter.canal.admin.service.impl;
 
+import com.alibaba.otter.canal.admin.common.DaemonThreadFactory;
+import com.alibaba.otter.canal.admin.model.*;
 import io.ebean.Query;
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
+import java.util.concurrent.*;
 
 import org.apache.commons.lang.StringUtils;
 import org.springframework.stereotype.Service;
@@ -15,7 +14,6 @@ import org.springframework.stereotype.Service;
 import com.alibaba.otter.canal.admin.common.exception.ServiceException;
 import com.alibaba.otter.canal.admin.connector.AdminConnector;
 import com.alibaba.otter.canal.admin.connector.SimpleAdminConnectors;
-import com.alibaba.otter.canal.admin.model.NodeServer;
 import com.alibaba.otter.canal.admin.service.NodeServerService;
 
 /**
@@ -31,7 +29,7 @@ public class NodeServerServiceImpl implements NodeServerService {
         int cnt = NodeServer.find.query()
             .where()
             .eq("ip", nodeServer.getIp())
-            .eq("admin_port", nodeServer.getAdminPort())
+            .eq("adminPort", nodeServer.getAdminPort())
             .findCount();
         if (cnt > 0) {
             throw new ServiceException("节点信息已存在");
@@ -48,25 +46,39 @@ public class NodeServerServiceImpl implements NodeServerService {
         int cnt = NodeServer.find.query()
             .where()
             .eq("ip", nodeServer.getIp())
-            .eq("admin_port", nodeServer.getAdminPort())
+            .eq("adminPort", nodeServer.getAdminPort())
             .ne("id", nodeServer.getId())
             .findCount();
         if (cnt > 0) {
             throw new ServiceException("节点信息已存在");
         }
 
-        nodeServer.update("name", "ip", "admin_port", "tcp_port", "metric_port");
+        nodeServer.update("name", "ip", "adminPort", "tcpPort", "metricPort", "clusterId");
     }
 
     public void delete(Long id) {
         NodeServer nodeServer = NodeServer.find.byId(id);
         if (nodeServer != null) {
+            // 判断是否存在实例
+            int cnt = CanalInstanceConfig.find.query().where().eq("serverId", nodeServer.getId()).findCount();
+            if (cnt > 0) {
+                throw new ServiceException("当前Server下存在Instance配置, 无法删除");
+            }
+
+            // 同时删除配置
+            CanalConfig canalConfig = CanalConfig.find.query().where().eq("serverId", id).findOne();
+            if (canalConfig != null) {
+                canalConfig.delete();
+            }
+
             nodeServer.delete();
         }
     }
 
-    public List<NodeServer> findList(NodeServer nodeServer) {
+    private Query<NodeServer> getBaseQuery(NodeServer nodeServer) {
         Query<NodeServer> query = NodeServer.find.query();
+        query.fetch("canalCluster", "name").setDisableLazyLoading(true);
+
         if (nodeServer != null) {
             if (StringUtils.isNotEmpty(nodeServer.getName())) {
                 query.where().like("name", "%" + nodeServer.getName() + "%");
@@ -74,14 +86,45 @@ public class NodeServerServiceImpl implements NodeServerService {
             if (StringUtils.isNotEmpty(nodeServer.getIp())) {
                 query.where().eq("ip", nodeServer.getIp());
             }
+            if (nodeServer.getClusterId() != null) {
+                if (nodeServer.getClusterId() == -1) {
+                    query.where().isNull("clusterId");
+                } else {
+                    query.where().eq("clusterId", nodeServer.getClusterId());
+                }
+            }
         }
+
+        return query;
+    }
+
+    public List<NodeServer> findAll(NodeServer nodeServer) {
+        Query<NodeServer> query = getBaseQuery(nodeServer);
         query.order().asc("id");
-        List<NodeServer> nodeServers = query.findList();
+        return query.findList();
+    }
+
+    public Pager<NodeServer> findList(NodeServer nodeServer, Pager<NodeServer> pager) {
+
+        Query<NodeServer> query = getBaseQuery(nodeServer);
+        Query<NodeServer> queryCnt = query.copy();
+
+        int count = queryCnt.findCount();
+        pager.setCount((long) count);
+
+        List<NodeServer> nodeServers = query.order()
+            .asc("id")
+            .setFirstRow(pager.getOffset().intValue())
+            .setMaxRows(pager.getSize())
+            .findList();
+        pager.setItems(nodeServers);
+
         if (nodeServers.isEmpty()) {
-            return nodeServers;
+            return pager;
         }
 
-        ExecutorService executorService = Executors.newFixedThreadPool(nodeServers.size());
+        ExecutorService executorService = Executors.newFixedThreadPool(nodeServers.size(),
+            DaemonThreadFactory.daemonThreadFactory);
         List<Future<Boolean>> futures = new ArrayList<>(nodeServers.size());
         // get all nodes status
         for (NodeServer ns : nodeServers) {
@@ -93,15 +136,15 @@ public class NodeServerServiceImpl implements NodeServerService {
         }
         futures.forEach(f -> {
             try {
-                f.get();
-            } catch (InterruptedException | ExecutionException e) {
+                f.get(3, TimeUnit.SECONDS);
+            } catch (TimeoutException | InterruptedException | ExecutionException e) {
                 // ignore
             }
         });
 
         executorService.shutdownNow();
 
-        return nodeServers;
+        return pager;
     }
 
     public int remoteNodeStatus(String ip, Integer port) {
@@ -114,9 +157,8 @@ public class NodeServerServiceImpl implements NodeServerService {
         if (nodeServer == null) {
             return "";
         }
-        return SimpleAdminConnectors.execute(nodeServer.getIp(),
-            nodeServer.getAdminPort(),
-            adminConnector -> adminConnector.canalLog(100));
+        return SimpleAdminConnectors
+            .execute(nodeServer.getIp(), nodeServer.getAdminPort(), adminConnector -> adminConnector.canalLog(100));
     }
 
     public boolean remoteOperation(Long id, String option) {
@@ -126,7 +168,8 @@ public class NodeServerServiceImpl implements NodeServerService {
         }
         Boolean result = null;
         if ("start".equals(option)) {
-            result = SimpleAdminConnectors.execute(nodeServer.getIp(), nodeServer.getAdminPort(), AdminConnector::start);
+            result = SimpleAdminConnectors
+                .execute(nodeServer.getIp(), nodeServer.getAdminPort(), AdminConnector::start);
         } else if ("stop".equals(option)) {
             result = SimpleAdminConnectors.execute(nodeServer.getIp(), nodeServer.getAdminPort(), AdminConnector::stop);
         } else {

+ 96 - 0
canal-admin/canal-admin-server/src/main/java/com/alibaba/otter/canal/admin/service/impl/PollingConfigServiceImpl.java

@@ -0,0 +1,96 @@
+package com.alibaba.otter.canal.admin.service.impl;
+
+import java.security.NoSuchAlgorithmException;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.commons.lang.StringUtils;
+import org.springframework.stereotype.Service;
+
+import com.alibaba.otter.canal.admin.model.CanalConfig;
+import com.alibaba.otter.canal.admin.model.CanalInstanceConfig;
+import com.alibaba.otter.canal.admin.model.NodeServer;
+import com.alibaba.otter.canal.admin.service.PollingConfigServer;
+import com.alibaba.otter.canal.protocol.SecurityUtil;
+import com.google.common.base.Joiner;
+
+@Service
+public class PollingConfigServiceImpl implements PollingConfigServer {
+
+    public CanalConfig getChangedConfig(String ip, Integer port, String md5) {
+        NodeServer server = NodeServer.find.query().where().eq("ip", ip).eq("adminPort", port).findOne();
+        if (server == null) {
+            return null;
+        }
+        CanalConfig canalConfig;
+        if (server.getClusterId() != null) { // 集群模式
+            canalConfig = CanalConfig.find.query().where().eq("clusterId", server.getClusterId()).findOne();
+        } else { // 单机模式
+            canalConfig = CanalConfig.find.query().where().eq("serverId", server.getId()).findOne();
+        }
+        if (canalConfig != null && !canalConfig.getContentMd5().equals(md5)) { // 内容发生变化
+            return canalConfig;
+        }
+        return null;
+    }
+
+    public CanalInstanceConfig getInstancesConfig(String ip, Integer port, String md5) {
+        NodeServer server = NodeServer.find.query().where().eq("ip", ip).eq("adminPort", port).findOne();
+        if (server == null) {
+            return null;
+        }
+        List<CanalInstanceConfig> canalInstanceConfigs;
+        if (server.getClusterId() != null) { // 集群模式
+            canalInstanceConfigs = CanalInstanceConfig.find.query()
+                .where()
+                .eq("status", "1")
+                .eq("clusterId", server.getClusterId())
+                .findList(); // 取属于该集群的所有instance config
+        } else { // 单机模式
+            canalInstanceConfigs = CanalInstanceConfig.find.query()
+                .where()
+                .eq("status", "1")
+                .eq("serverId", server.getId())
+                .findList();
+        }
+
+        CanalInstanceConfig canalInstanceConfig = new CanalInstanceConfig();
+        List<String> instances = canalInstanceConfigs.stream()
+            .map(CanalInstanceConfig::getName)
+            .collect(Collectors.toList());
+        String data = Joiner.on(',').join(instances);
+        canalInstanceConfig.setContent(data);
+        if (!StringUtils.isEmpty(md5)) {
+            try {
+                String newMd5 = SecurityUtil.md5String(canalInstanceConfig.getContent());
+                if (StringUtils.equals(md5, newMd5)) {
+                    canalInstanceConfig.setContent(null);
+                }
+            } catch (NoSuchAlgorithmException e) {
+                // ignore
+            }
+        }
+        return canalInstanceConfig;
+    }
+
+    public CanalInstanceConfig getInstanceConfig(String destination, String md5) {
+        CanalInstanceConfig instanceConfig = CanalInstanceConfig.find.query().where().eq("name", destination).findOne();
+        if (instanceConfig == null) {
+            return null;
+        }
+        if (StringUtils.isEmpty(md5)) {
+            return instanceConfig;
+        } else {
+            try {
+                String newMd5 = SecurityUtil.md5String(instanceConfig.getContent());
+                if (StringUtils.equals(md5, newMd5)) {
+                    instanceConfig.setContent(null);
+                }
+            } catch (NoSuchAlgorithmException e) {
+                // ignore
+            }
+
+            return instanceConfig;
+        }
+    }
+}

+ 9 - 9
canal-admin/canal-admin-server/src/main/resources/application.yml

@@ -6,14 +6,14 @@ spring:
     time-zone: GMT+8
 
 spring.datasource:
-    url: jdbc:mysql://127.0.0.1:3306/canal_manager?useUnicode=true&characterEncoding=UTF-8&useSSL=false
-    username: root
-    password: 121212
-    driver-class-name: com.mysql.jdbc.Driver
-    hikari:
-      maximum-pool-size: 10
-      minimum-idle: 1
+  url: jdbc:mysql://127.0.0.1:3306/canal_manager?useUnicode=true&characterEncoding=UTF-8&useSSL=false
+  username: root
+  password: 121212
+  driver-class-name: com.mysql.jdbc.Driver
+  hikari:
+    maximum-pool-size: 10
+    minimum-idle: 1
 
 canal:
-    adminUser: admin
-    adminPasswd: admin
+  adminUser: admin
+  adminPasswd: admin

+ 27 - 6
canal-admin/canal-admin-server/src/main/resources/canal_manager.sql

@@ -17,7 +17,19 @@ CREATE TABLE `canal_adapter_config` (
   `content` text NOT NULL,
   `modified_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
   PRIMARY KEY (`id`)
-) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+-- ----------------------------
+-- Table structure for canal_cluster
+-- ----------------------------
+DROP TABLE IF EXISTS `canal_cluster`;
+CREATE TABLE `canal_cluster` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT,
+  `name` varchar(63) NOT NULL,
+  `zk_hosts` varchar(255) NOT NULL,
+  `modified_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+  PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
 -- ----------------------------
 -- Table structure for canal_config
@@ -25,13 +37,16 @@ CREATE TABLE `canal_adapter_config` (
 DROP TABLE IF EXISTS `canal_config`;
 CREATE TABLE `canal_config` (
   `id` bigint(20) NOT NULL AUTO_INCREMENT,
+  `cluster_id` bigint(20) DEFAULT NULL,
+  `server_id` bigint(20) DEFAULT NULL,
   `name` varchar(45) NOT NULL,
   `status` varchar(45) DEFAULT NULL,
   `content` text NOT NULL,
+  `content_md5` varchar(128) NOT NULL,
   `modified_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
   PRIMARY KEY (`id`),
-  UNIQUE KEY `name_UNIQUE` (`name`)
-) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
+  UNIQUE KEY `sid_UNIQUE` (`server_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
 -- ----------------------------
 -- Table structure for canal_instance_config
@@ -39,13 +54,16 @@ CREATE TABLE `canal_config` (
 DROP TABLE IF EXISTS `canal_instance_config`;
 CREATE TABLE `canal_instance_config` (
   `id` bigint(20) NOT NULL AUTO_INCREMENT,
+  `cluster_id` bigint(20) DEFAULT NULL,
+  `server_id` bigint(20) DEFAULT NULL,
   `name` varchar(45) NOT NULL,
   `status` varchar(45) DEFAULT NULL,
   `content` text NOT NULL,
+  `content_md5` varchar(128) DEFAULT NULL,
   `modified_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
   PRIMARY KEY (`id`),
   UNIQUE KEY `name_UNIQUE` (`name`)
-) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
 -- ----------------------------
 -- Table structure for canal_node_server
@@ -53,6 +71,7 @@ CREATE TABLE `canal_instance_config` (
 DROP TABLE IF EXISTS `canal_node_server`;
 CREATE TABLE `canal_node_server` (
   `id` bigint(20) NOT NULL AUTO_INCREMENT,
+  `cluster_id` bigint(20) DEFAULT NULL,
   `name` varchar(63) NOT NULL,
   `ip` varchar(63) NOT NULL,
   `admin_port` int(11) DEFAULT NULL,
@@ -61,7 +80,7 @@ CREATE TABLE `canal_node_server` (
   `status` varchar(45) DEFAULT NULL,
   `modified_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
   PRIMARY KEY (`id`)
-) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
 -- ----------------------------
 -- Table structure for canal_user
@@ -77,7 +96,9 @@ CREATE TABLE `canal_user` (
   `avatar` varchar(255) DEFAULT NULL,
   `creation_date` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
   PRIMARY KEY (`id`)
-) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+SET FOREIGN_KEY_CHECKS = 1;
 
 -- ----------------------------
 -- Records of canal_user

+ 6 - 0
canal-admin/canal-admin-server/src/main/resources/logback.xml

@@ -38,4 +38,10 @@
 		<appender-ref ref="STDOUT"/>
 		<appender-ref ref="CANAL-ROOT" />
 	</root>
+
+	<logger name="io.ebean.SQL" additivity="false">
+		<level value="INFO" />
+		<appender-ref ref="STDOUT"/>
+		<appender-ref ref="CANAL-ROOT" />
+	</logger>
 </configuration>

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
canal-admin/canal-admin-server/src/main/resources/public/index.html


+ 1 - 0
canal-admin/canal-admin-server/src/main/resources/public/static/css/chunk-14b5f7a4.f3e06673.css

@@ -0,0 +1 @@
+.pagination-container[data-v-cebf2f0c]{background:#fff;padding:32px 16px}.pagination-container.hidden[data-v-cebf2f0c]{display:none}

+ 1 - 0
canal-admin/canal-admin-server/src/main/resources/public/static/css/chunk-22553be3.f3e06673.css

@@ -0,0 +1 @@
+.pagination-container[data-v-cebf2f0c]{background:#fff;padding:32px 16px}.pagination-container.hidden[data-v-cebf2f0c]{display:none}

+ 1 - 0
canal-admin/canal-admin-server/src/main/resources/public/static/css/chunk-2301924a.160e7b4a.css

@@ -0,0 +1 @@
+.line[data-v-28f0cd0f]{text-align:center}

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
canal-admin/canal-admin-server/src/main/resources/public/static/css/chunk-49959c8b.e8e2beee.css


+ 1 - 0
canal-admin/canal-admin-server/src/main/resources/public/static/css/chunk-98f505d0.5280f88f.css

@@ -0,0 +1 @@
+.line[data-v-35af5ff9]{text-align:center}

+ 1 - 0
canal-admin/canal-admin-server/src/main/resources/public/static/css/chunk-bd1d44ee.1528199a.css

@@ -0,0 +1 @@
+.line[data-v-5c332416]{text-align:center}

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
canal-admin/canal-admin-server/src/main/resources/public/static/js/app.cae6e777.js


Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
canal-admin/canal-admin-server/src/main/resources/public/static/js/chunk-0dca2f22.a2bc28b8.js


Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
canal-admin/canal-admin-server/src/main/resources/public/static/js/chunk-14b5f7a4.d0531bb5.js


Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
canal-admin/canal-admin-server/src/main/resources/public/static/js/chunk-22553be3.21abed4f.js


Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
canal-admin/canal-admin-server/src/main/resources/public/static/js/chunk-2301924a.b0e97fa6.js


+ 1 - 0
canal-admin/canal-admin-server/src/main/resources/public/static/js/chunk-49959c8b.6d226f70.js

@@ -0,0 +1 @@
+(window["webpackJsonp"]=window["webpackJsonp"]||[]).push([["chunk-49959c8b"],{"26fc":function(t,s,a){t.exports=a.p+"static/img/404_cloud.0f4bc32b.png"},"8cdb":function(t,s,a){"use strict";a.r(s);var e=function(){var t=this,s=t.$createElement,a=t._self._c||s;return a("div",{staticClass:"wscn-http404-container"},[a("div",{staticClass:"wscn-http404"},[t._m(0),t._v(" "),a("div",{staticClass:"bullshit"},[a("div",{staticClass:"bullshit__oops"},[t._v("OOPS!")]),t._v(" "),t._m(1),t._v(" "),a("div",{staticClass:"bullshit__headline"},[t._v(t._s(t.message))]),t._v(" "),a("div",{staticClass:"bullshit__info"},[t._v("Please check that the URL you entered is correct, or click the button below to return to the homepage.")]),t._v(" "),a("a",{staticClass:"bullshit__return-home",attrs:{href:""}},[t._v("Back to home")])])])])},c=[function(){var t=this,s=t.$createElement,e=t._self._c||s;return e("div",{staticClass:"pic-404"},[e("img",{staticClass:"pic-404__parent",attrs:{src:a("a36b"),alt:"404"}}),t._v(" "),e("img",{staticClass:"pic-404__child left",attrs:{src:a("26fc"),alt:"404"}}),t._v(" "),e("img",{staticClass:"pic-404__child mid",attrs:{src:a("26fc"),alt:"404"}}),t._v(" "),e("img",{staticClass:"pic-404__child right",attrs:{src:a("26fc"),alt:"404"}})])},function(){var t=this,s=t.$createElement,a=t._self._c||s;return a("div",{staticClass:"bullshit__info"},[t._v("All rights reserved\n        "),a("a",{staticStyle:{color:"#20a0ff"},attrs:{href:"https://wallstreetcn.com",target:"_blank"}},[t._v("wallstreetcn")])])}],i={name:"Page404",computed:{message:function(){return"The webmaster said that you can not enter this page..."}}},l=i,n=(a("97ef"),a("2877")),r=Object(n["a"])(l,e,c,!1,null,"c095f994",null);s["default"]=r.exports},"97ef":function(t,s,a){"use strict";var e=a("b51e"),c=a.n(e);c.a},a36b:function(t,s,a){t.exports=a.p+"static/img/404.a57b6f31.png"},b51e:function(t,s,a){}}]);

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
canal-admin/canal-admin-server/src/main/resources/public/static/js/chunk-55380ff2.681c71c9.js


Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
canal-admin/canal-admin-server/src/main/resources/public/static/js/chunk-7ec889b7.295b8aad.js


Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
canal-admin/canal-admin-server/src/main/resources/public/static/js/chunk-98f505d0.aade2e3b.js


Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
canal-admin/canal-admin-server/src/main/resources/public/static/js/chunk-bd1d44ee.c2f6b1a9.js


+ 46 - 0
canal-admin/canal-admin-ui/src/api/canalCluster.js

@@ -0,0 +1,46 @@
+import request from '@/utils/request'
+
+export function getCanalClusters(params) {
+  return request({
+    url: '/canal/clusters',
+    method: 'get',
+    params: params
+  })
+}
+
+export function addCanalCluster(data) {
+  return request({
+    url: '/canal/cluster',
+    method: 'post',
+    data
+  })
+}
+
+export function canalClusterDetail(id) {
+  return request({
+    url: '/canal/cluster/' + id,
+    method: 'get'
+  })
+}
+
+export function updateCanalCluster(data) {
+  return request({
+    url: '/canal/cluster',
+    method: 'put',
+    data
+  })
+}
+
+export function deleteCanalCluster(id) {
+  return request({
+    url: '/canal/cluster/' + id,
+    method: 'delete'
+  })
+}
+
+export function getClustersAndServers() {
+  return request({
+    url: '/canal/clustersAndServers',
+    method: 'get'
+  })
+}

+ 2 - 2
canal-admin/canal-admin-ui/src/api/canalConfig.js

@@ -1,8 +1,8 @@
 import request from '@/utils/request'
 
-export function getCanalConfig() {
+export function getCanalConfig(clusterId, serverId) {
   return request({
-    url: '/canal/config',
+    url: '/canal/config/' + clusterId + '/' + serverId,
     method: 'get'
   })
 }

+ 14 - 0
canal-admin/canal-admin-ui/src/api/canalInstance.js

@@ -58,3 +58,17 @@ export function instanceLog(id, nodeId) {
     method: 'get'
   })
 }
+
+export function instanceStatus(id, option) {
+  return request({
+    url: '/canal/instance/status/' + id + '?option=' + option,
+    method: 'put'
+  })
+}
+
+export function getActiveInstances(serverId) {
+  return request({
+    url: '/canal/active/instances/' + serverId,
+    method: 'get'
+  })
+}

+ 100 - 0
canal-admin/canal-admin-ui/src/components/Pagination/index.vue

@@ -0,0 +1,100 @@
+<template>
+  <div :class="{'hidden':hidden}" class="pagination-container">
+    <el-pagination
+      :background="background"
+      :current-page.sync="currentPage"
+      :page-size.sync="pageSize"
+      :layout="layout"
+      :page-sizes="pageSizes"
+      :total="total"
+      v-bind="$attrs"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"/>
+  </div>
+</template>
+
+<script>
+import { scrollTo } from '@/utils/scrollTo'
+
+export default {
+  name: 'Pagination',
+  props: {
+    total: {
+      required: true,
+      type: Number
+    },
+    page: {
+      type: Number,
+      default: 1
+    },
+    limit: {
+      type: Number,
+      default: 20
+    },
+    pageSizes: {
+      type: Array,
+      default() {
+        return [10, 20, 30, 50]
+      }
+    },
+    layout: {
+      type: String,
+      default: 'total, sizes, prev, pager, next, jumper'
+    },
+    background: {
+      type: Boolean,
+      default: true
+    },
+    autoScroll: {
+      type: Boolean,
+      default: true
+    },
+    hidden: {
+      type: Boolean,
+      default: false
+    }
+  },
+  computed: {
+    currentPage: {
+      get() {
+        return this.page
+      },
+      set(val) {
+        this.$emit('update:page', val)
+      }
+    },
+    pageSize: {
+      get() {
+        return this.limit
+      },
+      set(val) {
+        this.$emit('update:limit', val)
+      }
+    }
+  },
+  methods: {
+    handleSizeChange(val) {
+      this.$emit('pagination', { page: this.currentPage, limit: val })
+      if (this.autoScroll) {
+        scrollTo(0, 800)
+      }
+    },
+    handleCurrentChange(val) {
+      this.$emit('pagination', { page: val, limit: this.pageSize })
+      if (this.autoScroll) {
+        scrollTo(0, 800)
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.pagination-container {
+  background: #fff;
+  padding: 32px 16px;
+}
+.pagination-container.hidden {
+  display: none;
+}
+</style>

+ 12 - 5
canal-admin/canal-admin-ui/src/router/index.js

@@ -76,17 +76,24 @@ export const constantRoutes = [
     name: 'Canal Server',
     meta: { title: 'Canal Server', icon: 'example' },
     children: [
+      {
+        path: 'canalClusters',
+        name: 'Canal 集群管理',
+        component: () => import('@/views/canalServer/CanalCluster'),
+        meta: { title: '集群管理', icon: 'tree' }
+      },
       {
         path: 'nodeServers',
         name: 'Server 状态',
         component: () => import('@/views/canalServer/NodeServer'),
-        meta: { title: 'Server 管理', icon: 'tree' }
+        meta: { title: 'Server 管理', icon: 'form' }
       },
       {
-        path: 'config',
-        name: 'Server 全局配置',
-        component: () => import('@/views/canalServer/Config'),
-        meta: { title: 'Server 全局配置', icon: 'form' }
+        path: 'nodeServer/config',
+        name: 'Server 配置',
+        component: () => import('@/views/canalServer/CanalConfig'),
+        meta: { title: 'Server 配置' },
+        hidden: true
       },
       {
         path: 'canalInstances',

+ 50 - 0
canal-admin/canal-admin-ui/src/utils/scrollTo.js

@@ -0,0 +1,50 @@
+Math.easeInOutQuad = function(t, b, c, d) {
+  t /= d / 2
+  if (t < 1) {
+    return c / 2 * t * t + b
+  }
+  t--
+  return -c / 2 * (t * (t - 2) - 1) + b
+}
+
+// requestAnimationFrame for Smart Animating http://goo.gl/sx5sts
+var requestAnimFrame = (function() {
+  return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function(callback) { window.setTimeout(callback, 1000 / 60) }
+})()
+
+// because it's so fucking difficult to detect the scrolling element, just move them all
+function move(amount) {
+  document.documentElement.scrollTop = amount
+  document.body.parentNode.scrollTop = amount
+  document.body.scrollTop = amount
+}
+
+function position() {
+  return document.documentElement.scrollTop || document.body.parentNode.scrollTop || document.body.scrollTop
+}
+
+export function scrollTo(to, duration, callback) {
+  const start = position()
+  const change = to - start
+  const increment = 20
+  let currentTime = 0
+  duration = (typeof (duration) === 'undefined') ? 500 : duration
+  var animateScroll = function() {
+    // increment the time
+    currentTime += increment
+    // find the value with the quadratic in-out easing function
+    var val = Math.easeInOutQuad(currentTime, start, change, duration)
+    // move the document.body
+    move(val)
+    // do the animation unless its over
+    if (currentTime < duration) {
+      requestAnimFrame(animateScroll)
+    } else {
+      if (callback && typeof (callback) === 'function') {
+        // the animation is done so lets callback
+        callback()
+      }
+    }
+  }
+  animateScroll()
+}

+ 204 - 0
canal-admin/canal-admin-ui/src/views/canalServer/CanalCluster.vue

@@ -0,0 +1,204 @@
+<template>
+  <div class="app-container">
+    <div class="filter-container">
+      <!-- <el-input v-model="listQuery.name" placeholder="Server 名称" style="width: 200px;" class="filter-item" />
+      <el-input v-model="listQuery.ip" placeholder="Server IP" style="width: 200px;" class="filter-item" />
+      <el-button class="filter-item" type="primary" icon="el-icon-search" plain @click="fetchData()">查询</el-button> -->
+      <el-button class="filter-item" type="primary" @click="handleCreate()">新建集群</el-button>
+    </div>
+    <el-table
+      v-loading="listLoading"
+      :data="list"
+      element-loading-text="Loading"
+      border
+      fit
+      highlight-current-row
+    >
+      <el-table-column label="集群名称" min-width="200" align="center">
+        <template slot-scope="scope">
+          {{ scope.row.name }}
+        </template>
+      </el-table-column>
+      <el-table-column label="ZK地址" min-width="300" align="center">
+        <template slot-scope="scope">
+          <span>{{ scope.row.zkHosts }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" prop="created_at" label="操作" min-width="150">
+        <template slot-scope="scope">
+          <el-dropdown trigger="click">
+            <el-button type="primary" size="mini">
+              操作<i class="el-icon-arrow-down el-icon--right" />
+            </el-button>
+            <el-dropdown-menu slot="dropdown">
+              <el-dropdown-item @click.native="handleConfig(scope.row)">主配置</el-dropdown-item>
+              <el-dropdown-item @click.native="handleUpdate(scope.row)">修改集群</el-dropdown-item>
+              <el-dropdown-item @click.native="handleDelete(scope.row)">删除集群</el-dropdown-item>
+              <el-dropdown-item @click.native="handleView(scope.row)">查看Server</el-dropdown-item>
+            </el-dropdown-menu>
+          </el-dropdown>
+        </template>
+      </el-table-column>
+    </el-table>
+    <el-dialog :visible.sync="dialogFormVisible" :title="textMap[dialogStatus]" width="600px">
+      <el-form ref="dataForm" :rules="rules" :model="canalCluster" label-position="left" label-width="120px" style="width: 400px; margin-left:30px;">
+        <el-form-item label="集群名称" prop="name">
+          <el-input v-model="canalCluster.name" />
+        </el-form-item>
+        <el-form-item label="ZK地址" prop="zkHosts">
+          <el-input v-model="canalCluster.zkHosts" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="dialogFormVisible = false">取消</el-button>
+        <el-button type="primary" @click="dataOperation()">确定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { addCanalCluster, getCanalClusters, updateCanalCluster, deleteCanalCluster } from '@/api/canalCluster'
+
+export default {
+  filters: {
+    statusFilter(status) {
+      const statusMap = {
+        '1': 'success',
+        '0': 'gray',
+        '-1': 'danger'
+      }
+      return statusMap[status]
+    },
+    statusLabel(status) {
+      const statusMap = {
+        '1': '启动',
+        '0': '停止',
+        '-1': '断开'
+      }
+      return statusMap[status]
+    }
+  },
+  data() {
+    return {
+      list: null,
+      listLoading: true,
+      listQuery: {
+        name: '',
+        ip: ''
+      },
+      dialogFormVisible: false,
+      textMap: {
+        create: '新建集群信息',
+        update: '修改集群信息'
+      },
+      canalCluster: {
+        id: null,
+        name: null,
+        zkHosts: null
+      },
+      rules: {
+        name: [{ required: true, message: '集群名称不能为空', trigger: 'change' }],
+        zkHosts: [{ required: true, message: 'zk地址不能为空', trigger: 'change' }]
+      },
+      dialogStatus: 'create'
+    }
+  },
+  created() {
+    this.fetchData()
+  },
+  methods: {
+    fetchData() {
+      this.listLoading = true
+      getCanalClusters(this.listQuery).then(res => {
+        this.list = res.data
+      }).finally(() => {
+        this.listLoading = false
+      })
+    },
+    resetModel() {
+      this.canalCluster = {
+        id: null,
+        name: null,
+        zkHosts: null
+      }
+    },
+    handleCreate() {
+      this.resetModel()
+      this.dialogStatus = 'create'
+      this.dialogFormVisible = true
+      this.$nextTick(() => {
+        this.$refs['dataForm'].clearValidate()
+      })
+    },
+    dataOperation() {
+      this.$refs['dataForm'].validate((valid) => {
+        if (valid) {
+          if (this.dialogStatus === 'create') {
+            addCanalCluster(this.canalCluster).then(res => {
+              this.operationRes(res)
+            })
+          }
+          if (this.dialogStatus === 'update') {
+            updateCanalCluster(this.canalCluster).then(res => {
+              this.operationRes(res)
+            })
+          }
+        }
+      })
+    },
+    operationRes(res) {
+      if (res.data === 'success') {
+        this.fetchData()
+        this.dialogFormVisible = false
+        this.$message({
+          message: this.textMap[this.dialogStatus] + '成功',
+          type: 'success'
+        })
+      } else {
+        this.$message({
+          message: this.textMap[this.dialogStatus] + '失败',
+          type: 'error'
+        })
+      }
+    },
+    handleView(row) {
+      this.$router.push('/canalServer/nodeServers?clusterId=' + row.id)
+    },
+    handleConfig(row) {
+      this.$router.push('/canalServer/nodeServer/config?clusterId=' + row.id)
+    },
+    handleUpdate(row) {
+      this.resetModel()
+      this.canalCluster = Object.assign({}, row)
+      this.dialogStatus = 'update'
+      this.dialogFormVisible = true
+      this.$nextTick(() => {
+        this.$refs['dataForm'].clearValidate()
+      })
+    },
+    handleDelete(row) {
+      this.$confirm('删除集群信息会导致服务停止', '确定删除集群信息', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        deleteCanalCluster(row.id).then((res) => {
+          if (res.data === 'success') {
+            this.fetchData()
+            this.$message({
+              message: '删除集群信息成功',
+              type: 'success'
+            })
+          } else {
+            this.$message({
+              message: '删除集群信息失败',
+              type: 'error'
+            })
+          }
+        })
+      })
+    }
+  }
+}
+</script>

+ 28 - 6
canal-admin/canal-admin-ui/src/views/canalServer/Config.vue → canal-admin/canal-admin-ui/src/views/canalServer/CanalConfig.vue

@@ -4,8 +4,9 @@
       <div style="padding-left: 10px;padding-top: 20px;">
         <el-form-item>
           {{ form.name }}&nbsp;&nbsp;&nbsp;&nbsp;
-          <el-button type="primary" @click="onSubmit">修改</el-button>
+          <el-button type="primary" @click="onSubmit">保存</el-button>
           <el-button type="warning" @click="onCancel">重置</el-button>
+          <el-button type="info" @click="onBack">返回</el-button>
         </el-form-item>
       </div>
       <editor v-model="form.content" lang="properties" theme="chrome" width="100%" :height="800" @init="editorInit" />
@@ -25,7 +26,9 @@ export default {
       form: {
         id: null,
         name: '',
-        content: ''
+        content: '',
+        serverId: null,
+        clusterId: null
       }
     }
   },
@@ -44,16 +47,32 @@ export default {
       require('brace/snippets/javascript')
     },
     loadCanalConfig() {
-      getCanalConfig().then(response => {
+      let clusterId = 0
+      let serverId = 0
+      if (this.$route.query.clusterId) {
+        clusterId = this.$route.query.clusterId
+      } else if (this.$route.query.serverId) {
+        serverId = this.$route.query.serverId
+      }
+      getCanalConfig(clusterId, serverId).then(response => {
         const data = response.data
         this.form.id = data.id
         this.form.name = data.name
         this.form.content = data.content
+        this.form.serverId = this.$route.query.serverId
+        this.form.clusterId = this.$route.query.clusterId
       })
     },
     onSubmit() {
+      if (this.form.content === null || this.form.content === '') {
+        this.$message({
+          message: '配置内容不能为空',
+          type: 'error'
+        })
+        return
+      }
       this.$confirm(
-        '修改Server主配置可能会导致Server重启,是否继续?',
+        '修改主配置可能会导致Server重启,是否继续?',
         '确定修改',
         {
           confirmButtonText: '确定',
@@ -64,13 +83,13 @@ export default {
         updateCanalConfig(this.form).then(response => {
           if (response.data === 'success') {
             this.$message({
-              message: '修改成功',
+              message: '保存成功',
               type: 'success'
             })
             this.loadCanalConfig()
           } else {
             this.$message({
-              message: '修改失败',
+              message: '保存失败',
               type: 'error'
             })
           }
@@ -79,6 +98,9 @@ export default {
     },
     onCancel() {
       this.loadCanalConfig()
+    },
+    onBack() {
+      history.go(-1)
     }
   }
 }

+ 85 - 65
canal-admin/canal-admin-ui/src/views/canalServer/CanalInstance.vue

@@ -2,7 +2,13 @@
   <div class="app-container">
     <div class="filter-container">
       <el-input v-model="listQuery.name" placeholder="Instance 名称" style="width: 200px;" class="filter-item" />
-      <el-button class="filter-item" type="primary" icon="el-icon-search" plain @click="fetchData()">查询</el-button>
+      <el-select v-model="listQuery.clusterServerId" placeholder="所属集群/主机" class="filter-item">
+        <el-option key="" label="所属集群/主机" value="" />
+        <el-option-group v-for="group in options" :key="group.label" :label="group.label">
+          <el-option v-for="item in group.options" :key="item.value" :label="item.label" :value="item.value" />
+        </el-option-group>
+      </el-select>
+      <el-button class="filter-item" type="primary" icon="el-icon-search" plain @click="queryData()">查询</el-button>
       &nbsp;&nbsp;
       <el-button class="filter-item" type="primary" @click="handleCreate()">新建 Instance</el-button>
       <el-button class="filter-item" type="info" @click="fetchData()">刷新列表</el-button>
@@ -20,9 +26,25 @@
           {{ scope.row.name }}
         </template>
       </el-table-column>
-      <el-table-column label="运行Server" min-width="200" align="center">
+      <el-table-column label="所属集群" min-width="200" align="center">
         <template slot-scope="scope">
-          <span>{{ scope.row.nodeIp }}</span>
+          <span v-if="scope.row.canalCluster !== null">
+            {{ scope.row.canalCluster.name }}
+          </span>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="所属主机" min-width="200" align="center">
+        <template slot-scope="scope">
+          <span v-if="scope.row.nodeServer !== null">
+            {{ scope.row.nodeServer.name }}
+          </span>
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column class-name="status-col" label="状态" min-width="150" align="center">
+        <template slot-scope="scope">
+          <el-tag :type="scope.row.runningStatus | statusFilter">{{ scope.row.runningStatus | statusLabel }}</el-tag>
         </template>
       </el-table-column>
       <el-table-column label="修改时间" min-width="200" align="center">
@@ -47,33 +69,29 @@
         </template>
       </el-table-column>
     </el-table>
-    <el-dialog :visible.sync="dialogFormVisible" title="确定启动Instance" width="500px">
-      <el-form ref="dataForm" :rules="rules" :model="nodeModel" label-position="left" label-width="120px" style="width: 350px; margin-left:30px;">
-        <el-form-item label="选择运行Server" prop="nodeId">
-          <el-select v-model="nodeModel.id" placeholder="选择运行Server">
-            <el-option v-for="item in nodeServices" :key="item.id" :label="item.name" :value="item.id" />
-          </el-select>
-        </el-form-item>
-      </el-form>
-      <div slot="footer" class="dialog-footer">
-        <el-button @click="dialogFormVisible = false">取消</el-button>
-        <el-button type="primary" @click="doStartInstance()">确定</el-button>
-      </div>
-    </el-dialog>
+    <pagination v-show="count>0" :total="count" :page.sync="listQuery.page" :limit.sync="listQuery.size" @pagination="fetchData()" />
   </div>
 </template>
 
 <script>
-import { getCanalInstances, deleteCanalInstance, startInstance, stopInstance } from '@/api/canalInstance'
-import { getNodeServers } from '@/api/nodeServer'
+import { getCanalInstances, deleteCanalInstance, instanceStatus } from '@/api/canalInstance'
+import Pagination from '@/components/Pagination'
+import { getClustersAndServers } from '@/api/canalCluster'
 
 export default {
+  components: { Pagination },
   filters: {
     statusFilter(status) {
       const statusMap = {
-        published: 'success',
-        draft: 'gray',
-        deleted: 'danger'
+        '1': 'success',
+        '0': 'gray'
+      }
+      return statusMap[status]
+    },
+    statusLabel(status) {
+      const statusMap = {
+        '1': '启动',
+        '0': '停止'
       }
       return statusMap[status]
     }
@@ -84,26 +102,37 @@ export default {
       listLoading: true,
       dialogFormVisible: false,
       nodeServices: [],
+      count: 0,
+      options: [],
       listQuery: {
-        name: ''
+        name: '',
+        clusterServerId: '',
+        page: 1,
+        size: 20
       },
       currentId: null,
-      nodeModel: {
-        id: null
-      },
       rules: {
         id: [{ required: true, message: '请选择运行Server', trigger: 'change' }]
       }
     }
   },
   created() {
+    getClustersAndServers().then((res) => {
+      this.options = res.data
+    })
     this.fetchData()
   },
   methods: {
+    queryData() {
+      this.listQuery.page = 1
+      this.fetchData()
+    },
     fetchData() {
       this.listLoading = true
       getCanalInstances(this.listQuery).then(res => {
-        this.list = res.data
+        this.list = res.data.items
+        this.count = res.data.count
+      }).finally(() => {
         this.listLoading = false
       })
     },
@@ -136,55 +165,46 @@ export default {
       })
     },
     handleStart(row) {
-      if (row.nodeId !== null) {
-        this.$message({ message: '当前Instance已处于启动状态!', type: 'error' })
-        return
-      }
-
-      this.currentId = row.id
-      this.nodeModel.id = null
-
-      this.$nextTick(() => {
-        this.$refs['dataForm'].clearValidate()
-      })
-
-      getNodeServers().then((res) => {
-        this.nodeServices = res.data
-        this.dialogFormVisible = true
-      })
-    },
-    doStartInstance() {
-      startInstance(this.currentId, this.nodeModel.id).then((res) => {
-        if (res.data) {
-          this.fetchData()
-          this.$message({
-            message: '启动成功',
-            type: 'success'
-          })
-          this.dialogFormVisible = false
-        } else {
-          this.$message({
-            message: '启动Instance出现异常',
-            type: 'error'
-          })
-        }
+      // if (row.runningStatus === '1') {
+      //   this.$message({ message: '当前Instance已处于启动状态!', type: 'error' })
+      //   return
+      // }
+      this.$confirm('启动Instance: ' + row.name, '确定启动Instance服务', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        instanceStatus(row.id, 'start').then((res) => {
+          if (res.data) {
+            this.fetchData()
+            this.$message({
+              message: '启动成功, 稍后请刷新列表查看状态',
+              type: 'success'
+            })
+          } else {
+            this.$message({
+              message: '启动Instance出现异常',
+              type: 'error'
+            })
+          }
+        })
       })
     },
     handleStop(row) {
-      if (row.nodeId === null) {
-        this.$message({ message: '当前Instance已处于停止状态!', type: 'error' })
-        return
-      }
+      // if (row.runningStatus === '0') {
+      //   this.$message({ message: '当前Instance已处于停止状态!', type: 'error' })
+      //   return
+      // }
       this.$confirm('停止Instance: ' + row.name, '确定停止Instance服务', {
         confirmButtonText: '确定',
         cancelButtonText: '取消',
         type: 'warning'
       }).then(() => {
-        stopInstance(row.id, row.nodeId).then((res) => {
+        instanceStatus(row.id, 'stop').then((res) => {
           if (res.data) {
             this.fetchData()
             this.$message({
-              message: '停止成功',
+              message: '停止成功, 稍后请刷新列表查看状态',
               type: 'success'
             })
           } else {
@@ -201,7 +221,7 @@ export default {
         this.$message({ message: '当前Instance不是启动状态,无法查看日志', type: 'warning' })
         return
       }
-      this.$router.push('canalInstance/log?id=' + row.id + '&nodeId=' + row.nodeId)
+      this.$router.push('canalInstance/log?id=' + row.id + '&nodeId=' + row.nodeServer.id)
     }
   }
 }

+ 26 - 2
canal-admin/canal-admin-ui/src/views/canalServer/CanalInstanceAdd.vue

@@ -3,7 +3,11 @@
     <el-form ref="form" :model="form">
       <div class="filter-container" style="padding-left: 10px;padding-top: 20px;">
         <el-input v-model="form.name" placeholder="Instance名称" style="width: 200px;" class="filter-item" />
-        &nbsp;
+        <el-select v-model="form.clusterServerId" placeholder="所属集群/主机" class="filter-item">
+          <el-option-group v-for="group in options" :key="group.label" :label="group.label">
+            <el-option v-for="item in group.options" :key="item.value" :label="item.label" :value="item.value" />
+          </el-option-group>
+        </el-select>
         <el-button class="filter-item" type="primary" @click="onSubmit">保存</el-button>
         <el-button class="filter-item" type="info" @click="onBack">返回</el-button>
       </div>
@@ -14,6 +18,7 @@
 
 <script>
 import { addCanalInstance } from '@/api/canalInstance'
+import { getClustersAndServers } from '@/api/canalCluster'
 
 export default {
   components: {
@@ -21,13 +26,18 @@ export default {
   },
   data() {
     return {
+      options: [],
       form: {
         name: '',
-        content: ''
+        content: '',
+        clusterServerId: ''
       }
     }
   },
   created() {
+    getClustersAndServers().then((res) => {
+      this.options = res.data
+    })
   },
   methods: {
     editorInit() {
@@ -48,6 +58,20 @@ export default {
         })
         return
       }
+      if (this.form.clusterServerId === '') {
+        this.$message({
+          message: '请选择所属集群/主机',
+          type: 'error'
+        })
+        return
+      }
+      if (this.form.content === null || this.form.content === '') {
+        this.$message({
+          message: '请输入配置内容',
+          type: 'error'
+        })
+        return
+      }
       this.$confirm(
         '确定新建',
         '确定新建',

+ 13 - 1
canal-admin/canal-admin-ui/src/views/canalServer/CanalInstanceUpdate.vue

@@ -4,6 +4,11 @@
       <div style="padding-left: 10px;padding-top: 20px;">
         <el-form-item>
           {{ form.name }}&nbsp;&nbsp;&nbsp;&nbsp;
+          <el-select v-model="form.clusterServerId" placeholder="所属集群/主机" class="filter-item">
+            <el-option-group v-for="group in options" :key="group.label" :label="group.label">
+              <el-option v-for="item in group.options" :key="item.value" :label="item.label" :value="item.value" />
+            </el-option-group>
+          </el-select>
           <el-button type="primary" @click="onSubmit">修改</el-button>
           <el-button type="warning" @click="onCancel">重置</el-button>
           <el-button type="info" @click="onBack">返回</el-button>
@@ -16,6 +21,7 @@
 
 <script>
 import { canalInstanceDetail, updateCanalInstance } from '@/api/canalInstance'
+import { getClustersAndServers } from '@/api/canalCluster'
 
 export default {
   components: {
@@ -23,15 +29,20 @@ export default {
   },
   data() {
     return {
+      options: [],
       form: {
         id: null,
         name: '',
-        content: ''
+        content: '',
+        clusterServerId: ''
       }
     }
   },
   created() {
     this.loadCanalConfig()
+    getClustersAndServers().then((res) => {
+      this.options = res.data
+    })
   },
   methods: {
     editorInit() {
@@ -50,6 +61,7 @@ export default {
         this.form.id = data.id
         this.form.name = data.name + '/instance.propertios'
         this.form.content = data.content
+        this.form.clusterServerId = data.clusterServerId
       })
     },
     onSubmit() {

+ 170 - 6
canal-admin/canal-admin-ui/src/views/canalServer/NodeServer.vue

@@ -1,9 +1,14 @@
 <template>
   <div class="app-container">
     <div class="filter-container">
-      <!-- <el-input v-model="listQuery.name" placeholder="Server 名称" style="width: 200px;" class="filter-item" />
+      <!--<el-input v-model="listQuery.name" placeholder="Server 名称" style="width: 200px;" class="filter-item" />-->
+      <el-select v-model="listQuery.clusterId" placeholder="所属集群" class="filter-item">
+        <el-option key="" label="所属集群" value="" />
+        <el-option key="-1" label="单机" value="-1" />
+        <el-option v-for="item in canalClusters" :key="item.id" :label="item.name" :value="item.id" />
+      </el-select>
       <el-input v-model="listQuery.ip" placeholder="Server IP" style="width: 200px;" class="filter-item" />
-      <el-button class="filter-item" type="primary" icon="el-icon-search" plain @click="fetchData()">查询</el-button> -->
+      <el-button class="filter-item" type="primary" icon="el-icon-search" plain @click="queryData()">查询</el-button>
       <el-button class="filter-item" type="primary" @click="handleCreate()">新建Server</el-button>
       <el-button class="filter-item" type="info" @click="fetchData()">刷新列表</el-button>
     </div>
@@ -15,6 +20,16 @@
       fit
       highlight-current-row
     >
+      <el-table-column label="所属集群" min-width="200" align="center">
+        <template slot-scope="scope">
+          <span v-if="scope.row.canalCluster !== null">
+            {{ scope.row.canalCluster.name }}
+          </span>
+          <span v-else>
+            -
+          </span>
+        </template>
+      </el-table-column>
       <el-table-column label="Server 名称" min-width="200" align="center">
         <template slot-scope="scope">
           {{ scope.row.name }}
@@ -52,18 +67,31 @@
               操作<i class="el-icon-arrow-down el-icon--right" />
             </el-button>
             <el-dropdown-menu slot="dropdown">
+              <el-dropdown-item @click.native="handleConfig(scope.row)">配置</el-dropdown-item>
               <el-dropdown-item @click.native="handleUpdate(scope.row)">修改</el-dropdown-item>
               <el-dropdown-item @click.native="handleDelete(scope.row)">删除</el-dropdown-item>
               <el-dropdown-item @click.native="handleStart(scope.row)">启动</el-dropdown-item>
               <el-dropdown-item @click.native="handleStop(scope.row)">停止</el-dropdown-item>
+              <el-dropdown-item @click.native="handleInstances(scope.row)">实例</el-dropdown-item>
               <el-dropdown-item @click.native="handleLog(scope.row)">日志</el-dropdown-item>
             </el-dropdown-menu>
           </el-dropdown>
         </template>
       </el-table-column>
     </el-table>
+    <pagination v-show="count>0" :total="count" :page.sync="listQuery.page" :limit.sync="listQuery.size" @pagination="fetchData()" />
     <el-dialog :visible.sync="dialogFormVisible" :title="textMap[dialogStatus]" width="600px">
       <el-form ref="dataForm" :rules="rules" :model="nodeModel" label-position="left" label-width="120px" style="width: 400px; margin-left:30px;">
+        <el-form-item label="所属集群" prop="clusterId">
+          <el-select v-if="dialogStatus === 'create'" v-model="nodeModel.clusterId" placeholder="选择所属集群">
+            <el-option key="" label="单机" value="" />
+            <el-option v-for="item in canalClusters" :key="item.id" :label="item.name" :value="item.id" />
+          </el-select>
+          <el-select v-else v-model="nodeModel.clusterId" placeholder="选择所属集群" disabled="disabled">
+            <el-option key="" label="单机" value="" />
+            <el-option v-for="item in canalClusters" :key="item.id" :label="item.name" :value="item.id" />
+          </el-select>
+        </el-form-item>
         <el-form-item label="Server 名称" prop="name">
           <el-input v-model="nodeModel.name" />
         </el-form-item>
@@ -85,13 +113,54 @@
         <el-button type="primary" @click="dataOperation()">确定</el-button>
       </div>
     </el-dialog>
+    <el-dialog :visible.sync="dialogInstances" title="实例列表" width="800px">
+      <div class="filter-container">
+        <el-button class="filter-item" type="info" @click="activeInstances()">刷新列表</el-button>
+      </div>
+      <el-table
+        v-loading="listLoading2"
+        :data="instanceList"
+        element-loading-text="Loading"
+        border
+        fit
+        highlight-current-row
+      >
+        <el-table-column label="Instance 名称" min-width="200" align="center">
+          <template slot-scope="scope">
+            {{ scope.row.name }}
+          </template>
+        </el-table-column>
+        <el-table-column label="状态" min-width="200" align="center">
+          <template slot-scope="scope">
+            <el-tag :type="scope.row.runningStatus | statusFilter">{{ scope.row.runningStatus | statusLabel }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" min-width="200" align="center">
+          <template slot-scope="scope">
+            <el-dropdown trigger="click">
+              <el-button type="primary" size="mini">
+                操作<i class="el-icon-arrow-down el-icon--right" />
+              </el-button>
+              <el-dropdown-menu slot="dropdown">
+                <el-dropdown-item @click.native="handleStartInstance(scope.row)">启动</el-dropdown-item>
+                <el-dropdown-item @click.native="handleStopInstance(scope.row)">停止</el-dropdown-item>
+              </el-dropdown-menu>
+            </el-dropdown>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-dialog>
   </div>
 </template>
 
 <script>
 import { addNodeServer, getNodeServers, updateNodeServer, deleteNodeServer, startNodeServer, stopNodeServer } from '@/api/nodeServer'
+import { getActiveInstances, stopInstance, startInstance } from '@/api/canalInstance'
+import { getCanalClusters } from '@/api/canalCluster'
+import Pagination from '@/components/Pagination'
 
 export default {
+  components: { Pagination },
   filters: {
     statusFilter(status) {
       const statusMap = {
@@ -113,18 +182,28 @@ export default {
   data() {
     return {
       list: null,
+      instanceList: null,
       listLoading: true,
+      listLoading2: true,
+      serverIdTmp: null,
+      canalClusters: [],
+      count: 0,
       listQuery: {
         name: '',
-        ip: ''
+        ip: '',
+        clusterId: null,
+        page: 1,
+        size: 20
       },
       dialogFormVisible: false,
+      dialogInstances: false,
       textMap: {
         create: '新建Server信息',
         update: '修改Server信息'
       },
       nodeModel: {
         id: undefined,
+        clusterId: null,
         name: null,
         ip: null,
         adminPort: 11110,
@@ -134,26 +213,43 @@ export default {
       rules: {
         name: [{ required: true, message: 'Server 名称不能为空', trigger: 'change' }],
         ip: [{ required: true, message: 'Server IP不能为空', trigger: 'change' }],
-        port: [{ required: true, message: 'Server admin端口不能为空', trigger: 'change' }]
+        adminPort: [{ required: true, message: 'Server admin端口不能为空', trigger: 'change' }]
       },
       dialogStatus: 'create'
     }
   },
   // { min: 2, max: 5, message: '长度在 2 到 5 个字符', trigger: 'change' }
   created() {
+    getCanalClusters().then((res) => {
+      this.canalClusters = res.data
+    })
+    if (this.$route.query.clusterId) {
+      try {
+        this.listQuery.clusterId = Number(this.$route.query.clusterId)
+      } catch (e) {
+        console.log(e)
+      }
+    }
     this.fetchData()
   },
   methods: {
     fetchData() {
       this.listLoading = true
       getNodeServers(this.listQuery).then(res => {
-        this.list = res.data
+        this.list = res.data.items
+        this.count = res.data.count
+      }).finally(() => {
         this.listLoading = false
       })
     },
+    queryData() {
+      this.listQuery.page = 1
+      this.fetchData()
+    },
     resetModel() {
       this.nodeModel = {
         id: undefined,
+        clusterId: null,
         name: null,
         ip: null,
         adminPort: null,
@@ -169,6 +265,19 @@ export default {
         this.$refs['dataForm'].clearValidate()
       })
     },
+    handleInstances(row) {
+      this.serverIdTmp = row.id
+      this.activeInstances()
+    },
+    activeInstances() {
+      this.listLoading2 = true
+      this.dialogInstances = true
+      getActiveInstances(this.serverIdTmp).then(res => {
+        this.instanceList = res.data
+      }).finally(() => {
+        this.listLoading2 = false
+      })
+    },
     dataOperation() {
       this.$refs['dataForm'].validate((valid) => {
         if (valid) {
@@ -200,6 +309,9 @@ export default {
         })
       }
     },
+    handleConfig(row) {
+      this.$router.push('/canalServer/nodeServer/config?serverId=' + row.id)
+    },
     handleUpdate(row) {
       this.resetModel()
       this.nodeModel = Object.assign({}, row)
@@ -210,7 +322,7 @@ export default {
       })
     },
     handleDelete(row) {
-      this.$confirm('删除Server信息并不会导致节点服务停止', '确定删除Server信息', {
+      this.$confirm('删除Server信息会导致节点服务停止', '确定删除Server信息', {
         confirmButtonText: '确定',
         cancelButtonText: '取消',
         type: 'warning'
@@ -285,6 +397,58 @@ export default {
     },
     handleLog(row) {
       this.$router.push('nodeServer/log?id=' + row.id)
+    },
+    handleStartInstance(row) {
+      if (row.runningStatus !== '0') {
+        this.$message({ message: '当前Instance不是停止状态,无法启动', type: 'error' })
+        return
+      }
+      this.$confirm('启动Instance服务', '确定启动Instance服务', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        startInstance(row.id, this.serverIdTmp).then((res) => {
+          if (res.data) {
+            this.activeInstances()
+            this.$message({
+              message: '启动成功, 稍后请刷新列表查看状态',
+              type: 'success'
+            })
+          } else {
+            this.$message({
+              message: '启动Instance服务出现异常',
+              type: 'error'
+            })
+          }
+        })
+      })
+    },
+    handleStopInstance(row) {
+      if (row.runningStatus !== '1') {
+        this.$message({ message: '当前Instance不是运行状态,无法停止', type: 'error' })
+        return
+      }
+      this.$confirm('集群模式下停止实例其它主机将会抢占执行该实例', '停止 Instance 服务', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(() => {
+        stopInstance(row.id, this.serverIdTmp).then((res) => {
+          if (res.data) {
+            this.activeInstances()
+            this.$message({
+              message: '停止成功, 稍后请刷新列表查看状态',
+              type: 'success'
+            })
+          } else {
+            this.$message({
+              message: '停止Instance服务出现异常',
+              type: 'error'
+            })
+          }
+        })
+      })
     }
   }
 }

+ 73 - 70
deployer/src/main/java/com/alibaba/otter/canal/deployer/CanalController.java

@@ -154,86 +154,87 @@ public class CanalController {
 
         final ServerRunningData serverData = new ServerRunningData(cid, registerIp + ":" + port);
         ServerRunningMonitors.setServerData(serverData);
-        ServerRunningMonitors.setRunningMonitors(MigrateMap.makeComputingMap(new Function<String, ServerRunningMonitor>() {
-
-            public ServerRunningMonitor apply(final String destination) {
-                ServerRunningMonitor runningMonitor = new ServerRunningMonitor(serverData);
-                runningMonitor.setDestination(destination);
-                runningMonitor.setListener(new ServerRunningListener() {
-
-                    public void processActiveEnter() {
-                        try {
-                            MDC.put(CanalConstants.MDC_DESTINATION, String.valueOf(destination));
-                            embededCanalServer.start(destination);
-                            if (canalMQStarter != null) {
-                                canalMQStarter.startDestination(destination);
+        ServerRunningMonitors
+            .setRunningMonitors(MigrateMap.makeComputingMap(new Function<String, ServerRunningMonitor>() {
+
+                public ServerRunningMonitor apply(final String destination) {
+                    ServerRunningMonitor runningMonitor = new ServerRunningMonitor(serverData);
+                    runningMonitor.setDestination(destination);
+                    runningMonitor.setListener(new ServerRunningListener() {
+
+                        public void processActiveEnter() {
+                            try {
+                                MDC.put(CanalConstants.MDC_DESTINATION, String.valueOf(destination));
+                                embededCanalServer.start(destination);
+                                if (canalMQStarter != null) {
+                                    canalMQStarter.startDestination(destination);
+                                }
+                            } finally {
+                                MDC.remove(CanalConstants.MDC_DESTINATION);
                             }
-                        } finally {
-                            MDC.remove(CanalConstants.MDC_DESTINATION);
                         }
-                    }
 
-                    public void processActiveExit() {
-                        try {
-                            MDC.put(CanalConstants.MDC_DESTINATION, String.valueOf(destination));
-                            if (canalMQStarter != null) {
-                                canalMQStarter.stopDestination(destination);
+                        public void processActiveExit() {
+                            try {
+                                MDC.put(CanalConstants.MDC_DESTINATION, String.valueOf(destination));
+                                if (canalMQStarter != null) {
+                                    canalMQStarter.stopDestination(destination);
+                                }
+                                embededCanalServer.stop(destination);
+                            } finally {
+                                MDC.remove(CanalConstants.MDC_DESTINATION);
                             }
-                            embededCanalServer.stop(destination);
-                        } finally {
-                            MDC.remove(CanalConstants.MDC_DESTINATION);
                         }
-                    }
-
-                    public void processStart() {
-                        try {
-                            if (zkclientx != null) {
-                                final String path = ZookeeperPathUtils.getDestinationClusterNode(destination,
-                                    registerIp + ":" + port);
-                                initCid(path);
-                                zkclientx.subscribeStateChanges(new IZkStateListener() {
-
-                                    public void handleStateChanged(KeeperState state) throws Exception {
-
-                                    }
 
-                                    public void handleNewSession() throws Exception {
-                                        initCid(path);
-                                    }
-
-                                    @Override
-                                    public void handleSessionEstablishmentError(Throwable error) throws Exception {
-                                        logger.error("failed to connect to zookeeper", error);
-                                    }
-                                });
+                        public void processStart() {
+                            try {
+                                if (zkclientx != null) {
+                                    final String path = ZookeeperPathUtils.getDestinationClusterNode(destination,
+                                        registerIp + ":" + port);
+                                    initCid(path);
+                                    zkclientx.subscribeStateChanges(new IZkStateListener() {
+
+                                        public void handleStateChanged(KeeperState state) throws Exception {
+
+                                        }
+
+                                        public void handleNewSession() throws Exception {
+                                            initCid(path);
+                                        }
+
+                                        @Override
+                                        public void handleSessionEstablishmentError(Throwable error) throws Exception {
+                                            logger.error("failed to connect to zookeeper", error);
+                                        }
+                                    });
+                                }
+                            } finally {
+                                MDC.remove(CanalConstants.MDC_DESTINATION);
                             }
-                        } finally {
-                            MDC.remove(CanalConstants.MDC_DESTINATION);
                         }
-                    }
 
-                    public void processStop() {
-                        try {
-                            MDC.put(CanalConstants.MDC_DESTINATION, String.valueOf(destination));
-                            if (zkclientx != null) {
-                                final String path = ZookeeperPathUtils.getDestinationClusterNode(destination,
-                                    registerIp + ":" + port);
-                                releaseCid(path);
+                        public void processStop() {
+                            try {
+                                MDC.put(CanalConstants.MDC_DESTINATION, String.valueOf(destination));
+                                if (zkclientx != null) {
+                                    final String path = ZookeeperPathUtils.getDestinationClusterNode(destination,
+                                        registerIp + ":" + port);
+                                    releaseCid(path);
+                                }
+                            } finally {
+                                MDC.remove(CanalConstants.MDC_DESTINATION);
                             }
-                        } finally {
-                            MDC.remove(CanalConstants.MDC_DESTINATION);
                         }
-                    }
 
-                });
-                if (zkclientx != null) {
-                    runningMonitor.setZkClient(zkclientx);
+                    });
+                    if (zkclientx != null) {
+                        runningMonitor.setZkClient(zkclientx);
+                    }
+                    // 触发创建一下cid节点
+                    runningMonitor.init();
+                    return runningMonitor;
                 }
-                // 触发创建一下cid节点
-                runningMonitor.init();
-                return runningMonitor;
-            }
-        }));
+            }));
 
         // 初始化monitor机制
         autoScan = BooleanUtils.toBoolean(getProperty(properties, CanalConstants.CANAL_AUTO_SCAN));
@@ -285,7 +286,8 @@ public class CanalController {
             instanceConfigMonitors = MigrateMap.makeComputingMap(new Function<InstanceMode, InstanceConfigMonitor>() {
 
                 public InstanceConfigMonitor apply(InstanceMode mode) {
-                    int scanInterval = Integer.valueOf(getProperty(properties, CanalConstants.CANAL_AUTO_SCAN_INTERVAL));
+                    int scanInterval = Integer
+                        .valueOf(getProperty(properties, CanalConstants.CANAL_AUTO_SCAN_INTERVAL));
 
                     if (mode.isSpring()) {
                         SpringInstanceConfigMonitor monitor = new SpringInstanceConfigMonitor();
@@ -381,7 +383,7 @@ public class CanalController {
     }
 
     private PlainCanalConfigClient getManagerClient(String managerAddress) {
-        return new PlainCanalConfigClient(managerAddress, this.adminUser, this.adminPasswd);
+        return new PlainCanalConfigClient(managerAddress, this.adminUser, this.adminPasswd, null, adminPort);
     }
 
     private void initInstanceConfig(Properties properties) {
@@ -393,7 +395,8 @@ public class CanalController {
             InstanceConfig oldConfig = instanceConfigs.put(destination, config);
 
             if (oldConfig != null) {
-                logger.warn("destination:{} old config:{} has replace by new config:{}", destination, oldConfig, config);
+                logger
+                    .warn("destination:{} old config:{} has replace by new config:{}", destination, oldConfig, config);
             }
         }
     }

+ 12 - 4
deployer/src/main/java/com/alibaba/otter/canal/deployer/CanalLauncher.java

@@ -27,7 +27,7 @@ public class CanalLauncher {
     private static final Logger             logger               = LoggerFactory.getLogger(CanalLauncher.class);
     public static final CountDownLatch      runningLatch         = new CountDownLatch(1);
     private static ScheduledExecutorService executor             = Executors.newScheduledThreadPool(1,
-                                                                     new NamedThreadFactory("canal-server-scan"));
+        new NamedThreadFactory("canal-server-scan"));
 
     public static void main(String[] args) {
         try {
@@ -49,11 +49,19 @@ public class CanalLauncher {
             if (StringUtils.isNotEmpty(managerAddress)) {
                 String user = properties.getProperty(CanalConstants.CANAL_ADMIN_USER);
                 String passwd = properties.getProperty(CanalConstants.CANAL_ADMIN_PASSWD);
-                final PlainCanalConfigClient configClient = new PlainCanalConfigClient(managerAddress, user, passwd);
+                String adminPort = properties.getProperty(CanalConstants.CANAL_ADMIN_PORT);
+                if (StringUtils.isEmpty(adminPort)) {
+                    adminPort = "11110";
+                }
+                final PlainCanalConfigClient configClient = new PlainCanalConfigClient(managerAddress,
+                    user,
+                    passwd,
+                    "",
+                    Integer.parseInt(adminPort));
                 PlainCanal canalConfig = configClient.findServer(null);
                 properties = canalConfig.getProperties();
-                int scanIntervalInSecond = Integer.valueOf(properties.getProperty(CanalConstants.CANAL_AUTO_SCAN_INTERVAL,
-                    "5"));
+                int scanIntervalInSecond = Integer
+                    .valueOf(properties.getProperty(CanalConstants.CANAL_AUTO_SCAN_INTERVAL, "5"));
                 executor.scheduleWithFixedDelay(new Runnable() {
 
                     private PlainCanal lastCanalConfig;

+ 11 - 9
deployer/src/main/java/com/alibaba/otter/canal/deployer/monitor/ManagerInstanceConfigMonitor.java

@@ -22,24 +22,26 @@ import com.google.common.collect.MigrateMap;
 
 /**
  * 基于manager配置的实现
- * 
+ *
  * @author agapple 2019年8月26日 下午10:00:20
  * @since 1.1.4
  */
 public class ManagerInstanceConfigMonitor extends AbstractCanalLifeCycle implements InstanceConfigMonitor, CanalLifeCycle {
 
-    private static final Logger         logger               = LoggerFactory.getLogger(ManagerInstanceConfigMonitor.class);
+    private static final Logger         logger               = LoggerFactory
+        .getLogger(ManagerInstanceConfigMonitor.class);
     private long                        scanIntervalInSecond = 5;
     private InstanceAction              defaultAction        = null;
     private Map<String, InstanceAction> actions              = new MapMaker().makeMap();
-    private Map<String, PlainCanal>     configs              = MigrateMap.makeComputingMap(new Function<String, PlainCanal>() {
+    private Map<String, PlainCanal>     configs              = MigrateMap
+        .makeComputingMap(new Function<String, PlainCanal>() {
 
-                                                                 public PlainCanal apply(String destination) {
-                                                                     return new PlainCanal();
-                                                                 }
-                                                             });
+                                                                     public PlainCanal apply(String destination) {
+                                                                         return new PlainCanal();
+                                                                     }
+                                                                 });
     private ScheduledExecutorService    executor             = Executors.newScheduledThreadPool(1,
-                                                                 new NamedThreadFactory("canal-instance-scan"));
+        new NamedThreadFactory("canal-instance-scan"));
 
     private volatile boolean            isFirst              = true;
     private PlainCanalConfigClient      configClient;
@@ -84,7 +86,7 @@ public class ManagerInstanceConfigMonitor extends AbstractCanalLifeCycle impleme
     }
 
     private void scan() {
-        String instances = configClient.findInstances(ip, String.valueOf(port), lastInstanceMD5);
+        String instances = configClient.findInstances(lastInstanceMD5);
         final List<String> is = Lists.newArrayList(StringUtils.split(instances, ','));
         List<String> start = Lists.newArrayList();
         List<String> stop = Lists.newArrayList();

+ 20 - 5
instance/manager/src/main/java/com/alibaba/otter/canal/instance/manager/plain/PlainCanalConfigClient.java

@@ -2,6 +2,8 @@ package com.alibaba.otter.canal.instance.manager.plain;
 
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
 import java.nio.charset.StandardCharsets;
 import java.security.NoSuchAlgorithmException;
 import java.util.HashMap;
@@ -19,7 +21,7 @@ import com.alibaba.otter.canal.protocol.SecurityUtil;
 
 /**
  * 远程配置获取
- * 
+ *
  * @author rewerma 2019-01-25 下午05:20:16
  * @author agapple 2019年8月26日 下午7:52:06
  * @since 1.1.4
@@ -31,8 +33,10 @@ public class PlainCanalConfigClient extends AbstractCanalLifeCycle implements Ca
     private String               user;
     private String               passwd;
     private HttpHelper           httpHelper;
+    private String               localIp;
+    private int                  adminPort;
 
-    public PlainCanalConfigClient(String configURL, String user, String passwd){
+    public PlainCanalConfigClient(String configURL, String user, String passwd, String localIp, int adminPort){
         this.configURL = configURL;
         if (!StringUtils.startsWithIgnoreCase(configURL, "http")) {
             this.configURL = "http://" + configURL;
@@ -42,6 +46,16 @@ public class PlainCanalConfigClient extends AbstractCanalLifeCycle implements Ca
         this.user = user;
         this.passwd = passwd;
         this.httpHelper = new HttpHelper();
+        if (StringUtils.isNotEmpty(localIp)) {
+            this.localIp = localIp;
+        } else {
+            try {
+                this.localIp = InetAddress.getLocalHost().getHostAddress();
+            } catch (UnknownHostException e) {
+                e.printStackTrace();
+            }
+        }
+        this.adminPort = adminPort;
     }
 
     /**
@@ -53,7 +67,7 @@ public class PlainCanalConfigClient extends AbstractCanalLifeCycle implements Ca
         if (StringUtils.isEmpty(md5)) {
             md5 = "";
         }
-        String url = configURL + "/api/v1/config/server_polling?md5=" + md5;
+        String url = configURL + "/api/v1/config/server_polling?ip=" + localIp + "&port=" + adminPort + "&md5=" + md5;
         return queryConfig(url);
     }
 
@@ -71,11 +85,12 @@ public class PlainCanalConfigClient extends AbstractCanalLifeCycle implements Ca
     /**
      * 返回需要运行的instance列表
      */
-    public String findInstances(String ip, String port, String md5) {
+    public String findInstances(String md5) {
         if (StringUtils.isEmpty(md5)) {
             md5 = "";
         }
-        String url = configURL + "/api/v1/config/instances_polling?md5=" + md5 + "&ip=" + ip + "&port=" + port;
+        String url = configURL + "/api/v1/config/instances_polling?md5=" + md5 + "&ip=" + localIp + "&port="
+                     + adminPort;
         ResponseModel<CanalConfig> config = doQuery(url);
         if (config.data != null) {
             return config.data.content;

+ 10 - 3
instance/manager/src/test/java/com/alibaba/otter/canal/instance/manager/PlainCanalConfigClientIntegration.java

@@ -1,18 +1,22 @@
 package com.alibaba.otter.canal.instance.manager;
 
+import org.junit.Ignore;
 import org.junit.Test;
 import org.springframework.util.Assert;
 
 import com.alibaba.otter.canal.instance.manager.plain.PlainCanal;
 import com.alibaba.otter.canal.instance.manager.plain.PlainCanalConfigClient;
 
+@Ignore
 public class PlainCanalConfigClientIntegration {
 
     @Test
     public void testSimple() {
         PlainCanalConfigClient client = new PlainCanalConfigClient("http://127.0.0.1:8089",
             "admin",
-            "4ACFE3202A5FF5CF467898FC58AAB1D615029441");
+            "4ACFE3202A5FF5CF467898FC58AAB1D615029441",
+            "127.0.0.1",
+            11110);
 
         PlainCanal plain = client.findServer(null);
         Assert.notNull(plain);
@@ -20,8 +24,11 @@ public class PlainCanalConfigClientIntegration {
         plain = client.findServer(plain.getMd5());
         Assert.isNull(plain);
 
-        plain = client.findInstance("example", null);
-        Assert.notNull(plain);
+        String instances = client.findInstances(null);
+        Assert.notNull(instances);
+
+         plain = client.findInstance("example", null);
+         Assert.notNull(plain);
 
         plain = client.findInstance("example", plain.getMd5());
         Assert.isNull(plain);

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff