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) {