This is an automated email from the ASF dual-hosted git repository.

dockerzhang pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/inlong.git


The following commit(s) were added to refs/heads/master by this push:
     new 89c965b4ae [INLONG-10409][Dashboard] Support installing agents by SSH 
key-based auth (#10636)
89c965b4ae is described below

commit 89c965b4aea0f7432066b55360f154d514148138
Author: qingliu <shuiqingli...@gmail.com>
AuthorDate: Wed Jul 17 20:50:22 2024 +0800

    [INLONG-10409][Dashboard] Support installing agents by SSH key-based auth 
(#10636)
    
    Co-authored-by: Charles Zhang <dockerzh...@apache.org>
---
 inlong-dashboard/src/ui/locales/cn.json            |  3 +
 inlong-dashboard/src/ui/locales/en.json            |  3 +
 .../src/ui/pages/Clusters/NodeEditModal.tsx        | 95 +++++++++++++++++++++-
 .../service/cluster/InlongClusterService.java      |  8 ++
 .../service/cluster/InlongClusterServiceImpl.java  | 13 +++
 .../web/controller/InlongClusterController.java    |  6 ++
 6 files changed, 124 insertions(+), 4 deletions(-)

diff --git a/inlong-dashboard/src/ui/locales/cn.json 
b/inlong-dashboard/src/ui/locales/cn.json
index 5e0fb31416..6ff457fe97 100644
--- a/inlong-dashboard/src/ui/locales/cn.json
+++ b/inlong-dashboard/src/ui/locales/cn.json
@@ -772,9 +772,12 @@
   "pages.Clusters.Node.IsInstall": "安装方式",
   "pages.Clusters.Node.ManualInstall": "手动安装",
   "pages.Clusters.Node.SSHInstall": "SSH 安装",
+  "pages.Clusters.Node.IdentifyType": "认证方式",
   "pages.Clusters.Node.Username": "SSH 用户名",
   "pages.Clusters.Node.Password": "SSH 密码",
+  "pages.Clusters.Node.SSHKey": "SSH 密钥",
   "pages.Clusters.Node.SSHPort": "SSH 端口",
+  "pages.Clusters.Node.SSHKeyHelper": "请将公钥上传至 Agent 节点的 
~/.ssh/authorized_keys 文件中",
   "pages.Clusters.Node.Status": "状态",
   "pages.Clusters.Node.Status.Normal": "正常",
   "pages.Clusters.Node.Status.Timeout": "心跳超时",
diff --git a/inlong-dashboard/src/ui/locales/en.json 
b/inlong-dashboard/src/ui/locales/en.json
index 165e6deec6..8d05262a88 100644
--- a/inlong-dashboard/src/ui/locales/en.json
+++ b/inlong-dashboard/src/ui/locales/en.json
@@ -772,9 +772,12 @@
   "pages.Clusters.Node.IsInstall": "Installation",
   "pages.Clusters.Node.ManualInstall": "Manual",
   "pages.Clusters.Node.SSHInstall": "SSH",
+  "pages.Clusters.Node.IdentifyType": "Identify Type",
   "pages.Clusters.Node.Username": "SSH Username",
+  "pages.Clusters.Node.SSHKey": "SSH Key",
   "pages.Clusters.Node.Password": "SSH Password",
   "pages.Clusters.Node.SSHPort": "SSH Port",
+  "pages.Clusters.Node.SSHKeyHelper": "Please upload the public key to the 
~/.ssh/authorized_keys file of the Agent node",
   "pages.Clusters.Node.Status": "Status",
   "pages.Clusters.Node.Status.Normal": "Normal",
   "pages.Clusters.Node.Status.Timeout": "Timeout",
diff --git a/inlong-dashboard/src/ui/pages/Clusters/NodeEditModal.tsx 
b/inlong-dashboard/src/ui/pages/Clusters/NodeEditModal.tsx
index a8a070f3aa..5a16e0d771 100644
--- a/inlong-dashboard/src/ui/pages/Clusters/NodeEditModal.tsx
+++ b/inlong-dashboard/src/ui/pages/Clusters/NodeEditModal.tsx
@@ -17,9 +17,9 @@
  * under the License.
  */
 
-import React, { useMemo } from 'react';
+import React, { useEffect, useMemo, useState } from 'react';
 import i18n from '@/i18n';
-import { Modal, message } from 'antd';
+import { Modal, message, Button } from 'antd';
 import { ModalProps } from 'antd/es/modal';
 import FormGenerator, { useForm } from '@/ui/components/FormGenerator';
 import { useRequest, useUpdateEffect } from '@/ui/hooks';
@@ -34,6 +34,7 @@ export interface NodeEditModalProps extends ModalProps {
 
 const NodeEditModal: React.FC<NodeEditModalProps> = ({ id, type, clusterId, 
...modalProps }) => {
   const [form] = useForm();
+  const [isInstall, setInstallType] = useState(false);
 
   const { data: savedData, run: getData } = useRequest(
     id => ({
@@ -103,6 +104,34 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ id, 
type, clusterId, ...m
     },
   );
 
+  const { data: sshKeys, run: getSSHKeys } = useRequest(
+    () => ({
+      url: '/cluster/node/getManagerSSHPublicKey',
+      method: 'GET',
+    }),
+    {
+      manual: true,
+      onSuccess: result => {
+        form.setFieldValue('sshKey', result);
+      },
+    },
+  );
+
+  const testSSHConnection = async () => {
+    const values = await form.validateFields();
+    const submitData = {
+      ...values,
+      type,
+      parentId: savedData?.parentId || clusterId,
+    };
+    await request({
+      url: '/cluster/node/testSSHConnection',
+      method: 'POST',
+      data: submitData,
+    });
+    message.success(i18n.t('basic.ConnectionSuccess'));
+  };
+
   useUpdateEffect(() => {
     if (modalProps.open) {
       // open
@@ -212,6 +241,9 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ id, 
type, clusterId, ...m
         hidden: type !== 'AGENT',
         rules: [{ required: true }],
         props: {
+          onChange: ({ target: { value } }) => {
+            setInstallType(value);
+          },
           options: [
             {
               label: i18n.t('pages.Clusters.Node.ManualInstall'),
@@ -224,6 +256,32 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ id, 
type, clusterId, ...m
           ],
         },
       },
+      {
+        type: 'radio',
+        label: i18n.t('pages.Clusters.Node.IdentifyType'),
+        name: 'identifyType',
+        initialValue: 'password',
+        hidden: type !== 'AGENT',
+        visible: values => values?.isInstall,
+        rules: [{ required: true }],
+        props: {
+          onChange: ({ target: { value } }) => {
+            if (value === 'sshKey' && !form.getFieldValue('sshKey')) {
+              getSSHKeys();
+            }
+          },
+          options: [
+            {
+              label: i18n.t('pages.Clusters.Node.Password'),
+              value: 'password',
+            },
+            {
+              label: i18n.t('pages.Clusters.Node.SSHKey'),
+              value: 'sshKey',
+            },
+          ],
+        },
+      },
       {
         type: 'input',
         label: i18n.t('pages.Clusters.Node.Username'),
@@ -238,7 +296,20 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ id, 
type, clusterId, ...m
         name: 'password',
         rules: [{ required: true }],
         hidden: type !== 'AGENT',
-        visible: values => values?.isInstall,
+        visible: values => values?.isInstall && values?.identifyType === 
'password',
+      },
+      {
+        type: 'textarea',
+        label: i18n.t('pages.Clusters.Node.SSHKey'),
+        tooltip: i18n.t('pages.Clusters.Node.SSHKeyHelper'),
+        name: 'sshKey',
+        rules: [{ required: true }],
+        hidden: type !== 'AGENT',
+        visible: values => values?.isInstall && values?.identifyType === 
'sshKey',
+        props: {
+          readOnly: true,
+          autoSize: true,
+        },
       },
       {
         type: 'input',
@@ -285,7 +356,23 @@ const NodeEditModal: React.FC<NodeEditModalProps> = ({ id, 
type, clusterId, ...m
   }, []);
 
   return (
-    <Modal {...modalProps} title={i18n.t('pages.Clusters.Node.Name')} 
onOk={onOk}>
+    <Modal
+      {...modalProps}
+      title={i18n.t('pages.Clusters.Node.Name')}
+      footer={[
+        <Button key="cancel" onClick={e => modalProps.onCancel(e)}>
+          {i18n.t('basic.Cancel')}
+        </Button>,
+        <Button key="save" type="primary" onClick={onOk}>
+          {i18n.t('basic.Save')}
+        </Button>,
+        isInstall && (
+          <Button key="run" type="primary" onClick={testSSHConnection}>
+            {i18n.t('pages.Nodes.TestConnection')}
+          </Button>
+        ),
+      ]}
+    >
       <FormGenerator content={content} form={form} useMaxWidth />
     </Modal>
   );
diff --git 
a/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/cluster/InlongClusterService.java
 
b/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/cluster/InlongClusterService.java
index 22282ba67e..00a13e0804 100644
--- 
a/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/cluster/InlongClusterService.java
+++ 
b/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/cluster/InlongClusterService.java
@@ -293,6 +293,14 @@ public interface InlongClusterService {
      */
     String getManagerSSHPublicKey();
 
+    /**
+     * Test whether the SSH connection can be successfully established using 
the provided SSH information.
+     *
+     * @param request connection request
+     * @return true or false
+     */
+    Boolean testSSHConnection(ClusterNodeRequest request);
+
     /**
      * Query data proxy nodes by the given inlong group id and protocol type
      *
diff --git 
a/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/cluster/InlongClusterServiceImpl.java
 
b/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/cluster/InlongClusterServiceImpl.java
index 5be7ffd3b1..e5912bd93f 100644
--- 
a/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/cluster/InlongClusterServiceImpl.java
+++ 
b/inlong-manager/manager-service/src/main/java/org/apache/inlong/manager/service/cluster/InlongClusterServiceImpl.java
@@ -61,6 +61,7 @@ import 
org.apache.inlong.manager.pojo.cluster.ClusterTagResponse;
 import org.apache.inlong.manager.pojo.cluster.TenantClusterTagInfo;
 import org.apache.inlong.manager.pojo.cluster.TenantClusterTagPageRequest;
 import org.apache.inlong.manager.pojo.cluster.TenantClusterTagRequest;
+import org.apache.inlong.manager.pojo.cluster.agent.AgentClusterNodeRequest;
 import 
org.apache.inlong.manager.pojo.cluster.dataproxy.DataProxyClusterNodeDTO;
 import org.apache.inlong.manager.pojo.cluster.pulsar.PulsarClusterDTO;
 import org.apache.inlong.manager.pojo.common.PageResult;
@@ -78,6 +79,7 @@ import 
org.apache.inlong.manager.service.cluster.node.InlongClusterNodeInstallOp
 import 
org.apache.inlong.manager.service.cluster.node.InlongClusterNodeOperator;
 import 
org.apache.inlong.manager.service.cluster.node.InlongClusterNodeOperatorFactory;
 import org.apache.inlong.manager.service.cmd.CommandExecutor;
+import org.apache.inlong.manager.service.cmd.CommandResult;
 import org.apache.inlong.manager.service.repository.DataProxyConfigRepository;
 import org.apache.inlong.manager.service.tenant.InlongTenantService;
 import org.apache.inlong.manager.service.user.InlongRoleService;
@@ -891,6 +893,17 @@ public class InlongClusterServiceImpl implements 
InlongClusterService {
         }
     }
 
+    @Override
+    public Boolean testSSHConnection(ClusterNodeRequest request) {
+        AgentClusterNodeRequest nodeRequest = (AgentClusterNodeRequest) 
request;
+        try {
+            CommandResult commandResult = 
commandExecutor.execRemote(nodeRequest, "ls");
+            return commandResult.getCode() == 0;
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
     @Override
     public DataProxyNodeResponse getDataProxyNodes(String groupId, String 
protocolType) {
         LOGGER.debug("begin to get data proxy nodes for groupId={}, 
protocol={}", groupId, protocolType);
diff --git 
a/inlong-manager/manager-web/src/main/java/org/apache/inlong/manager/web/controller/InlongClusterController.java
 
b/inlong-manager/manager-web/src/main/java/org/apache/inlong/manager/web/controller/InlongClusterController.java
index da6bc39253..e66d4fb833 100644
--- 
a/inlong-manager/manager-web/src/main/java/org/apache/inlong/manager/web/controller/InlongClusterController.java
+++ 
b/inlong-manager/manager-web/src/main/java/org/apache/inlong/manager/web/controller/InlongClusterController.java
@@ -303,6 +303,12 @@ public class InlongClusterController {
         return Response.success(clusterService.getManagerSSHPublicKey());
     }
 
+    @PostMapping("/cluster/node/testSSHConnection")
+    @ApiOperation(value = "Test SSH connection for inlong cluster node")
+    public Response<Boolean> testSSHConnection(@RequestBody ClusterNodeRequest 
request) {
+        return Response.success(clusterService.testSSHConnection(request));
+    }
+
     @PostMapping("/cluster/testConnection")
     @ApiOperation(value = "Test connection for inlong cluster")
     public Response<Boolean> testConnection(@Validated @RequestBody 
ClusterRequest request) {

Reply via email to