Browse Source

添加 aliyun 证书和域名解析 demo

reghao 1 year ago
parent
commit
0359d0dd75

+ 21 - 0
mgr/pom.xml

@@ -163,6 +163,27 @@
             <artifactId>commons-io</artifactId>
             <version>2.6</version>
         </dependency>
+
+        <dependency>
+            <groupId>com.aliyun.oss</groupId>
+            <artifactId>aliyun-sdk-oss</artifactId>
+            <version>3.8.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>sts20150401</artifactId>
+            <version>1.1.4</version>
+        </dependency>
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>alibabacloud-cas20200407</artifactId>
+            <version>3.0.0</version>
+        </dependency>
+        <dependency>
+            <groupId>com.aliyun</groupId>
+            <artifactId>alibabacloud-alidns20150109</artifactId>
+            <version>3.0.24</version>
+        </dependency>
     </dependencies>
 
     <profiles>

+ 57 - 0
mgr/src/main/java/cn/reghao/devops/mgr/mgr/aliyun/controller/AliyunController.java

@@ -0,0 +1,57 @@
+package cn.reghao.devops.mgr.mgr.aliyun.controller;
+
+import cn.reghao.devops.mgr.mgr.app.model.vo.AppConfigVO;
+import cn.reghao.devops.mgr.util.DefaultSetting;
+import cn.reghao.devops.mgr.util.PageSort;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.PageRequest;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+/**
+ * @author reghao
+ * @date 2025-03-28 09:30:20
+ */
+@Api(tags = "阿里云管理页面")
+@Controller
+@RequestMapping("/aliyun")
+public class AliyunController {
+    @ApiOperation(value = "阿里云 key 页面", notes = "N")
+    @GetMapping("/key")
+    public String aliyunKeyPage(@RequestParam(value = "env", required = false) String env,
+                                Model model) {
+        if (env == null) {
+            env = DefaultSetting.getDefaultEnv();
+        }
+
+        PageRequest pageRequest = PageSort.pageRequest();
+        Page<AppConfigVO> page = Page.empty();
+
+        model.addAttribute("env", env);
+        model.addAttribute("page", page);
+        model.addAttribute("list", page.getContent());
+        return "/devops/aliyun/keylist";
+    }
+
+    @ApiOperation(value = "证书列表页面", notes = "N")
+    @GetMapping("/cert")
+    public String certPage(@RequestParam(value = "env", required = false) String env,
+                                Model model) {
+        if (env == null) {
+            env = DefaultSetting.getDefaultEnv();
+        }
+
+        PageRequest pageRequest = PageSort.pageRequest();
+        Page<AppConfigVO> page = Page.empty();
+
+        model.addAttribute("env", env);
+        model.addAttribute("page", page);
+        model.addAttribute("list", page.getContent());
+        return "/devops/aliyun/certlist";
+    }
+}

+ 13 - 0
mgr/src/main/java/cn/reghao/devops/mgr/mgr/aliyun/model/AliyunCert.java

@@ -0,0 +1,13 @@
+package cn.reghao.devops.mgr.mgr.aliyun.model;
+
+/**
+ * 阿里云 SSL 证书
+ *
+ * @author reghao
+ * @date 2025-03-28 09:39:59
+ */
+public class AliyunCert {
+    private String domain;
+    private String status;
+    private String expireAt;
+}

+ 16 - 0
mgr/src/main/java/cn/reghao/devops/mgr/mgr/aliyun/model/AliyunKey.java

@@ -0,0 +1,16 @@
+package cn.reghao.devops.mgr.mgr.aliyun.model;
+
+import java.util.List;
+
+/**
+ * 阿里云帐号
+ *
+ * @author reghao
+ * @date 2025-03-28 09:50:27
+ */
+public class AliyunKey {
+    private String aliyunAccount;
+    private String accessKeyId;
+    private String accessKeySecret;
+    private List<String> endpoints;
+}

+ 247 - 0
mgr/src/main/java/cn/reghao/devops/mgr/mgr/aliyun/service/AliyunCertificate.java

@@ -0,0 +1,247 @@
+package cn.reghao.devops.mgr.mgr.aliyun.service;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import cn.reghao.jutil.jdk.converter.DateTimeConverter;
+import cn.reghao.jutil.jdk.security.RsaCryptor;
+import cn.reghao.jutil.jdk.serializer.JsonConverter;
+import cn.reghao.jutil.jdk.shell.ShellExecutor;
+import cn.reghao.jutil.jdk.shell.ShellResult;
+import cn.reghao.jutil.jdk.thread.ThreadPoolWrapper;
+import com.aliyun.auth.credentials.Credential;
+import com.aliyun.auth.credentials.provider.StaticCredentialProvider;
+import com.aliyun.sdk.service.cas20200407.models.*;
+import com.aliyun.sdk.service.cas20200407.*;
+import com.google.gson.Gson;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import darabonba.core.client.ClientOverrideConfiguration;
+import lombok.extern.slf4j.Slf4j;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.*;
+
+/**
+ * @author reghao
+ * @date 2025-03-27 16:14:53
+ */
+@Slf4j
+public class AliyunCertificate {
+    static ScheduledExecutorService scheduler = ThreadPoolWrapper.scheduledThreadPool("checker", 10);
+    static Map<Long, ScheduledFuture<?>> futureMap = new HashMap<>();
+
+    static void setLogLevel() {
+        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
+        Logger rootLogger = loggerContext.getLogger("ROOT");
+        rootLogger.setLevel(Level.INFO);
+    }
+
+    public static void main(String[] args) throws Exception {
+        setLogLevel();
+
+        StaticCredentialProvider provider = StaticCredentialProvider.create(Credential.builder()
+                .accessKeyId(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"))
+                .accessKeySecret(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"))
+                //.securityToken(System.getenv("ALIBABA_CLOUD_SECURITY_TOKEN")) // use STS token
+                .build());
+
+        // Endpoint 请参考 https://api.aliyun.com/product/cas
+        String endpoint = "cas.aliyuncs.com";
+        AsyncClient client = AsyncClient.builder()
+                .credentialsProvider(provider)
+                .overrideConfiguration(
+                        ClientOverrideConfiguration.create()
+                                .setEndpointOverride(endpoint)
+                )
+                .build();
+
+        String orderType = "";
+        //orderType = "CERT";
+        listUserCertificateOrder(client, orderType);
+        log.info("main-thread goto sleep...");
+        Thread.sleep(3600_000);
+
+        //createCertificate(client);
+        long orderId = 13588158L;
+        //describeCertificateState(client, orderId);
+        // Finally, close the client
+        client.close();
+    }
+
+    static void listUserCertificateOrder(AsyncClient client, String orderType) throws Exception {
+        ListUserCertificateOrderRequest listUserCertificateOrderRequest;
+        if (orderType.isBlank()) {
+            listUserCertificateOrderRequest = ListUserCertificateOrderRequest.builder()
+                    .build();
+        } else {
+            listUserCertificateOrderRequest = ListUserCertificateOrderRequest.builder()
+                    .orderType(orderType)
+                    .build();
+        }
+
+        CompletableFuture<ListUserCertificateOrderResponse> response = client.listUserCertificateOrder(listUserCertificateOrderRequest);
+        ListUserCertificateOrderResponse resp = response.get();
+        String jsonData = new Gson().toJson(resp);
+        JsonObject jsonObject = JsonConverter.jsonToObject(jsonData, JsonObject.class);
+        JsonArray jsonArray = jsonObject.get("body").getAsJsonObject().get("certificateOrderList").getAsJsonArray();
+        for (JsonElement jsonElement : jsonArray) {
+            JsonObject jsonObject1 = jsonElement.getAsJsonObject();
+            if (orderType.isBlank()) {
+                long orderId = jsonObject1.get("orderId").getAsLong();
+                describeCertificateState(client, orderId);
+
+                /*String domain = jsonObject1.get("domain").getAsString();
+                long endTimeMs = jsonObject1.get("certEndTime").getAsLong();
+                LocalDateTime expireDateTime = DateTimeConverter.localDateTime(endTimeMs);*/
+            } else {
+                long certificateId = jsonObject1.get("certificateId").getAsLong();
+                getUserCertificateDetail(client, certificateId);
+            }
+        }
+    }
+
+    static void getUserCertificateDetail(AsyncClient client, long certificateId) throws Exception {
+        GetUserCertificateDetailRequest getUserCertificateDetailRequest = GetUserCertificateDetailRequest.builder()
+                .certId(certificateId)
+                .certFilter(false)
+                .build();
+
+        CompletableFuture<GetUserCertificateDetailResponse> response = client.getUserCertificateDetail(getUserCertificateDetailRequest);
+        GetUserCertificateDetailResponse resp = response.get();
+        String jsonData = new Gson().toJson(resp);
+        JsonObject jsonObject = JsonConverter.jsonToObject(jsonData, JsonObject.class);
+        JsonObject certDetail = jsonObject.get("body").getAsJsonObject();
+        String domain = certDetail.get("sans").getAsString();
+        String certificate = certDetail.get("cert").getAsString();
+        String privateKey = certDetail.get("key").getAsString();
+        long notBefore = certDetail.get("notBefore").getAsLong();
+        long notAfter = certDetail.get("notAfter").getAsLong();
+        LocalDateTime expireDateTime = DateTimeConverter.localDateTime(notAfter);
+
+        String certPath = String.format("/home/reghao/Downloads/1/%s.pem", domain);
+        File certFile = new File(certPath);
+        if (!certFile.exists()) {
+            RsaCryptor.savePemFile(certificate, certPath);
+        }
+
+        String keyPath = String.format("/home/reghao/Downloads/1/%s.key", domain);;
+        File keyFile = new File(keyPath);
+        if (!keyFile.exists()) {
+            RsaCryptor.savePemFile(privateKey, keyPath);
+        }
+        System.out.println();
+    }
+
+    static void createCertificates(AsyncClient client) throws Exception {
+        List<String> domainList = List.of("tnb.reghao.cn", "api.reghao.cn", "oss.reghao.cn", "bnt.reghao.cn");
+        List<String> domainList1 = List.of("blog.reghao.cn", "git.reghao.cn", "docker.reghao.cn");
+        for (String domain : domainList) {
+            createCertificate(client, domain);
+            Thread.sleep(5_000);
+        }
+    }
+
+    static void createCertificate(AsyncClient client, String domain) throws ExecutionException, InterruptedException {
+        CreateCertificateRequestRequest createCertificateRequestRequest = CreateCertificateRequestRequest.builder()
+                .productCode("digicert-free-1-free")
+                .username("赵梓潼")
+                .phone("18782243510")
+                .email("lhao.sg@qq.com")
+                .domain(domain)
+                .validateType("DNS")
+                .build();
+
+        CompletableFuture<CreateCertificateRequestResponse> response = client.createCertificateRequest(createCertificateRequestRequest);
+        CreateCertificateRequestResponse resp = response.get();
+        String jsonData = new Gson().toJson(resp);
+        JsonObject jsonObject = JsonConverter.jsonToObject(jsonData, JsonObject.class);
+        JsonObject respBody = jsonObject.get("body").getAsJsonObject();
+        long orderId = respBody.get("orderId").getAsLong();
+        log.info("orderId -> {}", orderId);
+    }
+
+    static void describeCertificateState(AsyncClient client, long orderId) throws ExecutionException, InterruptedException {
+        DescribeCertificateStateRequest describeCertificateStateRequest = DescribeCertificateStateRequest.builder()
+                .orderId(orderId)
+                .build();
+
+        CompletableFuture<DescribeCertificateStateResponse> response = client.describeCertificateState(describeCertificateStateRequest);
+        DescribeCertificateStateResponse resp = response.get();
+        String jsonData = new Gson().toJson(resp);
+        JsonObject jsonObject = JsonConverter.jsonToObject(jsonData, JsonObject.class);
+        JsonObject respBody = jsonObject.get("body").getAsJsonObject();
+        String type = respBody.get("type").getAsString();
+        if ("certificate".equals(type)) {
+            String certificate = respBody.get("certificate").getAsString();
+            String privateKey = respBody.get("privateKey").getAsString();
+        } else if ("domain_verify".equals(type)) {
+            String recordDomain = respBody.get("recordDomain").getAsString();
+            String recordType = respBody.get("recordType").getAsString();
+            String recordValue = respBody.get("recordValue").getAsString();
+            String domain = recordDomain.replace("_dnsauth.", "");
+            // 添加 TXT 记录
+            AliyunDns.addTxtRecord(domain, recordDomain, recordValue);
+
+            CheckTask checkTask = new CheckTask(client, domain, orderId);
+            ScheduledFuture<?> future = scheduler.scheduleAtFixedRate(checkTask, 60, 60, TimeUnit.SECONDS);
+            futureMap.put(orderId, future);
+            Thread.sleep(5_000);
+        }
+    }
+
+    static class CheckTask implements Runnable {
+        private final AsyncClient client;
+        private final String domain;
+        private final long orderId;
+
+        public CheckTask(AsyncClient client, String domain, long orderId) {
+            this.client = client;
+            this.domain = domain;
+            this.orderId = orderId;
+        }
+
+        public void run() {
+            try {
+                DescribeCertificateStateRequest describeCertificateStateRequest = DescribeCertificateStateRequest.builder()
+                        .orderId(orderId)
+                        .build();
+
+                CompletableFuture<DescribeCertificateStateResponse> response = client.describeCertificateState(describeCertificateStateRequest);
+                DescribeCertificateStateResponse resp = response.get();
+                String jsonData = new Gson().toJson(resp);
+                JsonObject jsonObject = JsonConverter.jsonToObject(jsonData, JsonObject.class);
+                JsonObject respBody = jsonObject.get("body").getAsJsonObject();
+                String type = respBody.get("type").getAsString();
+                if ("certificate".equals(type)) {
+                    String certificate = respBody.get("certificate").getAsString();
+                    String privateKey = respBody.get("privateKey").getAsString();
+                    ScheduledFuture<?> future = futureMap.get(orderId);
+                    if (future != null) {
+                        log.info("{} - {} 证书已签发, 取消定时任务...", domain, orderId);
+                        future.cancel(true);
+                    }
+                } else {
+                    log.info("{} - {} 证书状态 {}...", domain, orderId, type);
+                }
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+    static void reloadNginx() {
+        ShellExecutor shellExecutor = new ShellExecutor();
+        String cmd = "/usr/bin/nginx -t";
+        String cmd1 = "/usr/bin/nginx -s reload";
+        ShellResult shellResult = shellExecutor.exec(cmd.split("\\s+"));
+        String result = shellResult.getResult();
+        System.out.println(result);
+    }
+}

+ 91 - 0
mgr/src/main/java/cn/reghao/devops/mgr/mgr/aliyun/service/AliyunDns.java

@@ -0,0 +1,91 @@
+package cn.reghao.devops.mgr.mgr.aliyun.service;
+
+import cn.reghao.jutil.jdk.serializer.JsonConverter;
+import com.aliyun.auth.credentials.Credential;
+import com.aliyun.auth.credentials.provider.StaticCredentialProvider;
+import com.aliyun.sdk.service.alidns20150109.AsyncClient;
+import com.aliyun.sdk.service.alidns20150109.models.AddDomainRecordRequest;
+import com.aliyun.sdk.service.alidns20150109.models.AddDomainRecordResponse;
+import com.aliyun.sdk.service.alidns20150109.models.GetTxtRecordForVerifyRequest;
+import com.aliyun.sdk.service.alidns20150109.models.GetTxtRecordForVerifyResponse;
+import com.google.gson.Gson;
+import com.google.gson.JsonObject;
+import darabonba.core.client.ClientOverrideConfiguration;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+
+/**
+ * @author reghao
+ * @date 2025-03-28 16:22:34
+ */
+public class AliyunDns {
+    public static void addTxtRecord(String domain, String rr, String value) throws ExecutionException, InterruptedException {
+        StaticCredentialProvider provider = StaticCredentialProvider.create(Credential.builder()
+                .accessKeyId(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"))
+                .accessKeySecret(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"))
+                //.securityToken(System.getenv("ALIBABA_CLOUD_SECURITY_TOKEN")) // use STS token
+                .build());
+
+        // Endpoint 请参考 https://api.aliyun.com/product/Alidns
+        String endpoint = "alidns.cn-chengdu.aliyuncs.com";
+        String regionId = "cn-chengdu";
+        AsyncClient client = AsyncClient.builder()
+                .region(regionId)
+                .credentialsProvider(provider)
+                .overrideConfiguration(
+                        ClientOverrideConfiguration.create()
+                                .setEndpointOverride(endpoint)
+                )
+                .build();
+
+        /*String domain = "";
+        String rr = "";
+        String value = "";*/
+        AddDomainRecordRequest addDomainRecordRequest = AddDomainRecordRequest.builder()
+                .domainName(domain)
+                .type("TXT")
+                .rr(rr)
+                .value(value)
+                .build();
+
+        // Asynchronously get the return value of the API request
+        CompletableFuture<AddDomainRecordResponse> response = client.addDomainRecord(addDomainRecordRequest);
+        // Synchronously get the return value of the API request
+        AddDomainRecordResponse resp = response.get();
+        String jsonData = new Gson().toJson(resp);
+        JsonObject jsonObject = JsonConverter.jsonToObject(jsonData, JsonObject.class);
+        JsonObject respBody = jsonObject.get("body").getAsJsonObject();
+    }
+
+    static void test() throws ExecutionException, InterruptedException {
+        StaticCredentialProvider provider = StaticCredentialProvider.create(Credential.builder()
+                .accessKeyId(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_ID"))
+                .accessKeySecret(System.getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET"))
+                //.securityToken(System.getenv("ALIBABA_CLOUD_SECURITY_TOKEN")) // use STS token
+                .build());
+
+        // Endpoint 请参考 https://api.aliyun.com/product/Alidns
+        String endpoint = "alidns.cn-chengdu.aliyuncs.com";
+        String regionId = "cn-chengdu";
+        AsyncClient client = AsyncClient.builder()
+                .region(regionId)
+                .credentialsProvider(provider)
+                .overrideConfiguration(
+                        ClientOverrideConfiguration.create()
+                                .setEndpointOverride(endpoint)
+                )
+                .build();
+
+        GetTxtRecordForVerifyRequest getTxtRecordForVerifyRequest = GetTxtRecordForVerifyRequest.builder()
+                .domainName("")
+                .type("ADD_SUB_DOMAIN")
+                .lang("en")
+                .build();
+
+        CompletableFuture<GetTxtRecordForVerifyResponse> response = client.getTxtRecordForVerify(getTxtRecordForVerifyRequest);
+        GetTxtRecordForVerifyResponse resp = response.get();
+        System.out.println(new Gson().toJson(resp));
+        client.close();
+    }
+}

+ 110 - 0
mgr/src/main/resources/templates/devops/aliyun/certlist.html

@@ -0,0 +1,110 @@
+<!DOCTYPE html>
+<html xmlns:th="http://www.thymeleaf.org"
+      xmlns:mo="https://gitee.com/aun/Timo">
+<head th:replace="/common/template :: header(~{::title},~{::link},~{::style})">
+    <link rel="stylesheet" th:href="@{/lib/zTree_v3/css/zTreeStyle/zTreeStyle.css}" type="text/css">
+</head>
+
+<body class="timo-layout-page">
+<div class="layui-card">
+    <div class="layui-card-header timo-card-header">
+        <span><i class="fa fa-bars"></i> 证书列表</span>
+        <i class="layui-icon layui-icon-refresh refresh-btn"></i>
+    </div>
+    <div class="layui-card-body">
+        <div class="layui-row timo-card-screen put-row">
+            <div class="layui-row timo-card-screen put-row">
+                <div class="pull-left layui-form-pane">
+                    <div class="layui-inline">
+                        <label class="layui-form-label">环境</label>
+                        <div class="layui-input-block timo-search-status">
+                            <select id="getPageByEnv" class="timo-search-select" name="env" onchange="getPageByCriteria()"
+                                    mo:dict="ENVIRONMENT" mo-selected="${env}"></select>
+                        </div>
+                    </div>
+                    <div class="layui-inline">
+                        <button class="layui-btn timo-search-btn">
+                            <i class="fa fa-search"></i>
+                        </button>
+                    </div>
+                </div>
+                <div class="pull-right">
+                    <div class="btn-group-right">
+                        <button class="layui-btn open-popup" data-title="添加域名" th:attr="data-url=@{/app/config/app/add}"
+                                data-size="max">
+                            <i class="fa fa-plus"></i> 添加
+                        </button>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="timo-table-wrap">
+            <table class="layui-table timo-table">
+                <thead>
+                <tr>
+                    <th class="timo-table-checkbox">
+                        <label class="timo-checkbox"><input type="checkbox">
+                            <i class="layui-icon layui-icon-ok"></i></label>
+                    </th>
+                    <th class="sortable" data-field="appName">域名</th>
+                    <th class="sortable" data-field="appType">证书 ID</th>
+                    <th class="sortable" data-field="appId">证书状态</th>
+                    <th class="sortable" data-field="repoBranch">过期时间</th>
+                    <th class="sortable" data-field="bindPorts">监听端口</th>
+                    <th>操作</th>
+                </tr>
+                </thead>
+                <tbody>
+                <tr th:each="item:${list}">
+                    <td>
+                        <label class="timo-checkbox">
+                            <input type="checkbox" th:value="${item.appId}">
+                            <i class="layui-icon layui-icon-ok"></i>
+                        </label>
+                    </td>
+                    <td th:text="${item.appName}">应用名</td>
+                    <td th:text="${item.appId}">应用 ID</td>
+                    <td th:text="${item.repoBranch}">分支</td>
+                    <td th:text="${item.appType}">应用类型</td>
+                    <td th:text="${item.bindPorts?: ''}">监听端口</td>
+                    <td>
+                        <span style="color: red" th:text="${item.total}"></span>
+                        <a class="open-popup" data-title="部署配置" th:attr="data-url=@{'/app/config/app/deploy/'+${item.appId}}"
+                           data-size="max" href="#">配置</a>
+                    </td>
+                    <td>
+                        <a class="open-popup" data-title="拷贝应用" th:attr="data-url=@{'/app/config/app/copy/'+${item.appId}}"
+                           href="#">证书列表</a>
+                        <a class="open-popup" data-title="应用详细信息" th:attr="data-url=@{'/app/config/app/detail/'+${item.appId}}"
+                           data-size="1200,500" href="#">详细</a>
+                        <a class="open-popup" data-title="编辑" th:attr="data-url=@{'/app/config/app/edit/'+${item.id}}"
+                           data-size="1200,500" href="#">编辑</a>
+                        <a class="ajax-delete" th:attr="data-msg='确定要删除 '+ ${item.appId}"
+                           th:href="@{'/api/app/config/app/' + ${item.id}}">删除</a>
+                        <a class="ajax-delete" th:attr="data-msg='确定要清空 ' + ${item.appId} + ' 的本地仓库'"
+                           th:href="@{'/api/app/config/app/repo/' + ${item.id}}">清空本地仓库</a>
+                    </td>
+                </tr>
+                </tbody>
+            </table>
+        </div>
+        <div th:replace="/common/fragment :: page"></div>
+    </div>
+</div>
+
+<script th:replace="/common/template :: script"></script>
+<script type="text/javascript" th:src="@{/js/plugins/jquery-2.2.4.min.js}"></script>
+<script type="text/javascript">
+    function getPageByCriteria() {
+        var envSelectedOption = $("#getPageByEnv option:selected")
+        var envParam = envSelectedOption.text()
+
+        var typeSelectedOption = $("#getPageByType option:selected")
+        var typeParam = typeSelectedOption.text()
+
+        url = '?env=' + envParam + '&type=' + typeParam
+        window.location.href = window.location.pathname + url;
+    }
+</script>
+</body>
+</html>

+ 94 - 0
mgr/src/main/resources/templates/devops/aliyun/keylist.html

@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<html xmlns:th="http://www.thymeleaf.org"
+      xmlns:mo="https://gitee.com/aun/Timo">
+<head th:replace="/common/template :: header(~{::title},~{::link},~{::style})">
+    <link rel="stylesheet" th:href="@{/lib/zTree_v3/css/zTreeStyle/zTreeStyle.css}" type="text/css">
+</head>
+
+<body class="timo-layout-page">
+<div class="layui-card">
+    <div class="layui-card-header timo-card-header">
+        <span><i class="fa fa-bars"></i> 阿里云Key</span>
+        <i class="layui-icon layui-icon-refresh refresh-btn"></i>
+    </div>
+    <div class="layui-card-body">
+        <div class="layui-row timo-card-screen put-row">
+            <div class="layui-row timo-card-screen put-row">
+                <div class="pull-right">
+                    <div class="btn-group-right">
+                        <button class="layui-btn open-popup" data-title="添加域名" th:attr="data-url=@{/app/config/app/add}"
+                                data-size="max">
+                            <i class="fa fa-plus"></i> 添加
+                        </button>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="timo-table-wrap">
+            <table class="layui-table timo-table">
+                <thead>
+                <tr>
+                    <th class="timo-table-checkbox">
+                        <label class="timo-checkbox"><input type="checkbox">
+                            <i class="layui-icon layui-icon-ok"></i></label>
+                    </th>
+                    <th class="sortable" data-field="appId">阿里云帐号</th>
+                    <th class="sortable" data-field="appName">ACCESS_KEY_ID</th>
+                    <th class="sortable" data-field="appType">ACCESS_KEY_SECRET</th>
+                    <th>操作</th>
+                </tr>
+                </thead>
+                <tbody>
+                <tr th:each="item:${list}">
+                    <td>
+                        <label class="timo-checkbox">
+                            <input type="checkbox" th:value="${item.appId}">
+                            <i class="layui-icon layui-icon-ok"></i>
+                        </label>
+                    </td>
+                    <td th:text="${item.appName}">应用名</td>
+                    <td th:text="${item.appId}">应用 ID</td>
+                    <td th:text="${item.repoBranch}">分支</td>
+                    <td th:text="${item.appType}">应用类型</td>
+                    <td th:text="${item.bindPorts?: ''}">监听端口</td>
+                    <td>
+                        <span style="color: red" th:text="${item.total}"></span>
+                        <a class="open-popup" data-title="部署配置" th:attr="data-url=@{'/app/config/app/deploy/'+${item.appId}}"
+                           data-size="max" href="#">配置</a>
+                    </td>
+                    <td>
+                        <a class="open-popup" data-title="拷贝应用" th:attr="data-url=@{'/app/config/app/copy/'+${item.appId}}"
+                           href="#">Endpoint 列表</a>
+                        <a class="open-popup" data-title="应用详细信息" th:attr="data-url=@{'/app/config/app/detail/'+${item.appId}}"
+                           data-size="1200,500" href="#">详细</a>
+                        <a class="open-popup" data-title="编辑" th:attr="data-url=@{'/app/config/app/edit/'+${item.id}}"
+                           data-size="1200,500" href="#">编辑</a>
+                        <a class="ajax-delete" th:attr="data-msg='确定要删除 '+ ${item.appId}"
+                           th:href="@{'/api/app/config/app/' + ${item.id}}">删除</a>
+                        <a class="ajax-delete" th:attr="data-msg='确定要清空 ' + ${item.appId} + ' 的本地仓库'"
+                           th:href="@{'/api/app/config/app/repo/' + ${item.id}}">清空本地仓库</a>
+                    </td>
+                </tr>
+                </tbody>
+            </table>
+        </div>
+        <div th:replace="/common/fragment :: page"></div>
+    </div>
+</div>
+
+<script th:replace="/common/template :: script"></script>
+<script type="text/javascript" th:src="@{/js/plugins/jquery-2.2.4.min.js}"></script>
+<script type="text/javascript">
+    function getPageByCriteria() {
+        var envSelectedOption = $("#getPageByEnv option:selected")
+        var envParam = envSelectedOption.text()
+
+        var typeSelectedOption = $("#getPageByType option:selected")
+        var typeParam = typeSelectedOption.text()
+
+        url = '?env=' + envParam + '&type=' + typeParam
+        window.location.href = window.location.pathname + url;
+    }
+</script>
+</body>
+</html>