From 9c6cf15d70552b91a660c753347fcd0ee89c443c Mon Sep 17 00:00:00 2001 From: yud8 Date: Thu, 11 Dec 2025 21:02:31 +0800 Subject: [PATCH 01/11] [Feature-17501]Third-party System API Connector for REST-based Task Scheduling #17501 --- .../controller/ExternalSystemController.java | 63 ++++ .../api/service/ExternalSystemService.java | 29 ++ .../impl/ExternalSystemServiceImpl.java | 282 ++++++++++++++++++ .../dao/entity/ExternalSystemTaskQuery.java | 29 ++ 4 files changed, 403 insertions(+) create mode 100644 dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/ExternalSystemController.java create mode 100644 dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/ExternalSystemService.java create mode 100644 dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ExternalSystemServiceImpl.java create mode 100644 dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/ExternalSystemTaskQuery.java diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/ExternalSystemController.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/ExternalSystemController.java new file mode 100644 index 000000000000..d66fdd731755 --- /dev/null +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/ExternalSystemController.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.api.controller; + +import static org.apache.dolphinscheduler.api.enums.Status.QUERY_EXTERNAL_SYSTEM_ERROR; + +import org.apache.dolphinscheduler.api.exceptions.ApiException; +import org.apache.dolphinscheduler.api.service.ExternalSystemService; +import org.apache.dolphinscheduler.api.utils.Result; +import org.apache.dolphinscheduler.common.constants.Constants; +import org.apache.dolphinscheduler.dao.entity.ExternalSystemTaskQuery; +import org.apache.dolphinscheduler.dao.entity.User; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "EXTERNAL_SYSTEM_TAG") +@RestController +@RequestMapping("external-systems") +public class ExternalSystemController extends BaseController { + + @Autowired + private ExternalSystemService externalSystemService; + + @Operation(summary = "queryExternalSystemTasks", description = "QUERY_EXTERNAL_SYSTEM_TASKS_NOTES") + @GetMapping(value = "/queryExternalSystemTasks") + @ResponseStatus(HttpStatus.OK) + @ApiException(QUERY_EXTERNAL_SYSTEM_ERROR) + public Result> queryExternalSystemTasks(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser, + @RequestParam("externalSystemId") Integer externalSystemId) { + List result = + externalSystemService.queryExternalSystemTasks(loginUser, externalSystemId); + return Result.success(result); + } + +} diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/ExternalSystemService.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/ExternalSystemService.java new file mode 100644 index 000000000000..ec60b64e467b --- /dev/null +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/ExternalSystemService.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.api.service; + +import org.apache.dolphinscheduler.dao.entity.ExternalSystemTaskQuery; +import org.apache.dolphinscheduler.dao.entity.User; + +import java.util.List; + +public interface ExternalSystemService { + + List queryExternalSystemTasks(User loginUser, int externalSystemId); + +} diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ExternalSystemServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ExternalSystemServiceImpl.java new file mode 100644 index 000000000000..c479e244e64e --- /dev/null +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ExternalSystemServiceImpl.java @@ -0,0 +1,282 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.api.service.impl; + +import org.apache.dolphinscheduler.api.enums.Status; +import org.apache.dolphinscheduler.api.exceptions.ServiceException; +import org.apache.dolphinscheduler.api.service.ExternalSystemService; +import org.apache.dolphinscheduler.common.constants.Constants; +import org.apache.dolphinscheduler.common.model.OkHttpRequestHeaderContentType; +import org.apache.dolphinscheduler.common.model.OkHttpRequestHeaders; +import org.apache.dolphinscheduler.common.model.OkHttpResponse; +import org.apache.dolphinscheduler.common.utils.JSONUtils; +import org.apache.dolphinscheduler.common.utils.OkHttpUtils; +import org.apache.dolphinscheduler.dao.entity.DataSource; +import org.apache.dolphinscheduler.dao.entity.ExternalSystemTaskQuery; +import org.apache.dolphinscheduler.dao.entity.User; +import org.apache.dolphinscheduler.dao.mapper.DataSourceMapper; +import org.apache.dolphinscheduler.plugin.datasource.api.utils.PasswordUtils; +import org.apache.dolphinscheduler.plugin.task.api.TaskException; +import org.apache.dolphinscheduler.plugin.task.externalSystem.AuthenticationUtils; +import org.apache.dolphinscheduler.plugin.task.externalSystem.BaseExternalSystemParams; +import org.apache.dolphinscheduler.plugin.task.externalSystem.ExternalTaskConstants; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import lombok.extern.slf4j.Slf4j; +import okhttp3.FormBody; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.jayway.jsonpath.JsonPath; + +@Service +@Slf4j +public class ExternalSystemServiceImpl extends BaseServiceImpl implements ExternalSystemService { + + @Autowired + private DataSourceMapper dataSourceMapper; + + private static final String EXTERNAL_TASK_ID = "id"; + private static final String EXTERNAL_TASK_NAME = "name"; + + private OkHttpResponse callSelectInterface(BaseExternalSystemParams baseExternalSystemParam, boolean dbPassword) { + if (baseExternalSystemParam == null || baseExternalSystemParam.getSelectInterface() == null) { + throw new IllegalArgumentException("BaseExternalSystemParams or SelectInterface cannot be null"); + } + + BaseExternalSystemParams.InterfaceConfig selectConfig = baseExternalSystemParam.getSelectInterface(); + + // 替换参数占位符 + String url = baseExternalSystemParam.getCompleteUrl(selectConfig.getUrl()); + + Map headeMap = new HashMap<>(); + Map requestBody = new HashMap<>(); + Map requestParams = new HashMap<>(); + String token; + + try { + if (dbPassword) { + // 已保存信息,从数据库中获取,并解密 + BaseExternalSystemParams.AuthConfig authConfig = baseExternalSystemParam.getAuthConfig(); + decodePassword(authConfig); + baseExternalSystemParam.setAuthConfig(authConfig); + token = AuthenticationUtils.authenticateAndGetToken(baseExternalSystemParam); + } else { + if (baseExternalSystemParam.getId() != null) { + DataSource existingSystem = dataSourceMapper.selectById(baseExternalSystemParam.getId()); + if (existingSystem == null) { + // 新建信息测试连接 + token = AuthenticationUtils.authenticateAndGetToken(baseExternalSystemParam); + } else { + // 更新信息测试连接,如果密码没有修改,则使用数据库中保存的密码进行测试连接 + BaseExternalSystemParams oldParams = + JSONUtils.parseObject(existingSystem.getConnectionParams(), + BaseExternalSystemParams.class); + BaseExternalSystemParams.AuthConfig authConfig = baseExternalSystemParam.getAuthConfig(); + if (authConfig.getBasicPassword() != null + && authConfig.getBasicPassword().equals(Constants.XXXXXX)) { + authConfig.setBasicPassword(oldParams.getAuthConfig().getBasicPassword()); + } + if (authConfig.getOauth2ClientSecret() != null + && authConfig.getOauth2ClientSecret().equals(Constants.XXXXXX)) { + authConfig.setOauth2ClientSecret(oldParams.getAuthConfig().getOauth2ClientSecret()); + } + if (authConfig.getOauth2Password() != null + && authConfig.getOauth2Password().equals(Constants.XXXXXX)) { + authConfig.setOauth2Password(oldParams.getAuthConfig().getOauth2Password()); + } + if (authConfig.getJwtToken() != null && authConfig.getJwtToken().equals(Constants.XXXXXX)) { + authConfig.setJwtToken(oldParams.getAuthConfig().getJwtToken()); + } + decodePassword(authConfig); + baseExternalSystemParam.setAuthConfig(authConfig); + token = AuthenticationUtils.authenticateAndGetToken(baseExternalSystemParam); + } + } else { + // 新建信息测试连接 + token = AuthenticationUtils.authenticateAndGetToken(baseExternalSystemParam); + } + } + } catch (Exception e) { + log.error("Authentication failed: {}", e.getMessage()); + throw new ServiceException(Status.EXTERNAL_SYSTEM_CONNECT_AUTH_FAILED); + } + + try { + headeMap.put("Authorization", + baseExternalSystemParam.getTokenPrefix(baseExternalSystemParam.getAuthConfig().getHeaderPrefix()) + + token); + + // 处理参数 + for (BaseExternalSystemParams.RequestParameter param : selectConfig.getParameters()) { + // todo String value = replaceParameterPlaceholders(param.getParamValue()); + String value = param.getParamValue(); + + switch (param.getLocation().name()) { + case "HEADER": + headeMap.put(param.getParamName(), value); + break; + case "PARAM": + requestParams.put(param.getParamName(), value); + break; + } + } + if (selectConfig.getBody() != null) { + requestBody = JSONUtils.parseObject((selectConfig.getBody()), Map.class); + } + OkHttpRequestHeaders headers = new OkHttpRequestHeaders(); + OkHttpRequestHeaderContentType contentType = getContentType(headeMap); + + headers.setOkHttpRequestHeaderContentType(contentType); + if (!headeMap.isEmpty()) { + headers.setHeaders(headeMap); + } + OkHttpResponse response; + if (BaseExternalSystemParams.HttpMethod.POST.equals(selectConfig.getMethod())) { + if (contentType.equals(OkHttpRequestHeaderContentType.APPLICATION_JSON)) { + response = OkHttpUtils.post(url, headers, requestParams, requestBody, 120000, 120000, 120000); + + } else if (contentType.equals(OkHttpRequestHeaderContentType.APPLICATION_FORM_URLENCODED)) { + FormBody.Builder formBodyBuilder = new FormBody.Builder(); + if (requestBody != null) { + for (Map.Entry entry : requestBody.entrySet()) { + formBodyBuilder.add(entry.getKey(), entry.getValue().toString()); + } + } + response = OkHttpUtils.postFormBody(url, headers, requestParams, formBodyBuilder.build(), 120000, + 120000, 120000); + } else { + log.error("select task failed, OkHttpRequestHeaderContentType not support: {},", contentType); + throw new ServiceException(Status.EXTERNAL_SYSTEM_CONNECT_AUTH_FAILED); + } + } else if (BaseExternalSystemParams.HttpMethod.PUT.equals(selectConfig.getMethod())) { + response = OkHttpUtils.put(url, headers, requestBody, 120000, 120000, 120000); + } else { + response = OkHttpUtils.get(url, headers, requestParams, 120000, 120000, 120000); + } + return response; + + } catch (Exception e) { + log.error("select task failed, baseExternalSystemParam: {}, dbPassword: {}", baseExternalSystemParam, + dbPassword, e); + throw new ServiceException(Status.EXTERNAL_SYSTEM_CONNECT_AUTH_FAILED); + } + } + + private OkHttpRequestHeaderContentType getContentType(Map headers) { + String contentType = headers.get("Content-Type"); + if (contentType != null) { + if (contentType.contains("application/json")) { + return OkHttpRequestHeaderContentType.APPLICATION_JSON; + } else if (contentType.contains("application/x-www-form-urlencoded")) { + return OkHttpRequestHeaderContentType.APPLICATION_FORM_URLENCODED; + } + } + return OkHttpRequestHeaderContentType.APPLICATION_JSON; // 默认值 + } + + private void decodePassword(BaseExternalSystemParams.AuthConfig authConfig) { + if (null != authConfig.getOauth2ClientSecret() && !authConfig.getOauth2ClientSecret().isEmpty()) { + authConfig.setOauth2ClientSecret(PasswordUtils.decodePassword(authConfig.getOauth2ClientSecret())); + } + if (null != authConfig.getOauth2Password() && !authConfig.getOauth2Password().isEmpty()) { + authConfig.setOauth2Password(PasswordUtils.decodePassword(authConfig.getOauth2Password())); + } + if (null != authConfig.getJwtToken() && !authConfig.getJwtToken().isEmpty()) { + authConfig.setJwtToken(PasswordUtils.decodePassword(authConfig.getJwtToken())); + } + if (null != authConfig.getBasicPassword() && !authConfig.getBasicPassword().isEmpty()) { + authConfig.setBasicPassword(PasswordUtils.decodePassword(authConfig.getBasicPassword())); + } + } + + /** + * get hidden password (resolve the security hotspot) + * + * @return hidden password + */ + private String getHiddenPassword() { + return Constants.XXXXXX; + } + + @Override + public List queryExternalSystemTasks(User loginUser, int externalSystemId) { + + DataSource dataSource = dataSourceMapper.selectById(externalSystemId); + BaseExternalSystemParams baseExternalSystemParam = + JSONUtils.parseObject(dataSource.getConnectionParams(), BaseExternalSystemParams.class); + + // 校验查询必要 + String taskIdExpression = ""; + String taskNameExpression = ""; + for (BaseExternalSystemParams.ResponseParameter param : baseExternalSystemParam.getSelectInterface() + .getResponseParameters()) { + if (EXTERNAL_TASK_ID.equals(param.getKey())) { + taskIdExpression = param.getJsonPath(); + } + if (EXTERNAL_TASK_NAME.equals(param.getKey())) { + taskNameExpression = param.getJsonPath(); + } + } + if (taskIdExpression.isEmpty() || taskNameExpression.isEmpty()) { + throw new IllegalStateException("External field mapping for 'id' and 'name' not found"); + } + + OkHttpResponse selectResponse = callSelectInterface(baseExternalSystemParam, true); + if (selectResponse.getStatusCode() != ExternalTaskConstants.RESPONSE_CODE_SUCCESS) { + throw new TaskException("Select task failed: " + selectResponse.getBody()); + } + // 解析响应获取id name + return parseSelectResponse(selectResponse.getBody(), taskIdExpression, taskNameExpression); + + } + + private List parseSelectResponse(String responseBody, String taskIdExpression, + String taskNameExpression) throws TaskException { + List resultList = new ArrayList<>(); + + try { + + List idValues = JsonPath.read(responseBody, taskIdExpression); + List nameValues = JsonPath.read(responseBody, taskNameExpression); + + if (idValues.size() != nameValues.size()) { + throw new TaskException("ID and name lists have different sizes"); + } + + // Create tasks + for (int i = 0; i < idValues.size(); i++) { + ExternalSystemTaskQuery task = new ExternalSystemTaskQuery(); + task.setId(idValues.get(i)); + task.setName(nameValues.get(i)); + resultList.add(task); + } + + } catch (Exception e) { + log.error("Parse select response failed", e); + throw new TaskException("Parse select response failed", e); + } + return resultList; + } + +} diff --git a/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/ExternalSystemTaskQuery.java b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/ExternalSystemTaskQuery.java new file mode 100644 index 000000000000..c607d57a16a1 --- /dev/null +++ b/dolphinscheduler-dao/src/main/java/org/apache/dolphinscheduler/dao/entity/ExternalSystemTaskQuery.java @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.dao.entity; + +import lombok.Data; + +@Data +public class ExternalSystemTaskQuery { + + private String id; + + private String name; + +} From edce44e66a8e0e5b2d9267b736aba164da5178b1 Mon Sep 17 00:00:00 2001 From: yud8 Date: Thu, 11 Dec 2025 21:04:08 +0800 Subject: [PATCH 02/11] [Feature-17501]Generic Third-party System API Connector for REST-based Task Scheduling #17501 --- config/plugins_config | 1 + docs/img/tasks/icons/external_system.png | Bin 0 -> 48646 bytes dolphinscheduler-api/pom.xml | 5 + .../dolphinscheduler/api/enums/Status.java | 35 + .../src/main/resources/task-type-config.yaml | 1 + dolphinscheduler-bom/pom.xml | 18 + .../common/constants/Constants.java | 5 + .../common/utils/OkHttpUtils.java | 21 + .../dolphinscheduler-datasource-all/pom.xml | 5 + .../pom.xml | 71 + ...ySystemConnectorAdHocDataSourceClient.java | 32 + ...hirdPartySystemConnectorClientWrapper.java | 44 + ...PartySystemConnectorDataSourceChannel.java | 37 + ...stemConnectorDataSourceChannelFactory.java | 39 + ...SystemConnectorPooledDataSourceClient.java | 32 + .../param/AuthConfig.java | 47 + .../param/AuthMapping.java | 27 + .../param/InterfaceInfo.java | 32 + .../param/PollingFailureConfig.java | 27 + .../param/PollingInterfaceInfo.java | 27 + .../param/PollingSuccessConfig.java | 27 + .../param/RequestParameter.java | 28 + .../param/ResponseParameter.java | 27 + ...rdPartySystemConnectorConnectionParam.java | 42 + ...artySystemConnectorDataSourceParamDTO.java | 47 + ...rtySystemConnectorDataSourceProcessor.java | 301 ++++ dolphinscheduler-datasource-plugin/pom.xml | 1 + .../dolphinscheduler/spi/enums/DbType.java | 3 +- .../dolphinscheduler-task-all/pom.xml | 5 + .../pom.xml | 103 ++ .../externalSystem/AuthenticationUtils.java | 123 ++ .../BaseExternalSystemParams.java | 158 ++ .../ExternalSystemParameters.java | 101 ++ .../externalSystem/ExternalSystemTask.java | 503 +++++++ .../ExternalSystemTaskChannel.java | 38 + .../ExternalSystemTaskChannelFactory.java | 37 + .../externalSystem/ExternalTaskConstants.java | 31 + dolphinscheduler-task-plugin/pom.xml | 1 + .../images/task-icons/external_system.png | Bin 0 -> 48646 bytes .../task-icons/external_system_hover.png | Bin 0 -> 66496 bytes .../src/locales/en_US/index.ts | 2 + .../src/locales/en_US/project.ts | 2 + .../src/locales/en_US/security.ts | 2 + .../locales/en_US/thirdparty-api-source.ts | 139 ++ .../src/locales/zh_CN/index.ts | 2 + .../src/locales/zh_CN/project.ts | 2 + .../src/locales/zh_CN/security.ts | 2 + .../locales/zh_CN/thirdparty-api-source.ts | 140 ++ .../src/service/modules/data-source/index.ts | 21 + .../modules/thirdparty-api-source/index.ts | 100 ++ .../modules/thirdparty-api-source/types.ts | 105 ++ .../src/store/project/task-type.ts | 4 + .../src/store/project/types.ts | 1 + .../src/views/datasource/list/detail.tsx | 783 +++++++++- .../src/views/datasource/list/types.ts | 67 +- .../src/views/datasource/list/use-detail.ts | 147 +- .../src/views/datasource/list/use-form.ts | 448 +++++- .../task/components/node/fields/index.ts | 2 + .../components/node/fields/use-datasource.ts | 9 +- .../node/fields/use-external-system.ts | 128 ++ .../task/components/node/format-data.ts | 4 + .../task/components/node/tasks/index.ts | 2 + .../node/tasks/use-external-system.ts | 73 + .../projects/task/components/node/types.ts | 2 + .../projects/task/constants/task-type.ts | 5 + .../workflow/components/dag/dag.module.scss | 6 + .../workflow/definition/tree/index.tsx | 4 + .../thirdparty-api-source/index.module.scss | 241 +++ .../src/views/thirdparty-api-source/index.tsx | 235 +++ .../src/views/thirdparty-api-source/modal.tsx | 1298 +++++++++++++++++ .../views/thirdparty-api-source/use-table.ts | 72 + 71 files changed, 6105 insertions(+), 25 deletions(-) create mode 100644 docs/img/tasks/icons/external_system.png create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/pom.xml create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/ThirdPartySystemConnectorAdHocDataSourceClient.java create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/ThirdPartySystemConnectorClientWrapper.java create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/ThirdPartySystemConnectorDataSourceChannel.java create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/ThirdPartySystemConnectorDataSourceChannelFactory.java create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/ThirdPartySystemConnectorPooledDataSourceClient.java create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/AuthConfig.java create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/AuthMapping.java create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/InterfaceInfo.java create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/PollingFailureConfig.java create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/PollingInterfaceInfo.java create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/PollingSuccessConfig.java create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/RequestParameter.java create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ResponseParameter.java create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorConnectionParam.java create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorDataSourceParamDTO.java create mode 100644 dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorDataSourceProcessor.java create mode 100644 dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/pom.xml create mode 100644 dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/AuthenticationUtils.java create mode 100644 dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/BaseExternalSystemParams.java create mode 100644 dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemParameters.java create mode 100644 dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTask.java create mode 100644 dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTaskChannel.java create mode 100644 dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTaskChannelFactory.java create mode 100644 dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalTaskConstants.java create mode 100644 dolphinscheduler-ui/public/images/task-icons/external_system.png create mode 100644 dolphinscheduler-ui/public/images/task-icons/external_system_hover.png create mode 100644 dolphinscheduler-ui/src/locales/en_US/thirdparty-api-source.ts create mode 100644 dolphinscheduler-ui/src/locales/zh_CN/thirdparty-api-source.ts create mode 100644 dolphinscheduler-ui/src/service/modules/thirdparty-api-source/index.ts create mode 100644 dolphinscheduler-ui/src/service/modules/thirdparty-api-source/types.ts create mode 100644 dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-external-system.ts create mode 100644 dolphinscheduler-ui/src/views/projects/task/components/node/tasks/use-external-system.ts create mode 100644 dolphinscheduler-ui/src/views/thirdparty-api-source/index.module.scss create mode 100644 dolphinscheduler-ui/src/views/thirdparty-api-source/index.tsx create mode 100644 dolphinscheduler-ui/src/views/thirdparty-api-source/modal.tsx create mode 100644 dolphinscheduler-ui/src/views/thirdparty-api-source/use-table.ts diff --git a/config/plugins_config b/config/plugins_config index 2d77bbfb426c..32832b0f7b53 100644 --- a/config/plugins_config +++ b/config/plugins_config @@ -108,4 +108,5 @@ dolphinscheduler-task-spark dolphinscheduler-task-sql dolphinscheduler-task-sqoop dolphinscheduler-task-zeppelin +dolphinscheduler-task-external-system --end-- diff --git a/docs/img/tasks/icons/external_system.png b/docs/img/tasks/icons/external_system.png new file mode 100644 index 0000000000000000000000000000000000000000..a564ebcc33a49d26a74f78971306879a33e6cc84 GIT binary patch literal 48646 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%0A)VkHWP$t5$na{`6#HZPj2vZ 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: Promise { + return axios({ + url: '/external-systems', + method: 'get', + params + }) +} + +// 新增 +export function createThirdpartyApiSource(data: ThirdpartyApiSource): Promise { + return axios({ + url: '/external-systems/', + method: 'post', + data, + headers: { + 'Content-Type': 'application/json;charset=UTF-8' + }, + transformRequest: (params) => JSON.stringify(params) + }) +} + +// 测试连接 +export function testThirdpartyApiSourceConnection(data: ThirdpartyApiSource): Promise { + return axios({ + url: '/external-systems/test-connection', + method: 'post', + data, + headers: { + 'Content-Type': 'application/json;charset=UTF-8' + }, + transformRequest: (params) => JSON.stringify(params) + }) +} + +// 更新 +export function updateThirdpartyApiSource(id: number, data: ThirdpartyApiSource): Promise { + return axios({ + url: `/external-systems/${id}`, + method: 'put', + data, + headers: { + 'Content-Type': 'application/json;charset=UTF-8' + }, + transformRequest: (params) => JSON.stringify(params) + }) +} + +// 删除 +export function deleteThirdpartyApiSource(id: number): Promise { + return axios({ + url: `/external-systems/${id}`, + method: 'delete' + }) +} + +// 查询详情 +export function getThirdpartyApiSourceById(id: number): Promise { + return axios({ + url: `/external-systems/${id}`, + method: 'get' + }) +} + +// 查询已授权第三方系统 +export function authedThirdpartySystem(params: { userId: number }): any { + return axios({ + url: '/external-systems/authed-externalSystem', + method: 'get', + params + }) +} + +// 查询未授权第三方系统 +export function unAuthThirdpartySystem(params: { userId: number }): any { + return axios({ + url: '/external-systems/unauth-externalSystem', + method: 'get', + params + }) +} + +// 授权第三方系统 +export function grantThirdpartySystem(data: { userId: number, externalSystemIds: string }): any { + const formData = new URLSearchParams() + formData.append('userId', String(data.userId)) + formData.append('externalSystemIds', data.externalSystemIds) + return axios({ + url: '/users/grant-externalSystem', + method: 'post', + data: formData, + headers: { 'Content-Type': 'application/x-www-form-urlencoded' } + }) +} \ No newline at end of file diff --git a/dolphinscheduler-ui/src/service/modules/thirdparty-api-source/types.ts b/dolphinscheduler-ui/src/service/modules/thirdparty-api-source/types.ts new file mode 100644 index 000000000000..58818b439bbe --- /dev/null +++ b/dolphinscheduler-ui/src/service/modules/thirdparty-api-source/types.ts @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +interface ThirdpartyApiSourceReq { + pageNo: number + pageSize: number + searchVal?: string +} + +interface ResponseMapping { + token?: string + taskInstanceId?: string + taskInstanceJobId?: string + type: string + expected_value: string +} + +interface Header { + [key: string]: string +} + +interface Condition { + path: string + expected_value: string +} + +interface Authentication { + url: string + method: string + queryParams: Record + body?: Record + responseMapping: ResponseMapping + headers?: Record[] +} + +interface SubmitTask { + url: string + method: string + queryParams: Record + body?: Record + responseMapping: ResponseMapping +} + +interface Polling { + url: string + method: string + queryParams: Record + successConditions: Condition[] + failureConditions: Condition[] +} + +interface Kill { + url: string + method: string + queryParams: Record + body?: Record + responseMapping: ResponseMapping +} + +interface ThirdpartyApiSource { + id?: number + name: string + authentication: Authentication + submit_task: SubmitTask + polling: Polling + kill: Kill + createTime?: string + updateTime?: string +} + +interface ThirdpartyApiSourceRes { + totalList: ThirdpartyApiSource[] + total: number + totalPage: number + pageSize: number + currentPage: number + start: number +} + +export { + ThirdpartyApiSourceReq, + ThirdpartyApiSource, + ThirdpartyApiSourceRes, + Authentication, + SubmitTask, + Polling, + Kill, + ResponseMapping, + Header, + Condition +} \ No newline at end of file diff --git a/dolphinscheduler-ui/src/store/project/task-type.ts b/dolphinscheduler-ui/src/store/project/task-type.ts index 53340a7d3823..c0d0e88f32a1 100644 --- a/dolphinscheduler-ui/src/store/project/task-type.ts +++ b/dolphinscheduler-ui/src/store/project/task-type.ts @@ -126,6 +126,10 @@ export const TASK_TYPES_MAP = { helperLinkDisable: true, taskExecuteType: 'STREAM' }, + EXTERNAL_SYSTEM : { + alias: 'EXTERNAL_SYSTEM', + helperLinkDisable: true + }, PYTORCH: { alias: 'Pytorch', helperLinkDisable: true diff --git a/dolphinscheduler-ui/src/store/project/types.ts b/dolphinscheduler-ui/src/store/project/types.ts index 849c1ba41912..ce87bc079e4e 100644 --- a/dolphinscheduler-ui/src/store/project/types.ts +++ b/dolphinscheduler-ui/src/store/project/types.ts @@ -49,6 +49,7 @@ type TaskType = | 'SAGEMAKER' | 'CHUNJUN' | 'FLINK_STREAM' + | 'EXTERNAL_SYSTEM' | 'PYTORCH' | 'HIVECLI' | 'DMS' diff --git a/dolphinscheduler-ui/src/views/datasource/list/detail.tsx b/dolphinscheduler-ui/src/views/datasource/list/detail.tsx index 6f12176f7940..2bf4df7753cc 100644 --- a/dolphinscheduler-ui/src/views/datasource/list/detail.tsx +++ b/dolphinscheduler-ui/src/views/datasource/list/detail.tsx @@ -115,6 +115,23 @@ const DetailModal = defineComponent({ )) props.show && props.id && setFieldsValue(await queryById(props.id)) } + ), + + //monitor authType change,update headerPrefix + watch( + () => state.detailForm.authConfig?.authType, + (newAuthType) => { + if (state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && state.detailForm.authConfig) { + if (newAuthType === 'BASIC_AUTH') { + state.detailForm.authConfig.headerPrefix = 'Basic' + } else if (newAuthType === 'JWT' || newAuthType === 'OAUTH2') { + state.detailForm.authConfig.headerPrefix = 'Bearer' + } else { + state.detailForm.authConfig.headerPrefix = '' + } + } + }, + { immediate: true } ) watch( @@ -610,7 +627,8 @@ const DetailModal = defineComponent({ v-show={ (!showMode || detailForm.mode === 'password') && detailForm.type != 'K8S' && - detailForm.type != 'ALIYUN_SERVERLESS_SPARK' + detailForm.type != 'ALIYUN_SERVERLESS_SPARK' && + detailForm.type != 'THIRDPARTY_SYSTEM_CONNECTOR' } label={t('datasource.user_name')} path='userName' @@ -629,7 +647,8 @@ const DetailModal = defineComponent({ v-show={ (!showMode || detailForm.mode === 'password') && detailForm.type != 'K8S' && - detailForm.type != 'ALIYUN_SERVERLESS_SPARK' + detailForm.type != 'ALIYUN_SERVERLESS_SPARK' && + detailForm.type != 'THIRDPARTY_SYSTEM_CONNECTOR' } label={t('datasource.user_password')} path='password' @@ -780,6 +799,766 @@ const DetailModal = defineComponent({ placeholder={t('datasource.namespace_tips')} /> + {/* THIRDPARTY_SYSTEM_CONNECTOR 特殊字段 */} + {detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && ( + <> + + + + + + + + + {{ + suffix: () => t('thirdparty_api_source.millisecond') + }} + + + + + + + + + {detailForm.authConfig.authType === 'BASIC_AUTH' && ( + <> + + + + + + + + )} + {detailForm.authConfig.authType === 'OAUTH2' && ( + <> + + + + + + + + + + + + + + + + + + + + )} + {detailForm.authConfig.authType === 'JWT' && ( + + + + )} + {/* 额外参数 */} + +

+ + +
+ + +
+
+ +
+ {/* 添加按钮 */} + { + if (!detailForm.selectInterface.parameters) { + detailForm.selectInterface.parameters = [] + } + detailForm.selectInterface.parameters.push({ paramName: '', paramValue: '', location: 'HEADER' }) + }} + style={{ marginBottom: '10px' }} + > + {t('thirdparty_api_source.add_param')} + + + {/* 参数列表 */} + {detailForm.selectInterface.parameters && detailForm.selectInterface.parameters.map((param: { paramName: string; paramValue: string; location: string }, index: number) => ( +
+ + + + { + detailForm.selectInterface.parameters.splice(index, 1) + }} + style={{ marginLeft: '10px' }} + > + {t('thirdparty_api_source.delete')} + +
+ ))} +
+
+ {(detailForm.selectInterface.method === 'POST' || detailForm.selectInterface.method === 'PUT') && ( + + + + )} + +
+ {/* 添加按钮 */} + { + if (!detailForm.selectInterface.responseParameters) { + detailForm.selectInterface.responseParameters = [] + } + detailForm.selectInterface.responseParameters.push({ key: '', jsonPath: '', disabled: false }) + }} + style={{ marginBottom: '10px' }} + > + {t('thirdparty_api_source.add_extract_field')} + + + {/* 参数列表 */} + {detailForm.selectInterface.responseParameters && detailForm.selectInterface.responseParameters.map((param: { key: string; jsonPath: string; disabled: boolean }, index: number) => ( +
+ + + { + detailForm.selectInterface.responseParameters.splice(index, 1) + }} + style={{ marginLeft: '10px' }} + > + {t('thirdparty_api_source.delete')} + +
+ ))} +
+
+ +
+ + +
+
+ +
+ {/* 添加按钮 */} + { + if (!detailForm.submitInterface.parameters) { + detailForm.submitInterface.parameters = [] + } + detailForm.submitInterface.parameters.push({ paramName: '', paramValue: '', location: 'HEADER' }) + }} + style={{ marginBottom: '10px' }} + > + {t('thirdparty_api_source.add_param')} + + + {/* 参数列表 */} + {detailForm.submitInterface.parameters && detailForm.submitInterface.parameters.map((param: { paramName: string; paramValue: string; location: string }, index: number) => ( +
+ + + + { + detailForm.submitInterface.parameters.splice(index, 1) + }} + style={{ marginLeft: '10px' }} + > + {t('thirdparty_api_source.delete')} + +
+ ))} +
+
+ {(detailForm.submitInterface.method === 'POST' || detailForm.submitInterface.method === 'PUT') && ( + + + + )} + +
+ {/* 添加按钮 */} + { + if (!detailForm.submitInterface.responseParameters) { + detailForm.submitInterface.responseParameters = [] + } + detailForm.submitInterface.responseParameters.push({ key: '', jsonPath: '', disabled: false }) + }} + style={{ marginBottom: '10px' }} + > + {t('thirdparty_api_source.add_extract_field')} + + + {/* 参数列表 */} + {detailForm.submitInterface.responseParameters && detailForm.submitInterface.responseParameters.map((param: { key: string; jsonPath: string; disabled: boolean }, index: number) => ( +
+ + + { + detailForm.submitInterface.responseParameters.splice(index, 1) + }} + > + {t('thirdparty_api_source.delete')} + +
+ ))} +
+
+ +
+ + +
+
+ +
+ {/* 添加按钮 */} + { + if (!detailForm.pollStatusInterface.parameters) { + detailForm.pollStatusInterface.parameters = [] + } + detailForm.pollStatusInterface.parameters.push({ paramName: '', paramValue: '', location: 'HEADER' }) + }} + style={{ marginBottom: '10px' }} + > + {t('thirdparty_api_source.add_param')} + + + {/* 参数列表 */} + {detailForm.pollStatusInterface.parameters && detailForm.pollStatusInterface.parameters.map((param: { paramName: string; paramValue: string; location: string }, index: number) => ( +
+ + + + { + detailForm.pollStatusInterface.parameters.splice(index, 1) + }} + style={{ marginLeft: '10px' }} + > + {t('thirdparty_api_source.delete')} + +
+ ))} +
+
+ {(detailForm.pollStatusInterface.method === 'POST' || detailForm.pollStatusInterface.method === 'PUT') && ( + + + + )} + +
+ {/* 添加按钮 */} + { + if (!detailForm.pollStatusInterface.responseParameters) { + detailForm.pollStatusInterface.responseParameters = [] + } + detailForm.pollStatusInterface.responseParameters.push({ key: '', jsonPath: '', disabled: false }) + }} + style={{ marginBottom: '10px' }} + > + {t('thirdparty_api_source.add_extract_field')} + + + {/* 参数列表 */} + {detailForm.pollStatusInterface.responseParameters && detailForm.pollStatusInterface.responseParameters.map((param: { key: string; jsonPath: string; disabled: boolean }, index: number) => ( +
+ + + { + detailForm.pollStatusInterface.responseParameters.splice(index, 1) + }} + style={{ marginLeft: '10px' }} + > + {t('thirdparty_api_source.delete')} + +
+ ))} +
+
+ +
+ + +
+
+ +
+ + +
+
+ +
+ + +
+
+ +
+ {/* 添加按钮 */} + { + if (!detailForm.stopInterface.parameters) { + detailForm.stopInterface.parameters = [] + } + detailForm.stopInterface.parameters.push({ paramName: '', paramValue: '', location: 'HEADER' }) + }} + style={{ marginBottom: '10px' }} + > + {t('thirdparty_api_source.add_param')} + + + {/* 参数列表 */} + {detailForm.stopInterface.parameters && detailForm.stopInterface.parameters.map((param: { paramName: string; paramValue: string; location: string }, index: number) => ( +
+ + + + { + detailForm.stopInterface.parameters.splice(index, 1) + }} + style={{ marginLeft: '10px' }} + > + {t('thirdparty_api_source.delete')} + +
+ ))} +
+
+ {(detailForm.stopInterface.method === 'POST' || detailForm.stopInterface.method === 'PUT') && ( + + + + )} + +
+ {/* 添加按钮 */} + { + if (!detailForm.stopInterface.responseParameters) { + detailForm.stopInterface.responseParameters = [] + } + detailForm.stopInterface.responseParameters.push({ key: '', jsonPath: '', disabled: false }) + }} + style={{ marginBottom: '10px' }} + > + {t('thirdparty_api_source.add_extract_field')} + + + {/* 参数列表 */} + {detailForm.stopInterface.responseParameters && detailForm.stopInterface.responseParameters.map((param: { key: string; jsonPath: string; disabled: boolean }, index: number) => ( +
+ + + { + detailForm.stopInterface.responseParameters.splice(index, 1) + }} + style={{ marginLeft: '10px' }} + > + {t('thirdparty_api_source.delete')} + +
+ ))} +
+
+ + )} ), diff --git a/dolphinscheduler-ui/src/views/datasource/list/types.ts b/dolphinscheduler-ui/src/views/datasource/list/types.ts index a3dc8b3c40e9..3ea40399aa6b 100644 --- a/dolphinscheduler-ui/src/views/datasource/list/types.ts +++ b/dolphinscheduler-ui/src/views/datasource/list/types.ts @@ -22,8 +22,73 @@ import type { import type { TableColumns } from 'naive-ui/es/data-table/src/interface' import type { SelectBaseOption } from 'naive-ui/es/select/src/interface' +// THIRDPARTY_SYSTEM_CONNECTOR +interface AuthMapping { + key: string + value: string +} + +interface AuthConfig { + authType: string + basicUsername?: string + basicPassword?: string + jwtToken?: string + oauth2TokenUrl?: string + oauth2ClientId?: string + oauth2ClientSecret?: string + oauth2GrantType?: string + oauth2Username?: string + oauth2Password?: string + headerPrefix?: string + authMappings?: AuthMapping[] +} + +interface InterfaceParameter { + paramName: string + paramValue: string + location: string +} + +interface ResponseParameter { + key: string + jsonPath: string + disabled?: boolean +} + +interface InterfaceConfig { + url: string + method: string + parameters: InterfaceParameter[] + body: string + responseParameters?: ResponseParameter[] +} + +interface PollingSuccessConfig { + successField: string + successValue: string +} + +interface PollingFailureConfig { + failureField: string + failureValue: string +} + +interface PollStatusInterfaceConfig extends InterfaceConfig { + pollingSuccessConfig: PollingSuccessConfig + pollingFailureConfig: PollingFailureConfig +} + interface IDataSourceDetail extends Omit { other?: string + // THIRDPARTY_SYSTEM_CONNECTOR + systemName?: string + serviceAddress?: string + interfaceTimeout?: number + authConfig?: AuthConfig + selectInterface?: InterfaceConfig + submitInterface?: InterfaceConfig + pollStatusInterface?: PollStatusInterfaceConfig + stopInterface?: InterfaceConfig } interface IDataBaseOption extends SelectBaseOption { @@ -44,4 +109,4 @@ export { IDataBaseOption, IDataBaseOptionKeys, TableColumns -} +} \ No newline at end of file diff --git a/dolphinscheduler-ui/src/views/datasource/list/use-detail.ts b/dolphinscheduler-ui/src/views/datasource/list/use-detail.ts index 3f533d00e719..e17924722856 100644 --- a/dolphinscheduler-ui/src/views/datasource/list/use-detail.ts +++ b/dolphinscheduler-ui/src/views/datasource/list/use-detail.ts @@ -38,10 +38,155 @@ export function useDetail(getFieldsValue: Function) { const formatParams = (): IDataSource => { const values = getFieldsValue() - return { + const params: IDataSource = { ...values, other: values.other ? JSON.parse(values.other) : null } + + if (values.type === 'THIRDPARTY_SYSTEM_CONNECTOR') { + if (params.authConfig && params.authConfig.authMappings) { + params.authConfig.authMappings = params.authConfig.authMappings.filter( + (mapping: { key: string; value: string }) => mapping.key || mapping.value + ) + } + + if (!params.selectInterface) { + params.selectInterface = { + url: '', + method: 'GET', + parameters: [], + body: '', + responseParameters: [ + { key: 'id', jsonPath: '' }, + { key: 'name', jsonPath: '' } + ] + } + } else { + if (params.selectInterface.parameters) { + params.selectInterface.parameters = params.selectInterface.parameters.filter( + (param: { paramName: string; paramValue: string }) => param.paramName || param.paramValue + ) + } + + if (params.selectInterface.responseParameters) { + params.selectInterface.responseParameters = params.selectInterface.responseParameters.filter( + (param: { key: string; jsonPath: string }) => param.key || param.jsonPath + ) + } + } + + if (!params.submitInterface) { + params.submitInterface = { + url: '', + method: 'POST', + parameters: [], + body: '', + responseParameters: [ + { key: 'taskInstanceId', jsonPath: '' } + ] + } + } else { + if (params.submitInterface.parameters) { + params.submitInterface.parameters = params.submitInterface.parameters.filter( + (param: { paramName: string; paramValue: string }) => param.paramName || param.paramValue + ) + } + + if (params.submitInterface.responseParameters) { + params.submitInterface.responseParameters = params.submitInterface.responseParameters.filter( + (param: { key: string; jsonPath: string }) => param.key || param.jsonPath + ) + } + } + + if (!params.pollStatusInterface) { + params.pollStatusInterface = { + url: '', + method: 'GET', + parameters: [], + body: '', + pollingSuccessConfig: { + successField: '', + successValue: '' + }, + pollingFailureConfig: { + failureField: '', + failureValue: '' + }, + responseParameters: [] + } + } else { + if (params.pollStatusInterface.parameters) { + params.pollStatusInterface.parameters = params.pollStatusInterface.parameters.filter( + (param: { paramName: string; paramValue: string }) => param.paramName || param.paramValue + ) + } + + if (!params.pollStatusInterface.pollingSuccessConfig) { + params.pollStatusInterface.pollingSuccessConfig = { + successField: '', + successValue: '' + } + } + + if (!params.pollStatusInterface.pollingFailureConfig) { + params.pollStatusInterface.pollingFailureConfig = { + failureField: '', + failureValue: '' + } + } + + if (params.pollStatusInterface.responseParameters) { + params.pollStatusInterface.responseParameters = params.pollStatusInterface.responseParameters.filter( + (param: { key: string; jsonPath: string }) => param.key || param.jsonPath + ) + } + } + + if (!params.stopInterface) { + params.stopInterface = { + url: '', + method: 'POST', + parameters: [], + body: '', + responseParameters: [] + } + } else { + if (params.stopInterface.parameters) { + params.stopInterface.parameters = params.stopInterface.parameters.filter( + (param: { paramName: string; paramValue: string }) => param.paramName || param.paramValue + ) + } + + if (params.stopInterface.responseParameters) { + params.stopInterface.responseParameters = params.stopInterface.responseParameters.filter( + (param: { key: string; jsonPath: string }) => param.key || param.jsonPath + ) + } + } + + if (!params.authConfig) { + params.authConfig = { + authType: 'BASIC_AUTH', + basicUsername: '', + basicPassword: '', + jwtToken: '', + oauth2TokenUrl: '', + oauth2ClientId: '', + oauth2ClientSecret: '', + oauth2GrantType: '', + oauth2Username: '', + oauth2Password: '', + headerPrefix: 'Basic', + authMappings: [] + } + } + + delete params.userName; + delete params.password; + } + + return params } const queryById = async (id: number) => { diff --git a/dolphinscheduler-ui/src/views/datasource/list/use-form.ts b/dolphinscheduler-ui/src/views/datasource/list/use-form.ts index 9701c79381a8..22cea164eca9 100644 --- a/dolphinscheduler-ui/src/views/datasource/list/use-form.ts +++ b/dolphinscheduler-ui/src/views/datasource/list/use-form.ts @@ -51,7 +51,64 @@ export function useForm(id?: number) { endpoint: '', MSIClientId: '', dbUser: '', - datawarehouse: '' + datawarehouse: '', + // THIRDPARTY_SYSTEM_CONNECTOR + systemName: '', + serviceAddress: 'http://', + interfaceTimeout: 120000, + authConfig: { + authType: 'BASIC_AUTH', + basicUsername: '', + basicPassword: '', + jwtToken: '', + oauth2TokenUrl: '', + oauth2ClientId: '', + oauth2ClientSecret: '', + oauth2GrantType: '', + oauth2Username: '', + oauth2Password: '', + headerPrefix: 'Basic', + authMappings: [] as { key: string; value: string }[] + }, + selectInterface: { + url: '', + method: 'GET', + parameters: [], + body: '', + responseParameters: [ + { key: 'id', jsonPath: '' }, + { key: 'name', jsonPath: '' } + ] + }, + submitInterface: { + url: '', + method: 'POST', + parameters: [], + body: '', + responseParameters: [ + { key: 'taskInstanceId', jsonPath: '' } + ] + }, + pollStatusInterface: { + url: '', + method: 'GET', + parameters: [], + body: '', + pollingSuccessConfig: { + successField: '', + successValue: '' + }, + pollingFailureConfig: { + failureField: '', + failureValue: '' + } + }, + stopInterface: { + url: '', + method: 'POST', + parameters: [], + body: '' + } } as IDataSourceDetail const state = reactive({ @@ -68,7 +125,7 @@ export function useForm(id?: number) { showMode: false, showDataBaseName: true, showJDBCConnectParameters: true, - showPrivateKey: false, + showPublicKey: false, showNamespace: false, showKubeConfig: false, showAccessKeyId: false, @@ -126,12 +183,26 @@ export function useForm(id?: number) { !state.detailForm.userName && state.detailForm.type !== 'AZURESQL' && state.detailForm.type !== 'K8S' && - state.detailForm.type !== 'ALIYUN_SERVERLESS_SPARK' + state.detailForm.type !== 'ALIYUN_SERVERLESS_SPARK' && + state.detailForm.type !== 'THIRDPARTY_SYSTEM_CONNECTOR' ) { return new Error(t('datasource.user_name_tips')) } } }, + password: { + trigger: ['input'], + validator() { + if ( + !state.detailForm.password && + state.detailForm.type !== 'K8S' && + state.detailForm.type !== 'ALIYUN_SERVERLESS_SPARK' && + state.detailForm.type !== 'THIRDPARTY_SYSTEM_CONNECTOR' + ) { + return new Error(t('datasource.user_password_tips')) + } + } + }, awsRegion: { trigger: ['input'], validator() { @@ -196,15 +267,167 @@ export function useForm(id?: number) { return new Error(t('datasource.IAM-accessKey')) } } + }, + // THIRDPARTY_SYSTEM_CONNECTOR check rule + systemName: { + trigger: ['input'], + validator() { + if (state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && !state.detailForm.systemName) { + return new Error(t('thirdparty_api_source.system_name_required')) + } + } + }, + serviceAddress: { + trigger: ['input'], + validator() { + if (state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && !state.detailForm.serviceAddress) { + return new Error(t('thirdparty_api_source.service_address_required')) + } + } + }, + 'authConfig.authType': { + trigger: ['change'], + validator() { + if (state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && !state.detailForm.authConfig?.authType) { + return new Error(t('thirdparty_api_source.auth_type_required')) + } + } + }, + 'authConfig.basicUsername': { + trigger: ['blur'], + validator() { + if ( + state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && + state.detailForm.authConfig?.authType === 'BASIC_AUTH' && + !state.detailForm.authConfig?.basicUsername + ) { + return new Error(t('thirdparty_api_source.username_required')) + } + return true + } + }, + 'authConfig.basicPassword': { + trigger: ['blur'], + validator() { + if ( + state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && + state.detailForm.authConfig?.authType === 'BASIC_AUTH' && + !state.detailForm.authConfig?.basicPassword + ) { + return new Error(t('thirdparty_api_source.password_required')) + } + return true + } + }, + 'authConfig.oauth2TokenUrl': { + trigger: ['blur'], + validator() { + if ( + state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && + state.detailForm.authConfig?.authType === 'OAUTH2' && + !state.detailForm.authConfig?.oauth2TokenUrl + ) { + return new Error(t('thirdparty_api_source.oauth2_token_url_required')) + } + return true + } + }, + 'authConfig.oauth2ClientId': { + trigger: ['blur'], + validator() { + if ( + state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && + state.detailForm.authConfig?.authType === 'OAUTH2' && + !state.detailForm.authConfig?.oauth2ClientId + ) { + return new Error(t('thirdparty_api_source.oauth2_client_id_required')) + } + return true + } + }, + 'authConfig.oauth2ClientSecret': { + trigger: ['blur'], + validator() { + if ( + state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && + state.detailForm.authConfig?.authType === 'OAUTH2' && + !state.detailForm.authConfig?.oauth2ClientSecret + ) { + return new Error(t('thirdparty_api_source.oauth2_client_secret_required')) + } + return true + } + }, + 'authConfig.oauth2GrantType': { + trigger: ['blur'], + validator() { + if ( + state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && + state.detailForm.authConfig?.authType === 'OAUTH2' && + !state.detailForm.authConfig?.oauth2GrantType + ) { + return new Error(t('thirdparty_api_source.oauth2_grant_type_required')) + } + return true + } + }, + 'authConfig.jwtToken': { + trigger: ['blur'], + validator() { + if ( + state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && + state.detailForm.authConfig?.authType === 'JWT' && + !state.detailForm.authConfig?.jwtToken + ) { + return new Error(t('thirdparty_api_source.jwt_token_required')) + } + return true + } + }, + 'selectInterface.url': { + trigger: ['blur', 'change'], + validator() { + if ( + state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && + !state.detailForm.selectInterface?.url + ) { + return new Error(t('thirdparty_api_source.input_interface_url_required')) + } + } + }, + 'submitInterface.url': { + trigger: ['blur', 'change'], + validator() { + if ( + state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && + !state.detailForm.submitInterface?.url + ) { + return new Error(t('thirdparty_api_source.submit_interface_url_required')) + } + } + }, + 'pollStatusInterface.url': { + trigger: ['blur', 'change'], + validator() { + if ( + state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && + !state.detailForm.pollStatusInterface?.url + ) { + return new Error(t('thirdparty_api_source.query_interface_url_required')) + } + } + }, + 'stopInterface.url': { + trigger: ['blur', 'change'], + validator() { + if ( + state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && + !state.detailForm.stopInterface?.url + ) { + return new Error(t('thirdparty_api_source.stop_interface_url_required')) + } + } } - // databaseUserName: { - // trigger: ['input'], - // validator() { - // if (!state.detailForm.userName) { - // return new Error(t('datasource.user_name_tips')) - // } - // } - // }, } as FormRules, modeOptions: [ { @@ -277,18 +500,19 @@ export function useForm(id?: number) { type === 'SAGEMAKER' || type === 'K8S' || type === 'ALIYUN_SERVERLESS_SPARK' || - type === 'DOLPHINDB' + type === 'DOLPHINDB' || + type === 'THIRDPARTY_SYSTEM_CONNECTOR' ) { state.showDataBaseName = false state.requiredDataBase = false state.showJDBCConnectParameters = false - state.showPrivateKey = false + state.showPublicKey = false if (type === 'DOLPHINDB') { state.showJDBCConnectParameters = true - state.showPrivateKey = false + state.showPublicKey = false } if (type === 'SSH') { - state.showPrivateKey = true + state.showPublicKey = true } if (type === 'ZEPPELIN') { state.showHost = false @@ -300,7 +524,8 @@ export function useForm(id?: number) { if ( type === 'SAGEMAKER' || type === 'K8S' || - type == 'ALIYUN_SERVERLESS_SPARK' + type == 'ALIYUN_SERVERLESS_SPARK' || + type === 'THIRDPARTY_SYSTEM_CONNECTOR' ) { state.showHost = false state.showPort = false @@ -323,11 +548,57 @@ export function useForm(id?: number) { state.showRegionId = false state.showEndpoint = false } + // 处理THIRDPARTY_SYSTEM_CONNECTOR特殊字段显示 + if (type === 'THIRDPARTY_SYSTEM_CONNECTOR') { + state.showHost = false + state.showPort = false + state.showJDBCConnectParameters = false + state.showDataBaseName = false + state.requiredDataBase = false + + // make sure authConfig \ authMappings inited + if (!state.detailForm.authConfig) { + state.detailForm.authConfig = { + authType: 'BASIC_AUTH', + basicUsername: '', + basicPassword: '', + jwtToken: '', + oauth2TokenUrl: '', + oauth2ClientId: '', + oauth2ClientSecret: '', + oauth2GrantType: '', + oauth2Username: '', + oauth2Password: '', + headerPrefix: 'Basic', + authMappings: [] + } + } else if (!state.detailForm.authConfig.authMappings) { + state.detailForm.authConfig.authMappings = [] + } + } + + // init THIRDPARTY_SYSTEM_CONNECTOR authConfig + if (type === 'THIRDPARTY_SYSTEM_CONNECTOR' && !state.detailForm.authConfig) { + state.detailForm.authConfig = { + authType: 'BASIC_AUTH', + basicUsername: '', + basicPassword: '', + jwtToken: '', + oauth2TokenUrl: '', + oauth2ClientId: '', + oauth2ClientSecret: '', + oauth2GrantType: '', + oauth2Username: '', + oauth2Password: '', + headerPrefix: 'Basic', + authMappings: [] + } + } } else { state.showDataBaseName = true state.requiredDataBase = true state.showJDBCConnectParameters = true - state.showPrivateKey = false + state.showPublicKey = false state.showRestEndpoint = false state.showNamespace = false state.showKubeConfig = false @@ -354,6 +625,142 @@ export function useForm(id?: number) { ...values, other: values.other ? JSON.stringify(values.other) : values.other } + + // check THIRDPARTY_SYSTEM_CONNECTOR has right authConfig + if (values.type === 'THIRDPARTY_SYSTEM_CONNECTOR') { + if (!state.detailForm.authConfig) { + state.detailForm.authConfig = { + authType: 'BASIC_AUTH', + basicUsername: '', + basicPassword: '', + jwtToken: '', + oauth2TokenUrl: '', + oauth2ClientId: '', + oauth2ClientSecret: '', + oauth2GrantType: '', + oauth2Username: '', + oauth2Password: '', + headerPrefix: 'Basic', + authMappings: [] + } + } else { + state.detailForm.authConfig = { + authType: values.authConfig?.authType || 'BASIC_AUTH', + basicUsername: values.authConfig?.basicUsername || '', + basicPassword: values.authConfig?.basicPassword || '', + jwtToken: values.authConfig?.jwtToken || '', + oauth2TokenUrl: values.authConfig?.oauth2TokenUrl || '', + oauth2ClientId: values.authConfig?.oauth2ClientId || '', + oauth2ClientSecret: values.authConfig?.oauth2ClientSecret || '', + oauth2GrantType: values.authConfig?.oauth2GrantType || '', + oauth2Username: values.authConfig?.oauth2Username || '', + oauth2Password: values.authConfig?.oauth2Password || '', + headerPrefix: values.authConfig?.headerPrefix || 'Basic', + authMappings: values.authConfig?.authMappings ? [...values.authConfig.authMappings] : [] + } + } + + if (!state.detailForm.selectInterface) { + state.detailForm.selectInterface = { + url: '', + method: 'GET', + parameters: [], + body: '', + responseParameters: [ + { key: 'id', jsonPath: '' }, + { key: 'name', jsonPath: '' } + ] + } + } else { + state.detailForm.selectInterface = { + url: values.selectInterface?.url || '', + method: values.selectInterface?.method || 'GET', + parameters: values.selectInterface?.parameters ? [...values.selectInterface.parameters] : [], + body: values.selectInterface?.body || '', + responseParameters: values.selectInterface?.responseParameters ? [...values.selectInterface.responseParameters] : [ + { key: 'id', jsonPath: '' }, + { key: 'name', jsonPath: '' } + ] + } + } + + if (!state.detailForm.submitInterface) { + state.detailForm.submitInterface = { + url: '', + method: 'POST', + parameters: [], + body: '', + responseParameters: [ + { key: 'taskInstanceId', jsonPath: '' } + ] + } + } else { + state.detailForm.submitInterface = { + url: values.submitInterface?.url || '', + method: values.submitInterface?.method || 'POST', + parameters: values.submitInterface?.parameters ? [...values.submitInterface.parameters] : [], + body: values.submitInterface?.body || '', + responseParameters: values.submitInterface?.responseParameters ? [...values.submitInterface.responseParameters] : [ + { key: 'taskInstanceId', jsonPath: '' } + ] + } + } + + if (!state.detailForm.pollStatusInterface) { + state.detailForm.pollStatusInterface = { + url: '', + method: 'GET', + parameters: [], + body: '', + pollingSuccessConfig: { + successField: '', + successValue: '' + }, + pollingFailureConfig: { + failureField: '', + failureValue: '' + }, + responseParameters: [] + } + } else { + state.detailForm.pollStatusInterface = { + url: values.pollStatusInterface?.url || '', + method: values.pollStatusInterface?.method || 'GET', + parameters: values.pollStatusInterface?.parameters ? [...values.pollStatusInterface.parameters] : [], + body: values.pollStatusInterface?.body || '', + pollingSuccessConfig: { + successField: values.pollStatusInterface?.pollingSuccessConfig?.successField || '', + successValue: values.pollStatusInterface?.pollingSuccessConfig?.successValue || '' + }, + pollingFailureConfig: { + failureField: values.pollStatusInterface?.pollingFailureConfig?.failureField || '', + failureValue: values.pollStatusInterface?.pollingFailureConfig?.failureValue || '' + }, + responseParameters: values.pollStatusInterface?.responseParameters ? [...values.pollStatusInterface.responseParameters] : [] + } + } + + if (!state.detailForm.stopInterface) { + state.detailForm.stopInterface = { + url: '', + method: 'POST', + parameters: [], + body: '', + responseParameters: [] + } + } else { + state.detailForm.stopInterface = { + url: values.stopInterface?.url || '', + method: values.stopInterface?.method || 'POST', + parameters: values.stopInterface?.parameters ? [...values.stopInterface.parameters] : [], + body: values.stopInterface?.body || '', + responseParameters: values.stopInterface?.responseParameters ? [...values.stopInterface.responseParameters] : [] + } + } + + delete state.detailForm.userName; + delete state.detailForm.password; + } } const getFieldsValue = () => state.detailForm @@ -508,6 +915,11 @@ export const datasourceType: IDataBaseOptionKeys = { value: 'DOLPHINDB', label: 'DOLPHINDB', defaultPort: 8848 + }, + THIRDPARTY_SYSTEM_CONNECTOR: { + value: 'THIRDPARTY_SYSTEM_CONNECTOR', + label: 'THIRDPARTY_SYSTEM_CONNECTOR', + defaultPort: 80 } } @@ -516,4 +928,4 @@ export const datasourceTypeList: IDataBaseOption[] = Object.values( ).map((item) => { item.class = 'options-datasource-type' return item -}) +}) \ No newline at end of file diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts index 7d9e3fe5846e..c13bc5fb4e85 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts @@ -51,6 +51,8 @@ export { useMainJar } from './use-main-jar' export { useResources } from './use-resources' export { useTaskDefinition } from './use-task-definition' export { useJavaTaskMainJar } from './use-java-task-main-jar' +export { useExternalSystem } from './use-external-system' + export { useShell } from './use-shell' export { useSpark } from './use-spark' diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts index 950c7418eaa2..80eee267556d 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts @@ -17,7 +17,7 @@ import { ref, onMounted, nextTick, Ref } from 'vue' import { useI18n } from 'vue-i18n' -import { queryDataSourceList } from '@/service/modules/data-source' +import { queryDataSourceList, queryExternalSystemList, queryExternalSystemTasks } from '@/service/modules/data-source' import { indexOf, find } from 'lodash' import type { IJsonItem } from '../types' import type { TypeReq } from '@/service/modules/data-source/types' @@ -34,7 +34,7 @@ export function useDatasource( const { t } = useI18n() const options = ref([] as { label: string; value: string }[]) - const datasourceOptions = ref([] as { label: string; value: number }[]) + const datasourceOptions = ref([] as { label: string; value: number | string }[]) const datasourceTypes = [ { @@ -166,6 +166,11 @@ export function useDatasource( id: 28, code: 'DOLPHINDB', disabled: false + }, + { + id: 29, + code: 'THIRDPARTY_SYSTEM_CONNECTOR', + disabled: false } ] diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-external-system.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-external-system.ts new file mode 100644 index 000000000000..99e32e7c0600 --- /dev/null +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-external-system.ts @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ref, onMounted, nextTick, Ref } from 'vue' +import { useI18n } from 'vue-i18n' +import { queryDataSourceList, queryExternalSystemTasks } from '@/service/modules/data-source' +import { indexOf, find } from 'lodash' +import type { IJsonItem } from '../types' +import type { TypeReq } from '@/service/modules/data-source/types' + +export function useExternalSystem( + model: { [field: string]: any }, + params: { + externalSystemField?: string + taskField?: string + span?: Ref | number + } = {} +): IJsonItem[] { + const { t } = useI18n() + + const datasourceOptions = ref([] as { label: string; value: string }[]) + const taskOptions = ref([] as { label: string; value: string }[]) + + const getDataSources = async () => { + try { + const parameters = { + type: 'THIRDPARTY_SYSTEM_CONNECTOR' + } as TypeReq + + const res = await queryDataSourceList(parameters) + datasourceOptions.value = res.map((item: any) => ({ + label: item.name, + value: String(item.id) + })) + } catch (error) { + console.error('Error fetching data sources:', error) + } + } + + const refreshTasks = async () => { + const datasourceId = model[params.externalSystemField || 'externalSystemId'] + if (!datasourceId) return + + try { + const res = await queryExternalSystemTasks(datasourceId) + taskOptions.value = res.map((item: any) => ({ + label: item.name, + value: String(item.id) + })) + } catch (error) { + console.error('Error fetching external system tasks:', error) + } + + const taskField = params.taskField || 'task' + if (!taskOptions.value.length && model[taskField]) model[taskField] = null + if (taskOptions.value.length && model[taskField]) { + const item = find(taskOptions.value, { value: model[taskField] }) + if (!item) { + model[taskField] = null + } + } + } + + const onChange = () => { + // 清空任务选项 + taskOptions.value = [] + // 清空模型中的任务字段 + const taskField = params.taskField || 'externalTaskId' + model[taskField] = null + // 刷新任务选项 + refreshTasks() + } + + onMounted(async () => { + await getDataSources() + await nextTick() + refreshTasks() + }) + + return [ + { + type: 'select', + field: params.externalSystemField || 'externalSystemId', + span: params.span || 24, + name: t('project.node.datasource_instances'), + props: { 'on-update:value': onChange }, + options: datasourceOptions, + validate: { + trigger: ['input', 'blur'], + required: true, + validator(unuse: any, value) { + if (!value) { + return new Error(t('project.node.datasource_instances_required')) + } + } + } + }, + { + type: 'select', + field: params.taskField || 'externalTaskId', + span: params.span || 24, + name: t('project.node.external_system_tasks'), + options: taskOptions, + validate: { + trigger: ['input', 'blur'], + required: true, + validator(unuse: any, value) { + if (!value) { + return new Error(t('thirdparty_api_source.external_system_task_required')) + } + } + } + } + ] +} \ No newline at end of file diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/format-data.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/format-data.ts index 5285fff20ff5..682ef9541a12 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/format-data.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/format-data.ts @@ -115,6 +115,10 @@ export function formatParams(data: INodeData): { taskParams.connectTimeout = data.connectTimeout taskParams.socketTimeout = data.socketTimeout } + if (data.taskType === 'EXTERNAL_SYSTEM') { + taskParams.externalSystemId = data.externalSystemId + taskParams.externalTaskId = data.externalTaskId + } if (data.taskType === 'SQOOP') { taskParams.jobType = data.isCustomTask ? 'CUSTOM' : 'TEMPLATE' diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/index.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/index.ts index a57b2dce3025..9be9832647fb 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/index.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/index.ts @@ -31,6 +31,7 @@ import { useSeaTunnel } from './use-sea-tunnel' import { useSwitch } from './use-switch' import { useConditions } from './use-conditions' import { useDataX } from './use-datax' +import { useExternalSystem } from './use-external-system' import { useDependent } from './use-dependent' import { useEmr } from './use-emr' import { useZeppelin } from './use-zeppelin' @@ -81,6 +82,7 @@ export default { SAGEMAKER: userSagemaker, CHUNJUN: useChunjun, FLINK_STREAM: useFlinkStream, + EXTERNAL_SYSTEM: useExternalSystem, JAVA: useJava, PYTORCH: usePytorch, HIVECLI: useHiveCli, diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/use-external-system.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/use-external-system.ts new file mode 100644 index 000000000000..ad1c9a0776f1 --- /dev/null +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/use-external-system.ts @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +import { reactive } from 'vue' +import * as Fields from '../fields/index' +import type { IJsonItem, INodeData, ITaskData } from '../types' + +export function useExternalSystem({ + projectCode, + from = 0, + readonly, + data +}: { + projectCode: number + from?: number + readonly?: boolean + data?: ITaskData +}) { + const model = reactive({ + taskType: 'EXTERNAL_SYSTEM', + name: '', + flag: 'YES', + description: '', + timeoutFlag: false, + timeoutNotifyStrategy: ['WARN'], + timeout: 30, + localParams: [], + environmentCode: null, + failRetryInterval: 1, + failRetryTimes: 0, + workerGroup: 'default', + cpuQuota: -1, + memoryMax: -1, + delayTime: 0, + externalSystemId: '', + externalTaskId: '' + } as INodeData) + + return { + json: [ + Fields.useName(from), + ...Fields.useTaskDefinition({ projectCode, from, readonly, data, model }), + Fields.useRunFlag(), + Fields.useDescription(), + Fields.useTaskPriority(), + Fields.useWorkerGroup(projectCode), + Fields.useEnvironmentName(model, !data?.id), + ...Fields.useTaskGroup(model, projectCode), + ...Fields.useFailed(), + ...Fields.useResourceLimit(), + Fields.useDelayTime(model), + ...Fields.useTimeoutAlarm(model), + ...Fields.useExternalSystem(model), + Fields.usePreTasks() + ] as IJsonItem[], + model + } +} diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/types.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/types.ts index 76dcde23ee2d..bbd15a994c22 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/types.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/types.ts @@ -302,6 +302,8 @@ interface ITaskParams { connectTimeout?: number socketTimeout?: number type?: string + externalSystemId?: string + externalTaskId?: string datasource?: string sql?: string sqlType?: string diff --git a/dolphinscheduler-ui/src/views/projects/task/constants/task-type.ts b/dolphinscheduler-ui/src/views/projects/task/constants/task-type.ts index 3de2acd06e97..51a5552d052e 100644 --- a/dolphinscheduler-ui/src/views/projects/task/constants/task-type.ts +++ b/dolphinscheduler-ui/src/views/projects/task/constants/task-type.ts @@ -52,6 +52,7 @@ export type TaskType = | 'DATA_FACTORY' | 'REMOTESHELL' | 'ALIYUN_SERVERLESS_SPARK' + | 'EXTERNAL_SYSTEM' export type TaskExecuteType = 'STREAM' | 'BATCH' @@ -156,6 +157,10 @@ export const TASK_TYPES_MAP = { helperLinkDisable: true, taskExecuteType: 'STREAM' }, + EXTERNAL_SYSTEM: { + alias: 'EXTERNAL_SYSTEM', + helperLinkDisable: true + }, PYTORCH: { alias: 'Pytorch', helperLinkDisable: true diff --git a/dolphinscheduler-ui/src/views/projects/workflow/components/dag/dag.module.scss b/dolphinscheduler-ui/src/views/projects/workflow/components/dag/dag.module.scss index c23987c58c29..46cc5c585c42 100644 --- a/dolphinscheduler-ui/src/views/projects/workflow/components/dag/dag.module.scss +++ b/dolphinscheduler-ui/src/views/projects/workflow/components/dag/dag.module.scss @@ -117,6 +117,9 @@ $bgLight: #ffffff; &.icon-flink_stream { background-image: url('/images/task-icons/flink.png'); } + &.icon-external_system { + background-image: url('/images/task-icons/external_system.png'); + } &.icon-mr { background-image: url('/images/task-icons/mr.png'); } @@ -230,6 +233,9 @@ $bgLight: #ffffff; &.icon-flink_stream { background-image: url('/images/task-icons/flink_hover.png'); } + &.icon-external_stream { + background-image: url('/images/task-icons/external_stream_hover.png'); + } &.icon-mr { background-image: url('/images/task-icons/mr_hover.png'); } diff --git a/dolphinscheduler-ui/src/views/projects/workflow/definition/tree/index.tsx b/dolphinscheduler-ui/src/views/projects/workflow/definition/tree/index.tsx index 26d6540eb6b3..7098e2a8b679 100644 --- a/dolphinscheduler-ui/src/views/projects/workflow/definition/tree/index.tsx +++ b/dolphinscheduler-ui/src/views/projects/workflow/definition/tree/index.tsx @@ -157,6 +157,10 @@ export default defineComponent({ taskType: 'FLINK_STREAM', color: '#d68f5b', image: `${import.meta.env.BASE_URL}images/task-icons/flink.png` + },{ + taskType: 'EXTERNAL_SYSTEM', + color: '#d68f5b', + image: `${import.meta.env.BASE_URL}images/task-icons/external_system.png` } ]) diff --git a/dolphinscheduler-ui/src/views/thirdparty-api-source/index.module.scss b/dolphinscheduler-ui/src/views/thirdparty-api-source/index.module.scss new file mode 100644 index 000000000000..4879d138674a --- /dev/null +++ b/dolphinscheduler-ui/src/views/thirdparty-api-source/index.module.scss @@ -0,0 +1,241 @@ +// 模态框基础样式 +.thirdparty-modal { + position: relative; + width: 700px; + max-height: 100vh; + display: flex; + flex-direction: column; + overflow: hidden; +} + +// 隐藏模态框关闭按钮 +.thirdparty-modal :global(.n-modal__close), +.thirdparty-modal :global(.n-modal-header__close), +.thirdparty-modal :global(.n-modal-header .n-button), +.thirdparty-modal :global(.n-modal-header .n-icon), +.thirdparty-modal :global(.n-modal-header .n-base-icon), +.thirdparty-modal :global(.n-modal-header .n-button--icon-only), +.thirdparty-modal :global(.n-base-close), +.thirdparty-modal :global(.n-base-close--absolute), +.thirdparty-modal :global(.n-card-header__close) { + display: none !important; +} + +// 模态框内容区域 +.modal-content { + flex: 1; + max-height: calc(90vh - 120px); + overflow-y: auto; + overflow-x: hidden; + width: 100%; + padding: 0 16px; + box-sizing: border-box; +} + +// 表单基础样式 +.modal-content :global(.n-form) { + width: 100%; + box-sizing: border-box; +} + +.modal-content :global(.n-form-item) { + width: 100%; + margin-bottom: 16px; + box-sizing: border-box; +} + +.modal-content :global(.n-form-item__content) { + display: flex; + align-items: flex-start; + flex-wrap: wrap; + gap: 8px; + width: 100%; + box-sizing: border-box; +} + +.modal-content :global(.n-form-item__label) { + min-width: 120px; + text-align: right; + padding-right: 12px; + box-sizing: border-box; +} + +// 输入框和选择器基础样式 +.modal-content :global(.n-input), +.modal-content :global(.n-select) { + box-sizing: border-box; +} + +// 空间组件 +.modal-content :global(.n-space) { + flex-wrap: wrap; + gap: 8px; + width: 100%; + box-sizing: border-box; +} + +// 动态输入组件 +.modal-content :global(.n-dynamic-input) { + width: 100%; + box-sizing: border-box; +} + +.modal-content :global(.n-dynamic-input .n-dynamic-input-item) { + width: 100%; + margin-bottom: 12px; + display: flex; + align-items: center; + box-sizing: border-box; +} + +.modal-content :global(.n-dynamic-input .n-dynamic-input-item .n-space) { + width: 100%; + justify-content: flex-start; + flex: 1; + box-sizing: border-box; +} + +.modal-content :global(.n-dynamic-input .n-dynamic-input-item .n-button) { + margin-left: 8px; + flex-shrink: 0; + box-sizing: border-box; +} + +.modal-content :global(.n-dynamic-input .n-dynamic-input-item .n-input) { + flex: 1; + min-width: 0; + box-sizing: border-box; +} + +.modal-content :global(.n-dynamic-input .n-dynamic-input-item .n-select) { + flex-shrink: 0; + box-sizing: border-box; +} + +// 字段映射特殊样式 +.modal-content :global(.n-dynamic-input .n-dynamic-input-item .n-space .n-input) { + flex: 1; + width: 50%; + min-width: 200px; + box-sizing: border-box; +} + +// 参数输入框样式 +.param-location { + width: 100px; + min-width: 100px; +} + +.param-name { + width: 150px; + min-width: 150px; +} + +.param-value { + width: 180px; + min-width: 180px; +} + + + +// 选择器样式 +.method-select, +.auth-type-select { + width: 120px; + min-width: 120px; +} + +// 条件输入框样式 +.condition-field { + width: calc(50% - 4px); + min-width: 200px; +} + +.condition-value { + width: calc(50% - 4px); + min-width: 200px; + margin-left: 8px; +} + +// 提交接口样式 +.submit-url { + flex: 1; + min-width: 300px; +} + +.submit-method { + width: 120px; + min-width: 120px; +} + +// 超时时间输入框样式 +.timeout-input { + width: 200px; +} + +.timeout-description { + font-size: 12px; + color: #999; + margin-top: 4px; + line-height: 1.4; +} + +// 键值对输入框样式 +.key-input, +.value-input { + width: 200px; + min-width: 150px; +} + +// Monaco编辑器样式 +.modal-content :global(.monaco-editor) { + height: 200px !important; + min-height: 200px !important; +} + +.modal-content :global(.monaco-editor .monaco-editor-background) { + background-color: #fafafa !important; +} + +// 分割线样式 +.modal-content :global(.n-divider) { + margin: 20px 0; + border-color: #f0f0f0; +} + +// 底部按钮区域 +.modal-footer { + position: sticky; + bottom: 0; + background: #fff; + padding-top: 16px; + z-index: 10; + display: flex; + justify-content: flex-end; + border-top: 1px solid #f0f0f0; + margin-top: 16px; + box-sizing: border-box; + width: 100%; +} + +.modal-footer :global(.n-space) { + margin-right: 15px; + gap: 8px; +} + +.modal-footer :global(.n-button) { + min-width: 80px; + height: 32px; + padding: 0 14px; +} + +// 响应式设计 +@media screen and (min-width: 1920px) { + .thirdparty-modal { + width: 700px; + } + + .modal-content { + max-height: calc(90vh - 120px); + } +} \ No newline at end of file diff --git a/dolphinscheduler-ui/src/views/thirdparty-api-source/index.tsx b/dolphinscheduler-ui/src/views/thirdparty-api-source/index.tsx new file mode 100644 index 000000000000..67be57bf18f5 --- /dev/null +++ b/dolphinscheduler-ui/src/views/thirdparty-api-source/index.tsx @@ -0,0 +1,235 @@ +import { defineComponent, ref, onMounted, computed } from 'vue' +import { + NDataTable, + NButton, + NSpace, + NPopconfirm, + NIcon, + NPagination, + NTooltip +} from 'naive-ui' +import { useI18n } from 'vue-i18n' +import { useRouter } from 'vue-router' +import { deleteThirdpartyApiSource, queryThirdpartyApiSourceListPaging, createThirdpartyApiSource, updateThirdpartyApiSource, getThirdpartyApiSourceById, testThirdpartyApiSourceConnection } from '@/service/modules/thirdparty-api-source' +import type { ThirdpartyApiSource } from '@/service/modules/thirdparty-api-source/types' +import Card from '@/components/card' +import Search from '@/components/input-search' +import { EditOutlined, DeleteOutlined } from '@vicons/antd' +import ThirdpartyApiSourceModal from './modal' +import { de } from 'date-fns/locale' + +export default defineComponent({ + name: 'ThirdpartyApiSourceList', + setup() { + const { t } = useI18n() + const router = useRouter() + + const tableData = ref([]) + const loading = ref(false) + const searchVal = ref('') + const page = ref(1) + const pageSize = ref(10) + const itemCount = ref(0) + const showModal = ref(false) + const editData = ref(null) + const operationType = ref<'create' | 'edit'>('create') + + // 获取真实接口数据 + const getTableData = async () => { + loading.value = true + try { + const res = await queryThirdpartyApiSourceListPaging({ + pageNo: page.value, + pageSize: pageSize.value, + searchVal: searchVal.value || undefined + }) as any + if(res) { + tableData.value = (res.totalList || []) as ThirdpartyApiSource[] + itemCount.value = res.total || 0 + } + } finally { + loading.value = false + } + } + + const handleDelete = async (row: ThirdpartyApiSource) => { + await deleteThirdpartyApiSource(row.id!) + await getTableData() + } + + const changePage = (p: number) => { + page.value = p + getTableData() + } + const changePageSize = (ps: number) => { + page.value = 1 + pageSize.value = ps + getTableData() + } + + const handleCreate = () => { + editData.value = null + operationType.value = 'create' + showModal.value = true + } + const handleEdit = async (row: any) => { + // 获取详情 + const detail = await getThirdpartyApiSourceById(row.id) + editData.value = detail + operationType.value = 'edit' + showModal.value = true + } + const handleModalClose = () => { + showModal.value = false + editData.value = null + operationType.value = 'create' + } + const handleModalSubmit = async (data: any) => { + const res = data.id ? await updateThirdpartyApiSource(data.id, data) : await createThirdpartyApiSource(data) + if(res) { + window.$message.success(data.id ? t('message.edit.success') : t('message.create.success')) + } else { + window.$message.error(data.id ? t('message.edit.failed') : t('message.create.failed')) + } + showModal.value = false + editData.value = null + getTableData() + } + const handleModalTest = async (data: any) => { + try { + const res = await testThirdpartyApiSourceConnection(data) + window.$message.success( + res && res.msg + ? res.msg + : `${t('datasource.test_connect')} ${t('datasource.success')}` + ) + } catch (e: any) { + console.log(e) + } + } + + const columns = computed(() => [ + { + title: t('thirdparty_api_source.id'), + key: 'id' + }, + { + title: t('thirdparty_api_source.system_name'), + key: 'name' + }, + { + title: t('thirdparty_api_source.create_time'), + key: 'createTime', + render: (row: any) => row.createTime ? row.createTime : '-' + }, + { + title: t('thirdparty_api_source.update_time'), + key: 'updateTime', + render: (row: any) => row.updateTime ? row.updateTime : '-' + }, + { + title: t('datasource.operation'), + key: 'actions', + render: (row: ThirdpartyApiSource) => { + return ( + + + {{ + trigger: () => ( + handleEdit(row)} + > + + + ), + default: () => t('thirdparty_api_source.edit') + }} + + + {{ + trigger: () => ( + handleDelete(row)}> + {{ + trigger: () => ( + + + + ), + default: () => t('datasource.delete_confirm') + }} + + ), + default: () => t('thirdparty_api_source.delete') + }} + + + ) + } + } + ]) + + onMounted(() => { + getTableData() + }) + + return () => ( + + + + + {t('thirdparty_api_source.create_thirdparty_api_source')} + + + + + {t('thirdparty_api_source.search')} + + + + + + + + + + + + + + + ) + } +}) \ No newline at end of file diff --git a/dolphinscheduler-ui/src/views/thirdparty-api-source/modal.tsx b/dolphinscheduler-ui/src/views/thirdparty-api-source/modal.tsx new file mode 100644 index 000000000000..7f97fe6b1a1b --- /dev/null +++ b/dolphinscheduler-ui/src/views/thirdparty-api-source/modal.tsx @@ -0,0 +1,1298 @@ +import { defineComponent, PropType, reactive, watch, computed, ref } from 'vue' +import type { FormInst } from 'naive-ui' +import { useI18n } from 'vue-i18n' +import styles from './index.module.scss' +import { + NModal, + NForm, + NFormItem, + NInput, + NInputNumber, + NButton, + NSpace, + NDivider, + NDynamicInput, + NSelect, + NTooltip, + NIcon +} from 'naive-ui' +import MonacoEditor from '@/components/monaco-editor' +import { InfoCircleOutlined } from '@vicons/antd'; + +export default defineComponent({ + name: 'ThirdpartyApiSourceModal', + props: { + show: Boolean, + data: Object as PropType, + operationType: { + type: String as PropType<'create' | 'edit'>, + default: 'create' + } + }, + emits: ['close', 'submit', 'test'], + setup(props, { emit }) { + const { t } = useI18n() + + const authTypeOptions = computed(() => [ + { label: t('thirdparty_api_source.basic_auth'), value: 'BASIC_AUTH' }, + { label: t('thirdparty_api_source.oauth2'), value: 'OAUTH2' }, + { label: t('thirdparty_api_source.jwt'), value: 'JWT' } + ]) + + const methodOptions = computed(() => [ + { label: t('thirdparty_api_source.get'), value: 'GET' }, + { label: t('thirdparty_api_source.post'), value: 'POST' }, + { label: t('thirdparty_api_source.put'), value: 'PUT' } + ]) + + const form = reactive({ + systemName: '', + serviceAddress: '', + interfaceTimeout: 120000, // 默认2分钟 + authConfig: { + authType: 'BASIC_AUTH', + basicUsername: '', + basicPassword: '', + jwtToken: '', + oauth2TokenUrl: '', + oauth2ClientId: '', + oauth2ClientSecret: '', + oauth2GrantType: '', + oauth2Username: '', + oauth2Password: '', + headerPrefix: 'Basic', + authMappings: [] as any[] + }, + selectInterface: { + url: '', + method: 'GET', + parameters: [] as any[], + body: '', + responseParameters: [ + { key: 'id', jsonPath: '' }, + { key: 'name', jsonPath: '' } + ] + }, + submitInterface: { + url: '', + method: 'POST', + parameters: [] as any[], + body: '', + responseParameters: [ + { key: 'taskInstanceId', jsonPath: '' } + ] + }, + pollStatusInterface: { + url: '', + method: 'GET', + parameters: [] as any[], + body: '', + pollingSuccessConfig: { + successField: '', + successValue: '' + }, + pollingFailureConfig: { + failureField: '', + failureValue: '' + } + }, + stopInterface: { + url: '', + method: 'POST', + parameters: [] as any[], + body: '' + } + }) + + // 表单校验规则 + const rules = { + systemName: [ + { required: true, message: t('thirdparty_api_source.system_name_required'), trigger: 'blur' } + ], + serviceAddress: [ + { required: true, message: t('thirdparty_api_source.service_address_required'), trigger: 'blur' } + ], + 'authConfig.authType': [ + { required: true, message: t('thirdparty_api_source.auth_type_required'), trigger: 'change' } + ], + 'authConfig.basicUsername': [ + { + validator: (rule: any, value: any) => { + if (form.authConfig.authType === 'BASIC_AUTH' && !value) { + return new Error(t('thirdparty_api_source.username_required')) + } + return true + }, + trigger: 'blur' + } + ], + 'authConfig.basicPassword': [ + { + validator: (rule: any, value: any) => { + if (form.authConfig.authType === 'BASIC_AUTH' && !value) { + return new Error(t('thirdparty_api_source.password_required')) + } + return true + }, + trigger: 'blur' + } + ], + 'authConfig.oauth2TokenUrl': [ + { + validator: (rule: any, value: any) => { + if (form.authConfig.authType === 'OAUTH2' && !value) { + return new Error(t('thirdparty_api_source.oauth2_token_url_required')) + } + return true + }, + trigger: 'blur' + } + ], + 'authConfig.oauth2ClientId': [ + { + validator: (rule: any, value: any) => { + if (form.authConfig.authType === 'OAUTH2' && !value) { + return new Error(t('thirdparty_api_source.oauth2_client_id_required')) + } + return true + }, + trigger: 'blur' + } + ], + 'authConfig.oauth2ClientSecret': [ + { + validator: (rule: any, value: any) => { + if (form.authConfig.authType === 'OAUTH2' && !value) { + return new Error(t('thirdparty_api_source.oauth2_client_secret_required')) + } + return true + }, + trigger: 'blur' + } + ], + 'authConfig.oauth2GrantType': [ + { + validator: (rule: any, value: any) => { + if (form.authConfig.authType === 'OAUTH2' && !value) { + return new Error(t('thirdparty_api_source.oauth2_grant_type_required')) + } + return true + }, + trigger: 'blur' + } + ], + 'authConfig.jwtToken': [ + { + validator: (rule: any, value: any) => { + if (form.authConfig.authType === 'JWT' && !value) { + return new Error(t('thirdparty_api_source.jwt_token_required')) + } + return true + }, + trigger: 'blur' + } + ], + 'selectInterface.url': [ + { required: true, message: t('thirdparty_api_source.input_interface_url_required'), trigger: ['blur', 'change'] } + ], + 'submitInterface.url': [ + { required: true, message: t('thirdparty_api_source.submit_interface_url_required'), trigger: ['blur', 'change'] } + ], + 'pollStatusInterface.url': [ + { required: true, message: t('thirdparty_api_source.query_interface_url_required'), trigger: ['blur', 'change'] } + ], + 'stopInterface.url': [ + { required: true, message: t('thirdparty_api_source.stop_interface_url_required'), trigger: ['blur', 'change'] } + ], + 'pollStatusInterface.pollingSuccessConfig': [ + { + validator: (rule: any, value: any) => { + if (!value.successField || !value.successValue) { + return new Error(t('thirdparty_api_source.success_condition_required')) + } + return true + }, + trigger: ['blur', 'change'] + } + ], + 'pollStatusInterface.pollingFailureConfig': [ + { + validator: (rule: any, value: any) => { + if (!value.failureField || !value.failureValue) { + return new Error(t('thirdparty_api_source.failure_condition_required')) + } + return true + }, + trigger: ['blur', 'change'] + } + ], + 'selectInterface.responseParameters': [ + { + validator: (rule: any, value: any) => { + const idField = value.find((item: any) => item.key === 'id') + const nameField = value.find((item: any) => item.key === 'name') + + if (!idField || !idField.jsonPath) { + return new Error(t('thirdparty_api_source.id_jsonpath_required')) + } + if (!nameField || !nameField.jsonPath) { + return new Error(t('thirdparty_api_source.name_jsonpath_required')) + } + return true + }, + trigger: ['blur', 'change'] + } + ] + } + + const formRef = ref(null) + const isEditMode = computed(() => props.operationType === 'edit') + + // 定义表单的初始状态 + const getInitialFormState = () => ({ + systemName: '', + serviceAddress: 'http://', + interfaceTimeout: 120000, // 默认2分钟 + authConfig: { + authType: '', + headerPrefix: '', + basicUsername: '', + basicPassword: '', + jwtToken: '', + oauth2TokenUrl: '', + oauth2ClientId: '', + oauth2ClientSecret: '', + oauth2GrantType: '', + oauth2Username: '', + oauth2Password: '', + authMappings: [] + }, + selectInterface: { + url: '', + method: 'GET', + parameters: [] as any[], + body: '', + responseParameters: [ + { key: 'id', jsonPath: '' }, + { key: 'name', jsonPath: '' } + ] + }, + submitInterface: { + url: '', + method: 'POST', + parameters: [] as any[], + body: '', + responseParameters: [ + { key: 'taskInstanceId', jsonPath: '' } + ] + }, + pollStatusInterface: { + url: '', + method: 'GET', + parameters: [] as any[], + body: '', + pollingSuccessConfig: { successField: '', successValue: '' }, + pollingFailureConfig: { failureField: '', failureValue: '' } + }, + stopInterface: { url: '', method: 'POST', parameters: [] as any[], body: '' } + }) + + // 重置表单数据的函数 + const resetForm = () => { + const initialState = getInitialFormState() + Object.keys(form).forEach(key => { + delete (form as any)[key] + }) + Object.assign(form, initialState) + formRef.value?.restoreValidation?.() + } + + // 保存原始编辑数据,用于测试连接 + const originalEditData = ref(null) + // 监听modal显示状态和数据变化 + watch([() => props.show, () => props.data, () => props.operationType], ([show, data, operationType]) => { + if (show) { + if (data && operationType === 'edit') { + originalEditData.value = JSON.parse(JSON.stringify(data)) + resetForm() + const editData = originalEditData.value + // 完全使用后端返回的数据 + Object.assign(form, editData) + } else { + originalEditData.value = null + resetForm() + // 只在创建模式下设置默认值 + form.authConfig.authType = 'BASIC_AUTH' + form.authConfig.headerPrefix = 'Basic' + } + } + }, { immediate: true }) + + watch(() => form.authConfig.authType, (newAuthType) => { + // 只在创建模式下自动设置headerPrefix + if (!isEditMode.value) { + if (newAuthType === 'BASIC_AUTH') { + form.authConfig.headerPrefix = 'Basic' + } else if (newAuthType === 'JWT' || newAuthType === 'OAUTH2') { + form.authConfig.headerPrefix = 'Bearer' + } else { + form.authConfig.headerPrefix = '' + } + } + }) + + const handleClose = () => { + resetForm() + emit('close') + } + + const handleSubmit = () => { + (formRef.value as any)?.validate((errors: any) => { + if (!errors) { + if (isEditMode.value && originalEditData.value) { + const submitData = JSON.parse(JSON.stringify(originalEditData.value)) + const initialState = getInitialFormState() + Object.keys(initialState).forEach(key => { + if (form.hasOwnProperty(key)) { + submitData[key] = (form as any)[key] + } + }) + emit('submit', submitData) + } else { + emit('submit', JSON.parse(JSON.stringify(form))) + } + } + }) + } + + const handleTest = () => { + (formRef.value as any)?.validate((errors: any) => { + if (!errors) { + if (isEditMode.value && originalEditData.value) { + const testData = JSON.parse(JSON.stringify(originalEditData.value)) + const initialState = getInitialFormState() + Object.keys(initialState).forEach(key => { + if (form.hasOwnProperty(key)) { + testData[key] = (form as any)[key] + } + }) + emit('test', testData) + } else { + emit('test', JSON.parse(JSON.stringify(form))) + } + } + }) + } + + // location 下拉选项与 method 联动 + const getLocationOptions = (method: string) => { + return [ + { label: 'Header', value: 'HEADER' }, + { label: 'Param', value: 'PARAM' } + ] + } + + return () => ( + +
+ + + + + + + + + + {/* 接口超时时间 */} + + + {{ + suffix: () => t('thirdparty_api_source.millisecond') + }} + +
+ {t('thirdparty_api_source.interface_timeout_description')} +
+
+ + + + {/* 认证类型 */} + + {{ + label: () => ( + + {t('thirdparty_api_source.auth_type')} + + {{ + trigger: () => ( + + + + ), + default: () => t('thirdparty_api_source.auth_type_detail_info') + }} + + + ), + default: () => ( + + ) + }} + + + + + + + {/* BASIC_AUTH */} + + + + + + + + {/* OAUTH2 */} + + {{ + label: () => ( + + {t('thirdparty_api_source.oauth2_token_url')} + + {{ + trigger: () => ( + + + + ), + default: () => t('thirdparty_api_source.oauth2_url_info') + }} + + + ), + default: () => ( + + ) + }} + + + + + + + + + + + + + + + + + + + {/* JWT */} + + + + + {/* 额外参数 */} + + ({ key: '', value: '' })} + style={{ width: '100%' }} + > + {{ + default: ({ value }: { value: { key: string; value: string } }) => ( + + + + + ) + }} + + + + + + {/* 输入接口 */} + + {{ + label: () => ( + + {t('thirdparty_api_source.input_interface')} + + {{ + trigger: () => ( + + + + ), + default: () => t('thirdparty_api_source.input_interface_detail_info') + }} + + + ), + default: () => ( + <> + formRef.value?.validate?.()} + /> + + + ) + }} + + + + ({ paramName: '', paramValue: '', location: 'HEADER' })} + style={{ width: '100%' }} + > + {{ + default: ({ + value + }: { + value: { paramName: string; paramValue: string; location: string } + }) => ( + + + + + + ) + }} + + + + {(form.selectInterface.method === 'POST' || form.selectInterface.method === 'PUT') && ( + + {{ + label: () => ( + + {t('thirdparty_api_source.request_body')} + + {{ + trigger: () => ( + + + + ), + default: () => t('thirdparty_api_source.input_interface_body_info') + }} + + + ), + default: () => ( + + ) + }} + + )} + + + {{ + label: () => ( + + {t('thirdparty_api_source.extract_response_data')} + + {{ + trigger: () => ( + + + + ), + default: () => t('thirdparty_api_source.input_interface_extract_info') + }} + + + ), + default: () => ( + ({ key: '', jsonPath: '', disabled: false })} + style={{ width: '100%' }} + > + {{ + default: ({ + value + }: { + value: { key: string; jsonPath: string; disabled: boolean } + }) => ( + + + + + ) + }} + + ) + }} + + + + + {/* 提交接口 */} + + {{ + label: () => ( + + {t('thirdparty_api_source.submit_interface')} + + {{ + trigger: () => ( + + + + ), + default: () => t('thirdparty_api_source.submit_interface_detail_info') + }} + + + ), + default: () => ( + <> + formRef.value?.validate?.()} + /> + + + ) + }} + + + + ({ paramName: '', paramValue: '', location: 'HEADER' })} + style={{ width: '100%' }} + > + {{ + default: ({ + value + }: { + value: { paramName: string; paramValue: string; location: string } + }) => ( + + + + + + ) + }} + + + + {(form.submitInterface.method === 'POST' || form.submitInterface.method === 'PUT') && ( + + {{ + label: () => ( + + {t('thirdparty_api_source.request_body')} + + {{ + trigger: () => ( + + + + ), + default: () => t('thirdparty_api_source.submit_interface_body_info') + }} + + + ), + default: () => ( + + ) + }} + + )} + + + {{ + label: () => ( + + {t('thirdparty_api_source.extract_response_data')} + + {{ + trigger: () => ( + + + + ), + default: () => t('thirdparty_api_source.submit_interface_extract_info') + }} + + + ), + default: () => ( + ({ key: '', jsonPath: '', disabled: false })} + style={{ width: '100%' }} + > + {{ + default: ({ + value + }: { + value: { key: string; jsonPath: string; disabled: boolean } + }) => ( + + + + + ) + }} + + ) + }} + + + + + {/* 查询接口 */} + + {{ + label: () => ( + + {t('thirdparty_api_source.query_interface')} + + {{ + trigger: () => ( + + + + ), + default: () => t('thirdparty_api_source.query_interface_detail_info') + }} + + + ), + default: () => ( + <> + formRef.value?.validate?.()} + /> + + + ) + }} + + + + ({ paramName: '', paramValue: '', location: 'HEADER' })} + style={{ width: '100%' }} + > + {{ + default: ({ + value + }: { + value: { paramName: string; paramValue: string; location: string } + }) => ( + + + + + + ) + }} + + + + {(form.pollStatusInterface.method === 'POST' || + form.pollStatusInterface.method === 'PUT') && ( + + {{ + label: () => ( + + {t('thirdparty_api_source.request_body')} + + {{ + trigger: () => ( + + + + ), + default: () => t('thirdparty_api_source.query_interface_body_info') + }} + + + ), + default: () => ( + + ) + }} + + )} + + + {{ + label: () => ( + + {t('thirdparty_api_source.success_condition')} + + {{ + trigger: () => ( + + + + ), + default: () => t('thirdparty_api_source.query_interface_success_info') + }} + + + ), + default: () => ( + <> + formRef.value?.validate?.()} + /> + formRef.value?.validate?.()} + /> + + ) + }} + + + + {{ + label: () => ( + + {t('thirdparty_api_source.failure_condition')} + + {{ + trigger: () => ( + + + + ), + default: () => t('thirdparty_api_source.query_interface_failed_info') + }} + + + ), + default: () => ( + <> + formRef.value?.validate?.()} + /> + formRef.value?.validate?.()} + /> + + ) + }} + + + + + {/* 停止接口 */} + + {{ + label: () => ( + + {t('thirdparty_api_source.stop_interface')} + + {{ + trigger: () => ( + + + + ), + default: () => t('thirdparty_api_source.stop_interface_detail_info') + }} + + + ), + default: () => ( + <> + formRef.value?.validate?.()} + /> + + + ) + }} + + + + ({ paramName: '', paramValue: '', location: 'HEADER' })} + style={{ width: '100%' }} + > + {{ + default: ({ + value + }: { + value: { paramName: string; paramValue: string; location: string } + }) => ( + + + + + + ) + }} + + + + {(form.stopInterface.method === 'POST' || + form.stopInterface.method === 'PUT') && ( + + {{ + label: () => ( + + {t('thirdparty_api_source.request_body')} + + {{ + trigger: () => ( + + + + ), + default: () => t('thirdparty_api_source.stop_interface_body_info') + }} + + + ), + default: () => ( + + ) + }} + + )} +
+
+ +
+ + + {t('thirdparty_api_source.cancel')} + + + {t('thirdparty_api_source.test')} + + + {t('thirdparty_api_source.submit')} + + +
+
+ ) + } +}) diff --git a/dolphinscheduler-ui/src/views/thirdparty-api-source/use-table.ts b/dolphinscheduler-ui/src/views/thirdparty-api-source/use-table.ts new file mode 100644 index 000000000000..1862c95e953f --- /dev/null +++ b/dolphinscheduler-ui/src/views/thirdparty-api-source/use-table.ts @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { reactive, ref } from 'vue' +import { + queryDataSourceListPaging, + deleteDataSource +} from '@/service/modules/data-source' + +export function useTable() { + const data = reactive({ + page: 1, + pageSize: 10, + itemCount: 0, + searchVal: ref(''), + list: [], + loading: false + }) + + const getList = async () => { + if (data.loading) return + data.loading = true + + const listRes = await queryDataSourceListPaging({ + pageNo: data.page, + pageSize: data.pageSize, + searchVal: data.searchVal + }) + data.loading = false + data.list = listRes.totalList + data.itemCount = listRes.total + } + + const updateList = () => { + if (data.list.length === 1 && data.page > 1) { + --data.page + } + getList() + } + + const deleteRecord = async (id: number) => { + const ignored = await deleteDataSource(id) + updateList() + } + + const changePage = (page: number) => { + data.page = page + getList() + } + + const changePageSize = (pageSize: number) => { + data.page = 1 + data.pageSize = pageSize + getList() + } + + return { data, changePage, changePageSize, deleteRecord, updateList } +} \ No newline at end of file From 469bc549e8e3320bcdd7b1d1e4789f0130bea885 Mon Sep 17 00:00:00 2001 From: yud8 Date: Wed, 24 Dec 2025 11:54:22 +0800 Subject: [PATCH 03/11] [Feature-17501]simplify and modernize third-party connector --- .../dolphinscheduler/api/enums/Status.java | 32 --- .../impl/ExternalSystemServiceImpl.java | 56 ++-- .../common/utils/OkHttpUtils.java | 11 +- .../AuthenticationUtils.java | 44 ++-- .../param/AuthConfig.java | 8 +- .../param/InterfaceInfo.java | 7 +- .../param/RequestParameter.java | 6 +- ...rdPartySystemConnectorConnectionParam.java | 23 +- ...artySystemConnectorDataSourceParamDTO.java | 2 - ...rtySystemConnectorDataSourceProcessor.java | 246 ++++++++---------- .../pom.xml | 2 +- .../BaseExternalSystemParams.java | 158 ----------- .../ExternalSystemParameters.java | 19 +- .../externalSystem/ExternalSystemTask.java | 58 +++-- .../locales/en_US/thirdparty-api-source.ts | 10 +- .../locales/zh_CN/thirdparty-api-source.ts | 17 +- .../modules/thirdparty-api-source/index.ts | 83 ------ .../src/views/datasource/list/detail.tsx | 54 ++-- .../src/views/datasource/list/types.ts | 1 - .../src/views/datasource/list/use-form.ts | 9 - .../node/fields/use-external-system.ts | 20 +- .../task/components/node/format-data.ts | 4 +- .../node/tasks/use-external-system.ts | 6 +- .../projects/task/components/node/types.ts | 2 +- .../src/views/thirdparty-api-source/modal.tsx | 51 ++-- 25 files changed, 327 insertions(+), 602 deletions(-) rename {dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem => dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector}/AuthenticationUtils.java (67%) delete mode 100644 dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/BaseExternalSystemParams.java diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/enums/Status.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/enums/Status.java index a22b7c439fe6..c32c52e3f503 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/enums/Status.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/enums/Status.java @@ -73,40 +73,8 @@ public enum Status { VERIFY_DATASOURCE_NAME_FAILURE(10039, "verify datasource name failure", "验证数据源名称失败"), UNAUTHORIZED_DATASOURCE(10040, "unauthorized datasource", "未经授权的数据源"), AUTHORIZED_DATA_SOURCE(10041, "authorized data source", "授权数据源失败"), - CREATE_EXTERNAL_SYSTEM_ERROR(11042, "create external system error", "创建第三方系统错误"), - UPDATE_EXTERNAL_SYSTEM_ERROR(11043, "update external system error", "更新第三方系统错误"), QUERY_EXTERNAL_SYSTEM_ERROR(11044, "query external system error", "查询第三方系统错误"), - DELETE_EXTERNAL_SYSTEM_ERROR(11045, "delete external system error", "删除第三方系统错误"), - TEST_EXTERNAL_SYSTEM_CONNECTION_ERROR(11046, "connect external system failure", "建立第三方系统连接失败"), - EXTERNAL_SYSTEM_NOT_EXIST(11047, "external system not exist", "第三方系统不存在"), - EXTERNAL_SYSTEM_NAME_EXIST(11048, "external name exist", "第三方系统名称已存在"), EXTERNAL_SYSTEM_CONNECT_AUTH_FAILED(11049, "external connect failed", "第三方系统连接失败,检查认证信息"), - EXTERNAL_SYSTEM_CONNECT_SELECT_FAILED(11049, "external connect failed", "第三方系统连接失败,检查查询任务列表接口信息"), - UNAUTHORIZED_EXTERNAL_SYSTEM(10050, "unauthorized datasource", "未经授权的第三方系统"), - AUTHORIZED_EXTERNAL_SYSTEM(10051, "authorized data source", "授权第三方系统失败"), - EXTERNAL_SYSTEM_NAME_EMPTY(11052, "external system name is empty", "第三方系统名称为空"), - EXTERNAL_SYSTEM_SERVICE_ADDRESS_EMPTY(11053, "external system service address is empty", "第三方系统服务地址为空"), - EXTERNAL_SYSTEM_AUTH_CONFIG_EMPTY(11054, "external system auth config is empty", "第三方系统认证配置为空"), - EXTERNAL_SYSTEM_AUTH_CONFIG_TYPE_EMPTY(11055, "external system auth config type is empty", "第三方系统认证类型为空"), - EXTERNAL_SYSTEM_BASIC_USERNAME_EMPTY(11056, "external system basic username is empty", "第三方系统基础认证用户名为空"), - EXTERNAL_SYSTEM_BASIC_PASSWORD_EMPTY(11057, "external system basic password is empty", "第三方系统基础认证密码为空"), - EXTERNAL_SYSTEM_JWT_TOKEN_EMPTY(11058, "external system jwt token is empty", "第三方系统JWT令牌为空"), - EXTERNAL_SYSTEM_OAUTH2_TOKEN_URL_EMPTY(11059, "external system oauth2 token url is empty", "第三方系统OAuth2令牌URL为空"), - EXTERNAL_SYSTEM_OAUTH2_CLIENT_ID_EMPTY(11060, "external system oauth2 client id is empty", "第三方系统OAuth2客户端ID为空"), - EXTERNAL_SYSTEM_OAUTH2_CLIENT_SECRET_EMPTY(11061, "external system oauth2 client secret is empty", - "第三方系统OAuth2客户端密钥为空"), - EXTERNAL_SYSTEM_OAUTH2_GRANT_TYPE_EMPTY(11062, "external system oauth2 grant type is empty", "第三方系统OAuth2授权类型为空"), - EXTERNAL_SYSTEM_OAUTH2_USERNAME_EMPTY(11063, "external system oauth2 username is empty", "第三方系统OAuth2用户名为空"), - EXTERNAL_SYSTEM_OAUTH2_PASSWORD_EMPTY(11064, "external system oauth2 password is empty", "第三方系统OAuth2密码为空"), - EXTERNAL_SYSTEM_AUTH_TYPE_UNSUPPORTED(11065, "external system auth type is unsupported", "第三方系统认证类型不支持"), - EXTERNAL_SYSTEM_SELECT_INTERFACE_EMPTY(11066, "external system select interface is empty", "第三方系统查询接口配置为空"), - EXTERNAL_SYSTEM_SUBMIT_INTERFACE_EMPTY(11067, "external system submit interface is empty", "第三方系统提交接口配置为空"), - EXTERNAL_SYSTEM_POLL_STATUS_INTERFACE_EMPTY(11068, "external system poll status interface is empty", - "第三方系统轮询状态接口配置为空"), - EXTERNAL_SYSTEM_STOP_INTERFACE_EMPTY(11069, "external system stop interface is empty", "第三方系统停止接口配置为空"), - EXTERNAL_SYSTEM_INTERFACE_URL_EMPTY(11070, "external system interface url is empty", "第三方系统接口URL为空"), - EXTERNAL_SYSTEM_INTERFACE_METHOD_EMPTY(11071, "external system interface method is empty", "第三方系统接口方法为空"), - EXTERNAL_SYSTEM_NAME_TOO_LONG(11071, "external system name too long", "第三方系统接口名称太长"), LOGIN_SUCCESS(10042, "login success", "登录成功"), USER_LOGIN_FAILURE(10043, "user login failure", "用户登录失败"), LIST_WORKERS_ERROR(10044, "list workers error", "查询worker列表错误"), diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ExternalSystemServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ExternalSystemServiceImpl.java index c479e244e64e..462931aa8fe6 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ExternalSystemServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ExternalSystemServiceImpl.java @@ -31,9 +31,13 @@ import org.apache.dolphinscheduler.dao.entity.User; import org.apache.dolphinscheduler.dao.mapper.DataSourceMapper; import org.apache.dolphinscheduler.plugin.datasource.api.utils.PasswordUtils; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.AuthenticationUtils; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.AuthConfig; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.InterfaceInfo; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.RequestParameter; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.ResponseParameter; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.ThirdPartySystemConnectorConnectionParam; import org.apache.dolphinscheduler.plugin.task.api.TaskException; -import org.apache.dolphinscheduler.plugin.task.externalSystem.AuthenticationUtils; -import org.apache.dolphinscheduler.plugin.task.externalSystem.BaseExternalSystemParams; import org.apache.dolphinscheduler.plugin.task.externalSystem.ExternalTaskConstants; import java.util.ArrayList; @@ -59,14 +63,16 @@ public class ExternalSystemServiceImpl extends BaseServiceImpl implements Extern private static final String EXTERNAL_TASK_ID = "id"; private static final String EXTERNAL_TASK_NAME = "name"; - private OkHttpResponse callSelectInterface(BaseExternalSystemParams baseExternalSystemParam, boolean dbPassword) { + private OkHttpResponse callSelectInterface(ThirdPartySystemConnectorConnectionParam baseExternalSystemParam, + boolean dbPassword) { if (baseExternalSystemParam == null || baseExternalSystemParam.getSelectInterface() == null) { - throw new IllegalArgumentException("BaseExternalSystemParams or SelectInterface cannot be null"); + throw new IllegalArgumentException( + "ThirdPartySystemConnectorConnectionParam or SelectInterface cannot be null"); } - BaseExternalSystemParams.InterfaceConfig selectConfig = baseExternalSystemParam.getSelectInterface(); + InterfaceInfo selectConfig = baseExternalSystemParam.getSelectInterface(); - // 替换参数占位符 + // Replace parameter placeholders String url = baseExternalSystemParam.getCompleteUrl(selectConfig.getUrl()); Map headeMap = new HashMap<>(); @@ -76,8 +82,8 @@ private OkHttpResponse callSelectInterface(BaseExternalSystemParams baseExternal try { if (dbPassword) { - // 已保存信息,从数据库中获取,并解密 - BaseExternalSystemParams.AuthConfig authConfig = baseExternalSystemParam.getAuthConfig(); + // Saved information, retrieve from database and decrypt + AuthConfig authConfig = baseExternalSystemParam.getAuthConfig(); decodePassword(authConfig); baseExternalSystemParam.setAuthConfig(authConfig); token = AuthenticationUtils.authenticateAndGetToken(baseExternalSystemParam); @@ -85,14 +91,15 @@ private OkHttpResponse callSelectInterface(BaseExternalSystemParams baseExternal if (baseExternalSystemParam.getId() != null) { DataSource existingSystem = dataSourceMapper.selectById(baseExternalSystemParam.getId()); if (existingSystem == null) { - // 新建信息测试连接 + // New information test connection token = AuthenticationUtils.authenticateAndGetToken(baseExternalSystemParam); } else { - // 更新信息测试连接,如果密码没有修改,则使用数据库中保存的密码进行测试连接 - BaseExternalSystemParams oldParams = + // Update information test connection, if password is not modified, use password saved in + // database for test connection + ThirdPartySystemConnectorConnectionParam oldParams = JSONUtils.parseObject(existingSystem.getConnectionParams(), - BaseExternalSystemParams.class); - BaseExternalSystemParams.AuthConfig authConfig = baseExternalSystemParam.getAuthConfig(); + ThirdPartySystemConnectorConnectionParam.class); + AuthConfig authConfig = baseExternalSystemParam.getAuthConfig(); if (authConfig.getBasicPassword() != null && authConfig.getBasicPassword().equals(Constants.XXXXXX)) { authConfig.setBasicPassword(oldParams.getAuthConfig().getBasicPassword()); @@ -113,7 +120,6 @@ private OkHttpResponse callSelectInterface(BaseExternalSystemParams baseExternal token = AuthenticationUtils.authenticateAndGetToken(baseExternalSystemParam); } } else { - // 新建信息测试连接 token = AuthenticationUtils.authenticateAndGetToken(baseExternalSystemParam); } } @@ -127,8 +133,8 @@ private OkHttpResponse callSelectInterface(BaseExternalSystemParams baseExternal baseExternalSystemParam.getTokenPrefix(baseExternalSystemParam.getAuthConfig().getHeaderPrefix()) + token); - // 处理参数 - for (BaseExternalSystemParams.RequestParameter param : selectConfig.getParameters()) { + // Process parameters + for (RequestParameter param : selectConfig.getParameters()) { // todo String value = replaceParameterPlaceholders(param.getParamValue()); String value = param.getParamValue(); @@ -152,7 +158,7 @@ private OkHttpResponse callSelectInterface(BaseExternalSystemParams baseExternal headers.setHeaders(headeMap); } OkHttpResponse response; - if (BaseExternalSystemParams.HttpMethod.POST.equals(selectConfig.getMethod())) { + if (InterfaceInfo.HttpMethod.POST.equals(selectConfig.getMethod())) { if (contentType.equals(OkHttpRequestHeaderContentType.APPLICATION_JSON)) { response = OkHttpUtils.post(url, headers, requestParams, requestBody, 120000, 120000, 120000); @@ -169,7 +175,7 @@ private OkHttpResponse callSelectInterface(BaseExternalSystemParams baseExternal log.error("select task failed, OkHttpRequestHeaderContentType not support: {},", contentType); throw new ServiceException(Status.EXTERNAL_SYSTEM_CONNECT_AUTH_FAILED); } - } else if (BaseExternalSystemParams.HttpMethod.PUT.equals(selectConfig.getMethod())) { + } else if (InterfaceInfo.HttpMethod.PUT.equals(selectConfig.getMethod())) { response = OkHttpUtils.put(url, headers, requestBody, 120000, 120000, 120000); } else { response = OkHttpUtils.get(url, headers, requestParams, 120000, 120000, 120000); @@ -177,7 +183,9 @@ private OkHttpResponse callSelectInterface(BaseExternalSystemParams baseExternal return response; } catch (Exception e) { - log.error("select task failed, baseExternalSystemParam: {}, dbPassword: {}", baseExternalSystemParam, + log.error("select task failed, id: {}, serviceAddress: {}, dbPassword: {}", + baseExternalSystemParam.getId(), + baseExternalSystemParam.getServiceAddress(), dbPassword, e); throw new ServiceException(Status.EXTERNAL_SYSTEM_CONNECT_AUTH_FAILED); } @@ -195,7 +203,7 @@ private OkHttpRequestHeaderContentType getContentType(Map header return OkHttpRequestHeaderContentType.APPLICATION_JSON; // 默认值 } - private void decodePassword(BaseExternalSystemParams.AuthConfig authConfig) { + private void decodePassword(AuthConfig authConfig) { if (null != authConfig.getOauth2ClientSecret() && !authConfig.getOauth2ClientSecret().isEmpty()) { authConfig.setOauth2ClientSecret(PasswordUtils.decodePassword(authConfig.getOauth2ClientSecret())); } @@ -223,13 +231,13 @@ private String getHiddenPassword() { public List queryExternalSystemTasks(User loginUser, int externalSystemId) { DataSource dataSource = dataSourceMapper.selectById(externalSystemId); - BaseExternalSystemParams baseExternalSystemParam = - JSONUtils.parseObject(dataSource.getConnectionParams(), BaseExternalSystemParams.class); + ThirdPartySystemConnectorConnectionParam baseExternalSystemParam = + JSONUtils.parseObject(dataSource.getConnectionParams(), ThirdPartySystemConnectorConnectionParam.class); - // 校验查询必要 + // Validate query parameters String taskIdExpression = ""; String taskNameExpression = ""; - for (BaseExternalSystemParams.ResponseParameter param : baseExternalSystemParam.getSelectInterface() + for (ResponseParameter param : baseExternalSystemParam.getSelectInterface() .getResponseParameters()) { if (EXTERNAL_TASK_ID.equals(param.getKey())) { taskIdExpression = param.getJsonPath(); diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/OkHttpUtils.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/OkHttpUtils.java index 6d5c446b9b79..bd2ae77ff6d0 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/OkHttpUtils.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/OkHttpUtils.java @@ -109,9 +109,16 @@ public class OkHttpUtils { OkHttpClient client = getHttpClient(connectTimeout, writeTimeout, readTimeout); String finalUrl = addUrlParams(requestParamsMap, url); Request.Builder requestBuilder = new Request.Builder().url(finalUrl); - addHeader(okHttpRequestHeaders.getHeaders(), requestBuilder); + + if (okHttpRequestHeaders != null) { + addHeader(okHttpRequestHeaders.getHeaders(), requestBuilder); + } + + RequestBody safeFormBody = formBody != null ? formBody + : RequestBody.create("", + MediaType.parse(OkHttpRequestHeaderContentType.APPLICATION_FORM_URLENCODED.getValue())); Request request = requestBuilder - .post(formBody) // 明确使用POST方法 + .post(safeFormBody) .build(); try (Response response = client.newCall(request).execute()) { return new OkHttpResponse(response.code(), getResponseBody(response)); diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/AuthenticationUtils.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/AuthenticationUtils.java similarity index 67% rename from dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/AuthenticationUtils.java rename to dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/AuthenticationUtils.java index 7c10c65e88e8..ea9feeb604cb 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/AuthenticationUtils.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/AuthenticationUtils.java @@ -15,14 +15,16 @@ * limitations under the License. */ -package org.apache.dolphinscheduler.plugin.task.externalSystem; +package org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector; import org.apache.dolphinscheduler.common.model.OkHttpRequestHeaderContentType; import org.apache.dolphinscheduler.common.model.OkHttpRequestHeaders; import org.apache.dolphinscheduler.common.model.OkHttpResponse; import org.apache.dolphinscheduler.common.utils.JSONUtils; import org.apache.dolphinscheduler.common.utils.OkHttpUtils; -import org.apache.dolphinscheduler.plugin.task.api.TaskException; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.AuthConfig; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.AuthMapping; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.ThirdPartySystemConnectorConnectionParam; import java.util.HashMap; @@ -36,44 +38,44 @@ public class AuthenticationUtils { /** - * 认证并获取Token + * Authenticate and get Token * - * @param baseExternalSystemParams 认证配置 - * @return 认证后的Token + * @param thirdPartySystemConnectorConnectionParam configuration + * @return Authenticated Token * @throws Exception */ - public static String authenticateAndGetToken(BaseExternalSystemParams baseExternalSystemParams) throws Exception { - BaseExternalSystemParams.AuthConfig authConfig = baseExternalSystemParams.getAuthConfig(); + public static String authenticateAndGetToken(ThirdPartySystemConnectorConnectionParam thirdPartySystemConnectorConnectionParam) throws Exception { + AuthConfig authConfig = thirdPartySystemConnectorConnectionParam.getAuthConfig(); if (authConfig == null) { throw new RuntimeException("AuthConfig is not provided"); } switch (authConfig.getAuthType()) { case BASIC_AUTH: - // 基础认证 + // Basic authentication String auth = authConfig.getBasicUsername() + ":" + authConfig.getBasicPassword(); String encoding = java.util.Base64.getEncoder().encodeToString(auth.getBytes()); return encoding; case JWT: - // JWT认证 + // JWT authentication return authConfig.getJwtToken(); case OAUTH2: - // OAuth2认证 - return getOAuth2Token(baseExternalSystemParams); + // OAuth2 authentication + return getOAuth2Token(thirdPartySystemConnectorConnectionParam); default: throw new RuntimeException("Unsupported auth type: " + authConfig.getAuthType()); } } /** - * 获取OAuth2 Token + * Get OAuth2 Token * - * @param baseExternalSystemParams 认证配置 + * @param thirdPartySystemConnectorConnectionParam Authentication configuration * @return OAuth2 Token * @throws Exception */ - private static String getOAuth2Token(BaseExternalSystemParams baseExternalSystemParams) throws Exception { - BaseExternalSystemParams.AuthConfig authConfig = baseExternalSystemParams.getAuthConfig(); + private static String getOAuth2Token(ThirdPartySystemConnectorConnectionParam thirdPartySystemConnectorConnectionParam) throws Exception { + AuthConfig authConfig = thirdPartySystemConnectorConnectionParam.getAuthConfig(); try { OkHttpRequestHeaders headers = new OkHttpRequestHeaders(); headers.setHeaders(new HashMap<>()); @@ -86,9 +88,9 @@ private static String getOAuth2Token(BaseExternalSystemParams baseExternalSystem .add("password", authConfig.getOauth2Password()) .add("grant_type", authConfig.getOauth2GrantType()); - // 添加 authMappings 中的参数 + // Add parameters from authMappings if (authConfig.getAuthMappings() != null) { - for (BaseExternalSystemParams.AuthMapping authMapping : authConfig.getAuthMappings()) { + for (AuthMapping authMapping : authConfig.getAuthMappings()) { formBodyBuilder.add(authMapping.getKey(), authMapping.getValue()); } } @@ -96,14 +98,14 @@ private static String getOAuth2Token(BaseExternalSystemParams baseExternalSystem RequestBody formBody = formBodyBuilder.build(); OkHttpResponse response = OkHttpUtils.postFormBody( - baseExternalSystemParams.getCompleteUrl(authConfig.getOauth2TokenUrl()), + thirdPartySystemConnectorConnectionParam.getCompleteUrl(authConfig.getOauth2TokenUrl()), headers, null, formBody, 30000, 30000, 30000); if (response.getStatusCode() != 200) { - throw new TaskException("Authentication failed: " + response.getBody()); + throw new RuntimeException("Authentication failed: " + response.getBody()); } JsonNode authResult = JSONUtils.parseObject(response.getBody(), JsonNode.class); @@ -111,12 +113,12 @@ private static String getOAuth2Token(BaseExternalSystemParams baseExternalSystem log.info("Authentication successful, token obtained"); return authResult.get("access_token").asText(); } else { - throw new TaskException("Failed to get access token from response"); + throw new RuntimeException("Failed to get access token from response"); } } catch (Exception e) { log.error("Authentication failed", e); - throw new TaskException("Authentication failed", e); + throw new RuntimeException("Authentication failed", e); } } diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/AuthConfig.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/AuthConfig.java index d8d4794a4a28..b3d8e0c6d591 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/AuthConfig.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/AuthConfig.java @@ -24,8 +24,8 @@ @Data public class AuthConfig { - private String authType; // authType:BASIC, JWT, OAUTH2 - private String headerPrefix; // headerPrefix:BASIC, JWT, OAUTH2 + private AuthType authType; // authType:BASIC, JWT, OAUTH2 + private String headerPrefix; // headerPrefix:Basic, Bearer // === (Basic Auth) === private String basicUsername; @@ -44,4 +44,8 @@ public class AuthConfig { // === other authMappings === private List authMappings; + + public enum AuthType { + BASIC_AUTH, JWT, OAUTH2 + } } diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/InterfaceInfo.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/InterfaceInfo.java index 0eb6bdb14528..a0d679f607ea 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/InterfaceInfo.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/InterfaceInfo.java @@ -25,8 +25,13 @@ public class InterfaceInfo { private String url; - private String method; + private HttpMethod method; private String body; private List parameters; private List responseParameters; + + public enum HttpMethod { + GET, POST, PUT + } + } diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/RequestParameter.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/RequestParameter.java index 9b931c3bc1fe..e7ad0c0730fd 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/RequestParameter.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/RequestParameter.java @@ -24,5 +24,9 @@ public class RequestParameter { private String paramName; private String paramValue; - private String location; // header,param,body + private ParamLocation location; // header,param,body + + public enum ParamLocation { + HEADER, PARAM, BODY + } } diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorConnectionParam.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorConnectionParam.java index e71173a025b8..b8e3728e0a41 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorConnectionParam.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorConnectionParam.java @@ -20,14 +20,16 @@ import org.apache.dolphinscheduler.spi.datasource.ConnectionParam; import lombok.Data; +import lombok.extern.slf4j.Slf4j; import com.fasterxml.jackson.annotation.JsonInclude; @Data @JsonInclude(JsonInclude.Include.NON_NULL) +@Slf4j public class ThirdPartySystemConnectorConnectionParam implements ConnectionParam { - private String systemName; + private Integer id; // System ID private String serviceAddress; @@ -39,4 +41,23 @@ public class ThirdPartySystemConnectorConnectionParam implements ConnectionParam private InterfaceInfo stopInterface; private int interfaceTimeout = 120000; + + public String getCompleteUrl(String url) { + if (url == null || !url.startsWith("http")) { + if (serviceAddress == null) { + log.warn("Service address is not set."); + return url; + } + return serviceAddress + url; + } + return url; + } + + public String getTokenPrefix(String headerPrefix) { + if (null == headerPrefix || headerPrefix.isEmpty()) { + return ""; + } else { + return headerPrefix.trim() + " "; + } + } } diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorDataSourceParamDTO.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorDataSourceParamDTO.java index de53919ae19e..d10f12572f19 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorDataSourceParamDTO.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorDataSourceParamDTO.java @@ -27,8 +27,6 @@ @EqualsAndHashCode(callSuper = true) public class ThirdPartySystemConnectorDataSourceParamDTO extends BaseDataSourceParamDTO { - private String systemName; - private String serviceAddress; private AuthConfig authConfig; diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorDataSourceProcessor.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorDataSourceProcessor.java index 0d53de1f2e40..231d723bb481 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorDataSourceProcessor.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorDataSourceProcessor.java @@ -17,16 +17,11 @@ package org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param; -import static org.apache.dolphinscheduler.common.constants.Constants.HTTP_CONNECT_TIMEOUT; - -import org.apache.dolphinscheduler.common.model.OkHttpRequestHeaderContentType; -import org.apache.dolphinscheduler.common.model.OkHttpRequestHeaders; -import org.apache.dolphinscheduler.common.model.OkHttpResponse; import org.apache.dolphinscheduler.common.utils.JSONUtils; -import org.apache.dolphinscheduler.common.utils.OkHttpUtils; import org.apache.dolphinscheduler.plugin.datasource.api.datasource.AbstractDataSourceProcessor; import org.apache.dolphinscheduler.plugin.datasource.api.datasource.BaseDataSourceParamDTO; import org.apache.dolphinscheduler.plugin.datasource.api.datasource.DataSourceProcessor; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.AuthenticationUtils; import org.apache.dolphinscheduler.spi.datasource.ConnectionParam; import org.apache.dolphinscheduler.spi.enums.DbType; @@ -34,14 +29,9 @@ import java.sql.Connection; import java.text.MessageFormat; -import java.util.HashMap; -import java.util.Map; import lombok.extern.slf4j.Slf4j; -import okhttp3.FormBody; -import okhttp3.RequestBody; -import com.fasterxml.jackson.databind.JsonNode; import com.google.auto.service.AutoService; @AutoService(DataSourceProcessor.class) @@ -61,6 +51,7 @@ public void checkDatasourceParam(BaseDataSourceParamDTO datasourceParamDTO) { if (StringUtils.isEmpty(thirdPartySystemConnectorParamDTO.getServiceAddress())) { throw new IllegalArgumentException("third party system connector datasource param is not valid"); } + checkExternalSystemParam(thirdPartySystemConnectorParamDTO); } @Override @@ -78,7 +69,6 @@ public BaseDataSourceParamDTO createDatasourceParamDTO(String connectionJson) { ThirdPartySystemConnectorDataSourceParamDTO thirdPartySystemConnectorDataSourceParamDTO = new ThirdPartySystemConnectorDataSourceParamDTO(); - thirdPartySystemConnectorDataSourceParamDTO.setSystemName(connectionParams.getSystemName()); thirdPartySystemConnectorDataSourceParamDTO.setServiceAddress(connectionParams.getServiceAddress()); thirdPartySystemConnectorDataSourceParamDTO.setAuthConfig(connectionParams.getAuthConfig()); thirdPartySystemConnectorDataSourceParamDTO.setSelectInterface(connectionParams.getSelectInterface()); @@ -98,8 +88,6 @@ public ThirdPartySystemConnectorConnectionParam createConnectionParams(BaseDataS ThirdPartySystemConnectorConnectionParam thirdPartySystemConnectorConnectionParam = new ThirdPartySystemConnectorConnectionParam(); - thirdPartySystemConnectorConnectionParam.setSystemName( - thirdPartySystemConnectorDataSourceParamDTO.getSystemName()); thirdPartySystemConnectorConnectionParam.setServiceAddress( thirdPartySystemConnectorDataSourceParamDTO.getServiceAddress()); thirdPartySystemConnectorConnectionParam.setAuthConfig( @@ -147,155 +135,131 @@ public Connection getConnection(ConnectionParam connectionParam) { public boolean checkDataSourceConnectivity(ConnectionParam connectionParam) { ThirdPartySystemConnectorConnectionParam baseConnectionParam = (ThirdPartySystemConnectorConnectionParam) connectionParam; - try { - OkHttpResponse response = callSelectInterface(baseConnectionParam); - if (response.getStatusCode() == 200) { - return true; - } - return false; + String token = AuthenticationUtils.authenticateAndGetToken(baseConnectionParam); + return token != null; } catch (Exception e) { log.error("connect error, e:{}", e.getMessage()); return false; } } - private OkHttpResponse callSelectInterface(ThirdPartySystemConnectorConnectionParam baseConnectionParam) { - try { - InterfaceInfo selectConfig = baseConnectionParam.getSelectInterface(); - - // 替换参数占位符 - String url = selectConfig.getUrl(); - - OkHttpRequestHeaders headers = new OkHttpRequestHeaders(); - headers.setOkHttpRequestHeaderContentType(OkHttpRequestHeaderContentType.APPLICATION_JSON); - - Map headerMap = new HashMap<>(); - Map requestBody = new HashMap<>(); - Map requestParams = new HashMap<>(); - - // 获取认证token - String token = authenticateAndGetToken(baseConnectionParam); - - headerMap.put("Authorization", token); - - // 处理参数 - if (selectConfig.getParameters() != null) { - for (RequestParameter param : selectConfig.getParameters()) { - String value = param.getParamValue(); - - switch (param.getLocation()) { - case "HEADER": - headerMap.put(param.getParamName(), value); - break; - case "BODY": - if ("body".equals(param.getParamName())) { - requestBody = JSONUtils.parseObject(value, Map.class); - } - break; - case "PARAM": - requestParams.put(param.getParamName(), value); - break; - } - } - } - - if (!headerMap.isEmpty()) { - headers.setHeaders(headerMap); - } - - OkHttpResponse response; - if ("POST".equals(selectConfig.getMethod())) { - response = OkHttpUtils.post(url, headers, requestParams, requestBody, - HTTP_CONNECT_TIMEOUT, HTTP_CONNECT_TIMEOUT, HTTP_CONNECT_TIMEOUT); - } else if ("PUT".equals(selectConfig.getMethod())) { - response = OkHttpUtils.put(url, headers, requestBody, - HTTP_CONNECT_TIMEOUT, HTTP_CONNECT_TIMEOUT, HTTP_CONNECT_TIMEOUT); - } else { - response = OkHttpUtils.get(url, headers, requestParams, - HTTP_CONNECT_TIMEOUT, HTTP_CONNECT_TIMEOUT, HTTP_CONNECT_TIMEOUT); - } - return response; + @Override + public DbType getDbType() { + return DbType.THIRDPARTY_SYSTEM_CONNECTOR; + } - } catch (Exception e) { - log.error("select task failed", e); - throw new RuntimeException("select task failed", e); - } + @Override + public DataSourceProcessor create() { + return new ThirdPartySystemConnectorDataSourceProcessor(); } - private String authenticateAndGetToken(ThirdPartySystemConnectorConnectionParam baseConnectionParam) throws Exception { - AuthConfig authConfig = baseConnectionParam.getAuthConfig(); + private void checkExternalSystemParam(ThirdPartySystemConnectorDataSourceParamDTO paramDTO) { + + // Check system name + if (paramDTO.getName() == null || paramDTO.getName().trim().isEmpty()) { + throw new IllegalArgumentException("system name cannot be empty"); + } + String systemName = paramDTO.getName().trim(); + if (systemName.length() > 64) { + throw new IllegalArgumentException("system name length cannot exceed 64 characters"); + } + paramDTO.setName(systemName); + + // Check service address + if (paramDTO.getServiceAddress() == null || paramDTO.getServiceAddress().trim().isEmpty()) { + throw new IllegalArgumentException("service address cannot be empty"); + } + paramDTO.setServiceAddress(paramDTO.getServiceAddress().trim()); + + // Check authentication configuration + AuthConfig authConfig = paramDTO.getAuthConfig(); if (authConfig == null) { - throw new RuntimeException("AuthConfig is not provided"); + throw new IllegalArgumentException("auth config cannot be empty"); + } + if (authConfig.getAuthType() == null) { + throw new IllegalArgumentException("auth type cannot be empty"); } + // 根据认证类型进行具体校验 switch (authConfig.getAuthType()) { - case "BASIC": - // 基础认证 - String auth = authConfig.getBasicUsername() + ":" + authConfig.getBasicPassword(); - String encoding = java.util.Base64.getEncoder().encodeToString(auth.getBytes()); - return encoding; - case "JWT": - // JWT认证 - return authConfig.getJwtToken(); - case "OAUTH2": - // OAuth2认证 - return getOAuth2Token(baseConnectionParam); + case BASIC_AUTH: + if (authConfig.getBasicUsername() == null || authConfig.getBasicUsername().trim().isEmpty()) { + throw new IllegalArgumentException("basic auth username cannot be empty"); + } + authConfig.setBasicUsername(authConfig.getBasicUsername().trim()); + if (authConfig.getBasicPassword() == null || authConfig.getBasicPassword().trim().isEmpty()) { + throw new IllegalArgumentException("basic auth password cannot be empty"); + } + authConfig.setBasicPassword(authConfig.getBasicPassword().trim()); + break; + case JWT: + if (authConfig.getJwtToken() == null || authConfig.getJwtToken().trim().isEmpty()) { + throw new IllegalArgumentException("JWT token cannot be empty"); + } + authConfig.setJwtToken(authConfig.getJwtToken().trim()); + break; + case OAUTH2: + if (authConfig.getOauth2TokenUrl() == null || authConfig.getOauth2TokenUrl().trim().isEmpty()) { + throw new IllegalArgumentException("OAuth2 token URL cannot be empty"); + } + authConfig.setOauth2TokenUrl(authConfig.getOauth2TokenUrl().trim()); + if (authConfig.getOauth2ClientId() == null || authConfig.getOauth2ClientId().trim().isEmpty()) { + throw new IllegalArgumentException("OAuth2 client ID cannot be empty"); + } + authConfig.setOauth2ClientId(authConfig.getOauth2ClientId().trim()); + if (authConfig.getOauth2ClientSecret() == null || authConfig.getOauth2ClientSecret().trim().isEmpty()) { + throw new IllegalArgumentException("OAuth2 client secret cannot be empty"); + } + authConfig.setOauth2ClientSecret(authConfig.getOauth2ClientSecret().trim()); + if (authConfig.getOauth2GrantType() == null || authConfig.getOauth2GrantType().trim().isEmpty()) { + throw new IllegalArgumentException("OAuth2 grant type cannot be empty"); + } + authConfig.setOauth2GrantType(authConfig.getOauth2GrantType().trim()); + if (authConfig.getOauth2GrantType().equals("password")) { + if (authConfig.getOauth2Username() == null || authConfig.getOauth2Username().trim().isEmpty()) { + throw new IllegalArgumentException("OAuth2 username cannot be empty"); + } + authConfig.setOauth2Username(authConfig.getOauth2Username().trim()); + if (authConfig.getOauth2Password() == null || authConfig.getOauth2Password().trim().isEmpty()) { + throw new IllegalArgumentException("OAuth2 password cannot be empty"); + } + authConfig.setOauth2Password(authConfig.getOauth2Password().trim()); + } + break; default: - throw new RuntimeException("Unsupported auth type: " + authConfig.getAuthType()); + throw new IllegalArgumentException("unsupported auth type"); } - } - private String getOAuth2Token(ThirdPartySystemConnectorConnectionParam baseConnectionParam) throws Exception { - AuthConfig authConfig = baseConnectionParam.getAuthConfig(); - - OkHttpRequestHeaders headers = new OkHttpRequestHeaders(); - headers.setHeaders(new HashMap<>()); - headers.setOkHttpRequestHeaderContentType(OkHttpRequestHeaderContentType.APPLICATION_FORM_URLENCODED); - - FormBody.Builder formBodyBuilder = new FormBody.Builder() - .add("client_id", authConfig.getOauth2ClientId()) - .add("client_secret", authConfig.getOauth2ClientSecret()) - .add("username", authConfig.getOauth2Username()) - .add("password", authConfig.getOauth2Password()) - .add("grant_type", authConfig.getOauth2GrantType()); - - // 添加 authMappings 中的参数 - if (authConfig.getAuthMappings() != null) { - for (AuthMapping authMapping : authConfig.getAuthMappings()) { - formBodyBuilder.add(authMapping.getKey(), authMapping.getValue()); - } + // Check interface configuration + if (paramDTO.getSelectInterface() == null) { + throw new IllegalArgumentException("select interface config cannot be empty"); } - - RequestBody formBody = formBodyBuilder.build(); - - OkHttpResponse response = OkHttpUtils.postFormBody( - baseConnectionParam.getServiceAddress() + authConfig.getOauth2TokenUrl(), - headers, - null, - formBody, - HTTP_CONNECT_TIMEOUT, HTTP_CONNECT_TIMEOUT, HTTP_CONNECT_TIMEOUT); - - if (response.getStatusCode() != 200) { - throw new RuntimeException("Authentication failed: " + response.getBody()); + if (paramDTO.getSubmitInterface() == null) { + throw new IllegalArgumentException("submit interface config cannot be empty"); } - - JsonNode authResult = JSONUtils.parseObject(response.getBody(), JsonNode.class); - if (authResult.has("access_token")) { - log.info("Authentication successful, token obtained"); - return authResult.get("access_token").asText(); - } else { - throw new RuntimeException("Failed to get access token from response"); + if (paramDTO.getPollStatusInterface() == null) { + throw new IllegalArgumentException("poll status interface config cannot be empty"); + } + if (paramDTO.getStopInterface() == null) { + throw new IllegalArgumentException("stop interface config cannot be empty"); } - } - @Override - public DbType getDbType() { - return DbType.THIRDPARTY_SYSTEM_CONNECTOR; + // Check interface configuration URL and method + checkInterfaceConfig(paramDTO.getSelectInterface()); + checkInterfaceConfig(paramDTO.getSubmitInterface()); + checkInterfaceConfig(paramDTO.getPollStatusInterface()); + checkInterfaceConfig(paramDTO.getStopInterface()); } - @Override - public DataSourceProcessor create() { - return new ThirdPartySystemConnectorDataSourceProcessor(); + private void checkInterfaceConfig(InterfaceInfo interfaceInfo) { + if (interfaceInfo.getUrl() == null || interfaceInfo.getUrl().trim().isEmpty()) { + throw new IllegalArgumentException("interface URL cannot be empty"); + } + interfaceInfo.setUrl(interfaceInfo.getUrl().trim()); + if (interfaceInfo.getMethod() == null) { + throw new IllegalArgumentException("interface method cannot be empty"); + } } + } diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/pom.xml b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/pom.xml index 1e35830587a1..0c62577249e7 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/pom.xml +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/pom.xml @@ -55,7 +55,7 @@ org.apache.dolphinscheduler - dolphinscheduler-datasource-ssh + dolphinscheduler-datasource-all ${project.version} diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/BaseExternalSystemParams.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/BaseExternalSystemParams.java deleted file mode 100644 index ece3c3eb806e..000000000000 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/BaseExternalSystemParams.java +++ /dev/null @@ -1,158 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.apache.dolphinscheduler.plugin.task.externalSystem; - -import java.util.List; - -import lombok.Data; -import lombok.extern.slf4j.Slf4j; - -@Data -@Slf4j -public class BaseExternalSystemParams { - - private Integer id; // System ID - - private String systemName; // System name - private String serviceAddress; // Service address - - private AuthConfig authConfig; // Authentication configuration - - private InterfaceConfig selectInterface; // Query interface configuration - private InterfaceConfig submitInterface; // Submit interface configuration - private PollingInterfaceConfig pollStatusInterface; // Polling status interface configuration - private InterfaceConfig stopInterface; // Stop interface configuration - - private int interfaceTimeout = 120000; // Interface timeout, default 120000 milliseconds (2 minutes) - - @Data - public static class AuthConfig { - - private AuthType authType; // Authentication type: BASIC, JWT, OAUTH2 - private String headerPrefix; // Authentication type: BASIC, JWT, OAUTH2 - - // === Basic Authentication === - private String basicUsername; - private String basicPassword; - - // === JWT Authentication === - private String jwtToken; // JWT token - - // === OAuth2 Authentication === - private String oauth2TokenUrl; - private String oauth2ClientId; - private String oauth2ClientSecret; - private String oauth2GrantType; // e.g., "client_credentials", "password" - private String oauth2Username; // Password mode only - private String oauth2Password; // Password mode only - - // === Dynamic mapping configuration (e.g. request header/parameter mapping) === - private AuthMapping[] authMappings; - } - - public enum AuthType { - BASIC_AUTH, JWT, OAUTH2 - } - - @Data - public static class InterfaceConfig { - - private String url; - private HttpMethod method; // Request method GET/POST - private String body; - private List parameters; // Parameter list - private List responseParameters; // Parameter list - - } - - @Data - public static class PollingInterfaceConfig extends InterfaceConfig { - - private PollingSuccessConfig pollingSuccessConfig; // Polling success configuration - private PollingFailureConfig pollingFailureConfig; // Polling failure configuration - } - - @Data - public static class RequestParameter { - - private String paramName; // Parameter name - private String paramValue; // Parameter value (can be a fixed value or placeholder) - private ParamLocation location; // Parameter location (header,param,body) - } - - @Data - public static class ResponseParameter { - - private String key; - private String jsonPath; - } - - @Data - public static class PollingSuccessConfig { - - private String successField; // Success judgment field name - private String successValue; // Value corresponding to success field - } - - @Data - public static class PollingFailureConfig { - - private String failureField; // Failure judgment field name - private String failureValue; // Value corresponding to failure field - } - - // Enum: Field types - public enum FieldType { - STRING, INTEGER, BOOLEAN, DATE, JSON_OBJECT, CUSTOM - } - - // Enum: HTTP Methods - public enum HttpMethod { - GET, POST, PUT - } - - // Enum: Parameter locations - public enum ParamLocation { - HEADER, PARAM - } - - @Data - public static class AuthMapping { - - private String key; - private String value; - } - public String getTokenPrefix(String headerPrefix) { - if (null == headerPrefix || headerPrefix.isEmpty()) { - return ""; - } else { - return headerPrefix.trim() + " "; - } - } - public String getCompleteUrl(String url) { - if (url == null || !url.startsWith("http")) { - if (serviceAddress == null) { - log.warn("Service address is not set."); - return url; // 或者抛出异常,根据业务需求决定 - } - return serviceAddress + url; - } - return url; - } - -} diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemParameters.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemParameters.java index e7a3686ba307..39442f6ed39c 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemParameters.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemParameters.java @@ -18,6 +18,7 @@ package org.apache.dolphinscheduler.plugin.task.externalSystem; import org.apache.dolphinscheduler.common.utils.JSONUtils; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.ThirdPartySystemConnectorConnectionParam; import org.apache.dolphinscheduler.plugin.task.api.enums.ResourceType; import org.apache.dolphinscheduler.plugin.task.api.parameters.AbstractParameters; import org.apache.dolphinscheduler.plugin.task.api.parameters.resource.DataSourceParameters; @@ -29,8 +30,6 @@ public class ExternalSystemParameters extends AbstractParameters { private int datasource; - private int externalSystemId; - private String authenticationToken; private String externalTaskId; @@ -68,14 +67,6 @@ public void setExternalTaskName(String externalTaskName) { this.externalTaskName = externalTaskName; } - public int getExternalSystemId() { - return externalSystemId; - } - - public void setExternalSystemId(int externalSystemId) { - this.externalSystemId = externalSystemId; - } - @Override public ResourceParametersHelper getResources() { ResourceParametersHelper resources = super.getResources(); @@ -88,13 +79,13 @@ public boolean checkParameters() { return true; } - public BaseExternalSystemParams generateExtendedContext(@NotNull ResourceParametersHelper parametersHelper) { + public ThirdPartySystemConnectorConnectionParam generateExtendedContext(@NotNull ResourceParametersHelper parametersHelper) { DataSourceParameters externalSystemResourceParameters = (DataSourceParameters) parametersHelper.getResourceParameters(ResourceType.DATASOURCE, - externalSystemId); - BaseExternalSystemParams baseExternalSystemParams = + datasource); + ThirdPartySystemConnectorConnectionParam baseExternalSystemParams = JSONUtils.parseObject(externalSystemResourceParameters.getConnectionParams(), - BaseExternalSystemParams.class); + ThirdPartySystemConnectorConnectionParam.class); return baseExternalSystemParams; } diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTask.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTask.java index d915364e2bc2..e2852019b3f4 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTask.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTask.java @@ -22,6 +22,14 @@ import org.apache.dolphinscheduler.common.model.OkHttpResponse; import org.apache.dolphinscheduler.common.utils.JSONUtils; import org.apache.dolphinscheduler.common.utils.OkHttpUtils; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.AuthenticationUtils; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.InterfaceInfo; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.PollingFailureConfig; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.PollingInterfaceInfo; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.PollingSuccessConfig; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.RequestParameter; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.ResponseParameter; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.ThirdPartySystemConnectorConnectionParam; import org.apache.dolphinscheduler.plugin.task.api.AbstractTask; import org.apache.dolphinscheduler.plugin.task.api.TaskCallBack; import org.apache.dolphinscheduler.plugin.task.api.TaskConstants; @@ -52,7 +60,7 @@ public class ExternalSystemTask extends AbstractTask { private Boolean traceEnabled = true; private ExternalSystemParameters externalSystemParameters; - private BaseExternalSystemParams baseExternalSystemParams; + private ThirdPartySystemConnectorConnectionParam baseExternalSystemParams; private TaskExecutionContext taskExecutionContext; private String accessToken; private Map parameterMap = new HashMap<>(); @@ -69,10 +77,12 @@ public ExternalSystemTask(TaskExecutionContext taskExecutionContext) { JSONUtils.parseObject(taskExecutionContext.getTaskParams(), ExternalSystemParameters.class); baseExternalSystemParams = externalSystemParameters.generateExtendedContext(taskExecutionContext.getResourceParametersHelper()); - accessToken = - baseExternalSystemParams.getTokenPrefix(baseExternalSystemParams.getAuthConfig().getHeaderPrefix()) - + baseExternalSystemParams.getAuthConfig().getJwtToken();// todo - // AuthenticationUtils.generateTokenForSystem + try { + accessToken = baseExternalSystemParams.getAuthConfig().getHeaderPrefix() + " " + + AuthenticationUtils.authenticateAndGetToken(baseExternalSystemParams); + } catch (Exception e) { + throw new RuntimeException(e); + } } @Override @@ -80,8 +90,10 @@ public void init() { externalSystemParameters = JSONUtils.parseObject( taskExecutionContext.getTaskParams(), ExternalSystemParameters.class); - log.info("Initialize external system task params {}", - JSONUtils.toPrettyJsonString(externalSystemParameters)); + log.info("Initialize external system task with externalSystemId: {}, externalTaskId: {}, externalTaskName: {}", + externalSystemParameters.getDatasource(), + externalSystemParameters.getExternalTaskId(), + externalSystemParameters.getExternalTaskName()); if (externalSystemParameters == null || !externalSystemParameters.checkParameters()) { throw new RuntimeException("external system task params is not valid"); @@ -142,7 +154,7 @@ public void cancel() throws TaskException { private void submitExternalTask() throws TaskException { try { - BaseExternalSystemParams.InterfaceConfig submitConfig = baseExternalSystemParams.getSubmitInterface(); + InterfaceInfo submitConfig = baseExternalSystemParams.getSubmitInterface(); String url = replaceParameterPlaceholders(baseExternalSystemParams.getCompleteUrl(submitConfig.getUrl())); Map headers = new HashMap<>(); buildAuthHeader(accessToken, headers); @@ -213,7 +225,7 @@ private void trackExternalTaskStatus() throws TaskException { private String pollTaskStatus() throws TaskException { try { - BaseExternalSystemParams.PollingInterfaceConfig pollConfig = + PollingInterfaceInfo pollConfig = baseExternalSystemParams.getPollStatusInterface(); String url = replaceParameterPlaceholders(baseExternalSystemParams.getCompleteUrl(pollConfig.getUrl())); Map headers = new HashMap<>(); @@ -258,7 +270,7 @@ private String pollTaskStatus() throws TaskException { private void cancelTaskInstance() throws TaskException { try { traceEnabled = false; - BaseExternalSystemParams.InterfaceConfig stopConfig = baseExternalSystemParams.getStopInterface(); + InterfaceInfo stopConfig = baseExternalSystemParams.getStopInterface(); log.info("start cancel External System TaskInstance"); String url = replaceParameterPlaceholders(baseExternalSystemParams.getCompleteUrl(stopConfig.getUrl())); Map headers = new HashMap<>(); @@ -283,7 +295,7 @@ private void cancelTaskInstance() throws TaskException { } } - private OkHttpResponse executeRequestWithoutRetry(BaseExternalSystemParams.HttpMethod method, String url, + private OkHttpResponse executeRequestWithoutRetry(InterfaceInfo.HttpMethod method, String url, Map headers, Map requestParams, Map requestBody, int connectTimeout, int readTimeout, @@ -292,7 +304,7 @@ private OkHttpResponse executeRequestWithoutRetry(BaseExternalSystemParams.HttpM writeTimeout, 0); } - private OkHttpResponse executeRequestWithRetry(BaseExternalSystemParams.HttpMethod method, String url, + private OkHttpResponse executeRequestWithRetry(InterfaceInfo.HttpMethod method, String url, Map headers, Map requestParams, Map requestBody, int connectTimeout, int readTimeout, int writeTimeout, int maxRetries) throws TaskException { @@ -355,17 +367,17 @@ private void buildAuthHeader(String accessToken, Map headers) { headers.put("Authorization", accessToken); } - private Map buildHeaders(BaseExternalSystemParams.InterfaceConfig config, + private Map buildHeaders(InterfaceInfo config, Map requestParams) { - for (BaseExternalSystemParams.RequestParameter param : config.getParameters()) { - if (param.getLocation().equals(BaseExternalSystemParams.ParamLocation.HEADER)) { + for (RequestParameter param : config.getParameters()) { + if (param.getLocation().equals(RequestParameter.ParamLocation.HEADER)) { requestParams.put(param.getParamName(), replaceParameterPlaceholders(param.getParamValue())); } } return requestParams; } - private Map buildRequestBody(BaseExternalSystemParams.InterfaceConfig config) { + private Map buildRequestBody(InterfaceInfo config) { Map requestBody = new HashMap<>(); if (config.getBody() != null) { requestBody = JSONUtils.parseObject(replaceParameterPlaceholders(config.getBody()), Map.class); @@ -373,10 +385,10 @@ private Map buildRequestBody(BaseExternalSystemParams.InterfaceC return requestBody; } - private Map buildRequestParams(BaseExternalSystemParams.InterfaceConfig config) { + private Map buildRequestParams(InterfaceInfo config) { Map requestParams = new HashMap<>(); - for (BaseExternalSystemParams.RequestParameter param : config.getParameters()) { - if (param.getLocation().equals(BaseExternalSystemParams.ParamLocation.PARAM)) { + for (RequestParameter param : config.getParameters()) { + if (param.getLocation().equals(RequestParameter.ParamLocation.PARAM)) { requestParams.put(param.getParamName(), replaceParameterPlaceholders(param.getParamValue())); } } @@ -401,10 +413,10 @@ private String replaceParameterPlaceholders(String template) { return resultString; } - private void parseSubmitResponse(List responseParameters, + private void parseSubmitResponse(List responseParameters, String responseBody) throws TaskException { try { - for (BaseExternalSystemParams.ResponseParameter param : responseParameters) { + for (ResponseParameter param : responseParameters) { String jsonPath = param.getJsonPath(); String key = param.getKey(); Object value = JsonPath.read(responseBody, jsonPath); @@ -446,7 +458,7 @@ private void initParameterMap() { } private void initStatusCache() { - BaseExternalSystemParams.PollingSuccessConfig successConfig = + PollingSuccessConfig successConfig = baseExternalSystemParams.getPollStatusInterface().getPollingSuccessConfig(); if (successConfig != null && successConfig.getSuccessValue() != null) { @@ -462,7 +474,7 @@ private void initStatusCache() { log.error("Error: successValue is null"); } } - BaseExternalSystemParams.PollingFailureConfig failureConfig = + PollingFailureConfig failureConfig = baseExternalSystemParams.getPollStatusInterface().getPollingFailureConfig(); if (failureConfig != null && failureConfig.getFailureField() != null) { try { diff --git a/dolphinscheduler-ui/src/locales/en_US/thirdparty-api-source.ts b/dolphinscheduler-ui/src/locales/en_US/thirdparty-api-source.ts index 0c79c2c79df8..4c176e0722b4 100644 --- a/dolphinscheduler-ui/src/locales/en_US/thirdparty-api-source.ts +++ b/dolphinscheduler-ui/src/locales/en_US/thirdparty-api-source.ts @@ -66,6 +66,8 @@ export default { extract_response_data: 'Please enter response data jsonPath', extract_field: 'Please enter extract field', json_path: 'Please enter json path', + header_prefix:'Authorization Token Prefix', + header_prefix_tips: 'Authorization header before token,e.g.Bearer', system_field_tips: 'Please select system field', request_body: 'Request Body', header: 'Header', @@ -83,14 +85,14 @@ export default { polling_config: 'Polling Configuration', success_condition: 'Success Condition', success_field: 'Success Field', - success_field_tips: 'Please enter success field', + success_field_tips: 'Please enter success field JSONPath,e.g.$.data.status', success_value: 'Success Value', - success_value_tips: 'Please enter success value', + success_value_tips: 'Please enter all enum values for success,e.g.SUCCESS,FINISHED', failure_condition: 'Failure Condition', failure_field: 'Failure Field', - failure_field_tips: 'Please enter failure field', + failure_field_tips: 'Please enter failure field JSONPath,e.g.$.data.status', failure_value: 'Failure Value', - failure_value_tips: 'Please enter failure value', + failure_value_tips: 'Please enter all enum values for failure,e.g.CANCELED, FAILED', // Buttons and Operations cancel: 'Cancel', diff --git a/dolphinscheduler-ui/src/locales/zh_CN/thirdparty-api-source.ts b/dolphinscheduler-ui/src/locales/zh_CN/thirdparty-api-source.ts index bd33665e3e45..026fbbfaa03e 100644 --- a/dolphinscheduler-ui/src/locales/zh_CN/thirdparty-api-source.ts +++ b/dolphinscheduler-ui/src/locales/zh_CN/thirdparty-api-source.ts @@ -63,10 +63,12 @@ export default { param_location_tips: '请选择参数位置', param_name_tips: '请输入参数名', param_value_tips: '请输入参数值', - extract_response_data: '提取响应数据并存储变量', + extract_response_data: '提取响应并存储变量', extract_field: '请输入提取参数', json_path_list: '请输入,例如:$.data[*].id', json_path: '请输入,例如$.data.taskInstanceId', + header_prefix:'Authorization Token前缀', + header_prefix_tips: 'Authorization Token前缀,e.g.Bearer', system_field_tips: '请选择系统字段', request_body: '请求Body', header: '请求头', @@ -83,14 +85,14 @@ export default { polling_config: '轮询配置', success_condition: '成功条件', success_field: '成功字段', - success_field_tips: '请输入成功字段', + success_field_tips: '成功字段JSONPath:$.data.status', success_value: '成功值', - success_value_tips: '请输入成功值', + success_value_tips: '成功值枚举值:SUCCESS,FINISHED', failure_condition: '失败条件', failure_field: '失败字段', - failure_field_tips: '请输入失败字段', + failure_field_tips: '失败字段JSONPath:$.data.status', failure_value: '失败值', - failure_value_tips: '请输入失败值', + failure_value_tips: '失败值所有枚举值:CANCELED,FAILED', // 按钮和操作 cancel: '取消', @@ -128,13 +130,12 @@ export default { submit_interface_url_required: '提交接口地址为必填项', query_interface_url_required: '查询接口地址为必填项', stop_interface_url_required: '停止接口地址为必填项', - success_condition_required: '成功条件字段和值不能为空', - failure_condition_required: '失败条件字段和值不能为空', + success_condition_required: '成功条件字段路径和值不能为空', + failure_condition_required: '失败条件字段路径和值不能为空', external_system_required: '第三方系统不能为空', external_system_task_required: '第三方系统任务不能为空', id_jsonpath_required: '字段id以及JSONPath为必填项', name_jsonpath_required: '字段name以及JSONPath为必填项', - taskinstanceid_jsonpath_required: '字段taskInstanceId以及JSONPath为必填项' } \ No newline at end of file diff --git a/dolphinscheduler-ui/src/service/modules/thirdparty-api-source/index.ts b/dolphinscheduler-ui/src/service/modules/thirdparty-api-source/index.ts index f3ba6b51943f..6358515a2f05 100644 --- a/dolphinscheduler-ui/src/service/modules/thirdparty-api-source/index.ts +++ b/dolphinscheduler-ui/src/service/modules/thirdparty-api-source/index.ts @@ -4,7 +4,6 @@ import { ThirdpartyApiSource, } from './types' -// 分页查询 export function queryThirdpartyApiSourceListPaging(params: Partial): Promise { return axios({ url: '/external-systems', @@ -13,88 +12,6 @@ export function queryThirdpartyApiSourceListPaging(params: Partial { - return axios({ - url: '/external-systems/', - method: 'post', - data, - headers: { - 'Content-Type': 'application/json;charset=UTF-8' - }, - transformRequest: (params) => JSON.stringify(params) - }) -} - -// 测试连接 -export function testThirdpartyApiSourceConnection(data: ThirdpartyApiSource): Promise { - return axios({ - url: '/external-systems/test-connection', - method: 'post', - data, - headers: { - 'Content-Type': 'application/json;charset=UTF-8' - }, - transformRequest: (params) => JSON.stringify(params) - }) -} -// 更新 -export function updateThirdpartyApiSource(id: number, data: ThirdpartyApiSource): Promise { - return axios({ - url: `/external-systems/${id}`, - method: 'put', - data, - headers: { - 'Content-Type': 'application/json;charset=UTF-8' - }, - transformRequest: (params) => JSON.stringify(params) - }) -} -// 删除 -export function deleteThirdpartyApiSource(id: number): Promise { - return axios({ - url: `/external-systems/${id}`, - method: 'delete' - }) -} -// 查询详情 -export function getThirdpartyApiSourceById(id: number): Promise { - return axios({ - url: `/external-systems/${id}`, - method: 'get' - }) -} - -// 查询已授权第三方系统 -export function authedThirdpartySystem(params: { userId: number }): any { - return axios({ - url: '/external-systems/authed-externalSystem', - method: 'get', - params - }) -} - -// 查询未授权第三方系统 -export function unAuthThirdpartySystem(params: { userId: number }): any { - return axios({ - url: '/external-systems/unauth-externalSystem', - method: 'get', - params - }) -} - -// 授权第三方系统 -export function grantThirdpartySystem(data: { userId: number, externalSystemIds: string }): any { - const formData = new URLSearchParams() - formData.append('userId', String(data.userId)) - formData.append('externalSystemIds', data.externalSystemIds) - return axios({ - url: '/users/grant-externalSystem', - method: 'post', - data: formData, - headers: { 'Content-Type': 'application/x-www-form-urlencoded' } - }) -} \ No newline at end of file diff --git a/dolphinscheduler-ui/src/views/datasource/list/detail.tsx b/dolphinscheduler-ui/src/views/datasource/list/detail.tsx index 2bf4df7753cc..8961b42fa26d 100644 --- a/dolphinscheduler-ui/src/views/datasource/list/detail.tsx +++ b/dolphinscheduler-ui/src/views/datasource/list/detail.tsx @@ -117,7 +117,7 @@ const DetailModal = defineComponent({ } ), - //monitor authType change,update headerPrefix + // Monitor authType change, update headerPrefix watch( () => state.detailForm.authConfig?.authType, (newAuthType) => { @@ -392,7 +392,7 @@ const DetailModal = defineComponent({ placeholder={t('datasource.krb5_conf_tips')} /> - {/* 验证条件选择 */} + {/* validation */} - {/* THIRDPARTY_SYSTEM_CONNECTOR 特殊字段 */} + {/* THIRDPARTY_SYSTEM_CONNECTOR */} {detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && ( <> - - - )} - {/* 额外参数 */} + {/* additional params */}
- {/* 添加按钮 */} + {/* add button */} { if (!detailForm.authConfig.authMappings) { @@ -976,7 +966,7 @@ const DetailModal = defineComponent({ {t('thirdparty_api_source.add_param')} - {/* 参数列表 */} + {/* param list */} {detailForm.authConfig.authMappings && detailForm.authConfig.authMappings.map((param: { key: string; value: string }, index: number) => (
- {/* 添加按钮 */} + {/* add Button*/} { if (!detailForm.selectInterface.parameters) { @@ -1042,7 +1032,7 @@ const DetailModal = defineComponent({ {t('thirdparty_api_source.add_param')} - {/* 参数列表 */} + {/* parameter list */} {detailForm.selectInterface.parameters && detailForm.selectInterface.parameters.map((param: { paramName: string; paramValue: string; location: string }, index: number) => (
- {/* 添加按钮 */} + {/* add Button */} { if (!detailForm.selectInterface.responseParameters) { @@ -1107,7 +1097,7 @@ const DetailModal = defineComponent({ {t('thirdparty_api_source.add_extract_field')} - {/* 参数列表 */} + {/* responseParameters */} {detailForm.selectInterface.responseParameters && detailForm.selectInterface.responseParameters.map((param: { key: string; jsonPath: string; disabled: boolean }, index: number) => (
- {/* 添加按钮 */} + {/* add button */} { if (!detailForm.submitInterface.parameters) { @@ -1173,7 +1163,7 @@ const DetailModal = defineComponent({ {t('thirdparty_api_source.add_param')} - {/* 参数列表 */} + {/* parameter list */} {detailForm.submitInterface.parameters && detailForm.submitInterface.parameters.map((param: { paramName: string; paramValue: string; location: string }, index: number) => (
- {/* 添加按钮 */} + {/* add button */} { if (!detailForm.submitInterface.responseParameters) { @@ -1238,7 +1228,7 @@ const DetailModal = defineComponent({ {t('thirdparty_api_source.add_extract_field')} - {/* 参数列表 */} + {/* param list */} {detailForm.submitInterface.responseParameters && detailForm.submitInterface.responseParameters.map((param: { key: string; jsonPath: string; disabled: boolean }, index: number) => (
- {/* 添加按钮 */} + {/* add button */} { if (!detailForm.pollStatusInterface.parameters) { @@ -1304,7 +1294,7 @@ const DetailModal = defineComponent({ {t('thirdparty_api_source.add_param')} - {/* 参数列表 */} + {/* param list */} {detailForm.pollStatusInterface.parameters && detailForm.pollStatusInterface.parameters.map((param: { paramName: string; paramValue: string; location: string }, index: number) => (
- {/* 添加按钮 */} + {/* add button */} { if (!detailForm.pollStatusInterface.responseParameters) { @@ -1369,7 +1359,7 @@ const DetailModal = defineComponent({ {t('thirdparty_api_source.add_extract_field')} - {/* 参数列表 */} + {/* param list */} {detailForm.pollStatusInterface.responseParameters && detailForm.pollStatusInterface.responseParameters.map((param: { key: string; jsonPath: string; disabled: boolean }, index: number) => (
- {/* 添加按钮 */} + {/* add button */} { if (!detailForm.stopInterface.parameters) { @@ -1463,7 +1453,7 @@ const DetailModal = defineComponent({ {t('thirdparty_api_source.add_param')} - {/* 参数列表 */} + {/* param list */} {detailForm.stopInterface.parameters && detailForm.stopInterface.parameters.map((param: { paramName: string; paramValue: string; location: string }, index: number) => (
- {/* 添加按钮 */} + {/* add button */} { if (!detailForm.stopInterface.responseParameters) { @@ -1528,7 +1518,7 @@ const DetailModal = defineComponent({ {t('thirdparty_api_source.add_extract_field')} - {/* 参数列表 */} + {/* param list */} {detailForm.stopInterface.responseParameters && detailForm.stopInterface.responseParameters.map((param: { key: string; jsonPath: string; disabled: boolean }, index: number) => (
{ other?: string // THIRDPARTY_SYSTEM_CONNECTOR - systemName?: string serviceAddress?: string interfaceTimeout?: number authConfig?: AuthConfig diff --git a/dolphinscheduler-ui/src/views/datasource/list/use-form.ts b/dolphinscheduler-ui/src/views/datasource/list/use-form.ts index 22cea164eca9..a415b0f4d6b3 100644 --- a/dolphinscheduler-ui/src/views/datasource/list/use-form.ts +++ b/dolphinscheduler-ui/src/views/datasource/list/use-form.ts @@ -53,7 +53,6 @@ export function useForm(id?: number) { dbUser: '', datawarehouse: '', // THIRDPARTY_SYSTEM_CONNECTOR - systemName: '', serviceAddress: 'http://', interfaceTimeout: 120000, authConfig: { @@ -269,14 +268,6 @@ export function useForm(id?: number) { } }, // THIRDPARTY_SYSTEM_CONNECTOR check rule - systemName: { - trigger: ['input'], - validator() { - if (state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && !state.detailForm.systemName) { - return new Error(t('thirdparty_api_source.system_name_required')) - } - } - }, serviceAddress: { trigger: ['input'], validator() { diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-external-system.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-external-system.ts index 99e32e7c0600..9256f8d77219 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-external-system.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-external-system.ts @@ -51,7 +51,7 @@ export function useExternalSystem( } const refreshTasks = async () => { - const datasourceId = model[params.externalSystemField || 'externalSystemId'] + const datasourceId = model[params.externalSystemField || 'datasource'] if (!datasourceId) return try { @@ -75,15 +75,24 @@ export function useExternalSystem( } const onChange = () => { - // 清空任务选项 taskOptions.value = [] - // 清空模型中的任务字段 const taskField = params.taskField || 'externalTaskId' model[taskField] = null - // 刷新任务选项 + model.externalTaskName = '' refreshTasks() } + const onTaskChange = (value: string) => { + if (value) { + const taskItem = taskOptions.value.find(item => item.value === value) + if (taskItem) { + model.externalTaskName = taskItem.label // Set the name based on the selected task + } + } else { + model.externalTaskName = '' + } + } + onMounted(async () => { await getDataSources() await nextTick() @@ -93,7 +102,7 @@ export function useExternalSystem( return [ { type: 'select', - field: params.externalSystemField || 'externalSystemId', + field: params.externalSystemField || 'datasource', span: params.span || 24, name: t('project.node.datasource_instances'), props: { 'on-update:value': onChange }, @@ -113,6 +122,7 @@ export function useExternalSystem( field: params.taskField || 'externalTaskId', span: params.span || 24, name: t('project.node.external_system_tasks'), + props: { 'on-update:value': onTaskChange }, options: taskOptions, validate: { trigger: ['input', 'blur'], diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/format-data.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/format-data.ts index 682ef9541a12..8a91107d2ad4 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/format-data.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/format-data.ts @@ -116,8 +116,10 @@ export function formatParams(data: INodeData): { taskParams.socketTimeout = data.socketTimeout } if (data.taskType === 'EXTERNAL_SYSTEM') { - taskParams.externalSystemId = data.externalSystemId + taskParams.type = data.type + taskParams.datasource = data.datasource taskParams.externalTaskId = data.externalTaskId + taskParams.externalTaskName = data.externalTaskName } if (data.taskType === 'SQOOP') { diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/use-external-system.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/use-external-system.ts index ad1c9a0776f1..260de545045f 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/use-external-system.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/use-external-system.ts @@ -47,8 +47,10 @@ export function useExternalSystem({ cpuQuota: -1, memoryMax: -1, delayTime: 0, - externalSystemId: '', - externalTaskId: '' + datasource: '', + type: 'THIRDPARTY_SYSTEM_CONNECTOR', + externalTaskId: '', + externalTaskName: '' } as INodeData) return { diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/types.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/types.ts index bbd15a994c22..e69122c507b6 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/types.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/types.ts @@ -302,7 +302,7 @@ interface ITaskParams { connectTimeout?: number socketTimeout?: number type?: string - externalSystemId?: string + externalTaskName?: string externalTaskId?: string datasource?: string sql?: string diff --git a/dolphinscheduler-ui/src/views/thirdparty-api-source/modal.tsx b/dolphinscheduler-ui/src/views/thirdparty-api-source/modal.tsx index 7f97fe6b1a1b..772a6dc1fdbc 100644 --- a/dolphinscheduler-ui/src/views/thirdparty-api-source/modal.tsx +++ b/dolphinscheduler-ui/src/views/thirdparty-api-source/modal.tsx @@ -46,9 +46,8 @@ export default defineComponent({ ]) const form = reactive({ - systemName: '', serviceAddress: '', - interfaceTimeout: 120000, // 默认2分钟 + interfaceTimeout: 120000, // default 2 min authConfig: { authType: 'BASIC_AUTH', basicUsername: '', @@ -104,11 +103,8 @@ export default defineComponent({ } }) - // 表单校验规则 + // Form validation rules const rules = { - systemName: [ - { required: true, message: t('thirdparty_api_source.system_name_required'), trigger: 'blur' } - ], serviceAddress: [ { required: true, message: t('thirdparty_api_source.service_address_required'), trigger: 'blur' } ], @@ -248,11 +244,10 @@ export default defineComponent({ const formRef = ref(null) const isEditMode = computed(() => props.operationType === 'edit') - // 定义表单的初始状态 + // Define the initial state of the form const getInitialFormState = () => ({ - systemName: '', serviceAddress: 'http://', - interfaceTimeout: 120000, // 默认2分钟 + interfaceTimeout: 120000, // default 2 minutes authConfig: { authType: '', headerPrefix: '', @@ -297,7 +292,7 @@ export default defineComponent({ stopInterface: { url: '', method: 'POST', parameters: [] as any[], body: '' } }) - // 重置表单数据的函数 + // Function to reset form data const resetForm = () => { const initialState = getInitialFormState() Object.keys(form).forEach(key => { @@ -307,21 +302,21 @@ export default defineComponent({ formRef.value?.restoreValidation?.() } - // 保存原始编辑数据,用于测试连接 + // Save original edit data for testing connection const originalEditData = ref(null) - // 监听modal显示状态和数据变化 + // Listen for modal visibility and data changes watch([() => props.show, () => props.data, () => props.operationType], ([show, data, operationType]) => { if (show) { if (data && operationType === 'edit') { originalEditData.value = JSON.parse(JSON.stringify(data)) resetForm() const editData = originalEditData.value - // 完全使用后端返回的数据 + // Use data returned from backend completely Object.assign(form, editData) } else { originalEditData.value = null resetForm() - // 只在创建模式下设置默认值 + // Only set default values in create mode form.authConfig.authType = 'BASIC_AUTH' form.authConfig.headerPrefix = 'Basic' } @@ -329,7 +324,7 @@ export default defineComponent({ }, { immediate: true }) watch(() => form.authConfig.authType, (newAuthType) => { - // 只在创建模式下自动设置headerPrefix + // Only automatically set headerPrefix in create mode if (!isEditMode.value) { if (newAuthType === 'BASIC_AUTH') { form.authConfig.headerPrefix = 'Basic' @@ -384,7 +379,7 @@ export default defineComponent({ }) } - // location 下拉选项与 method 联动 + // Location dropdown options linked with method const getLocationOptions = (method: string) => { return [ { label: 'Header', value: 'HEADER' }, @@ -416,16 +411,6 @@ export default defineComponent({ rules={rules} ref={formRef} > - - - - {/* 接口超时时间 */} + {/* Interface timeout */} - {/* 认证类型 */} + {/* authType */} {{ label: () => ( @@ -628,7 +613,7 @@ export default defineComponent({ /> - {/* 额外参数 */} + {/* additional params */} - {/* 输入接口 */} + {/* selectInterface */} {{ label: () => ( @@ -818,7 +803,7 @@ export default defineComponent({ - {/* 提交接口 */} + {/* submitInterface */} {{ label: () => ( @@ -981,7 +966,7 @@ export default defineComponent({ - {/* 查询接口 */} + {/* pollStatusInterface */} {{ label: () => ( @@ -1170,7 +1155,7 @@ export default defineComponent({ - {/* 停止接口 */} + {/* stopInterface */} {{ label: () => ( From 1dbb6786ed9df5305f78175693c49b9b1b58c919 Mon Sep 17 00:00:00 2001 From: yud8 Date: Wed, 24 Dec 2025 15:46:12 +0800 Subject: [PATCH 04/11] [Feature-17501]Third-party System API Connector fix CI error --- docs/docs/en/guide/task/external-system.md | 24 ++++ docs/docs/zh/guide/task/external-system.md | 24 ++++ .../locales/en_US/thirdparty-api-source.ts | 17 +++ .../locales/zh_CN/thirdparty-api-source.ts | 25 ++-- .../modules/thirdparty-api-source/index.ts | 17 +++ .../components/node/fields/use-datasource.ts | 2 +- .../node/fields/use-external-system.ts | 6 +- .../thirdparty-api-source/index.module.scss | 134 +++--------------- .../src/views/thirdparty-api-source/index.tsx | 21 ++- .../src/views/thirdparty-api-source/modal.tsx | 26 +++- 10 files changed, 161 insertions(+), 135 deletions(-) create mode 100644 docs/docs/en/guide/task/external-system.md create mode 100644 docs/docs/zh/guide/task/external-system.md diff --git a/docs/docs/en/guide/task/external-system.md b/docs/docs/en/guide/task/external-system.md new file mode 100644 index 000000000000..2c82d8b3d054 --- /dev/null +++ b/docs/docs/en/guide/task/external-system.md @@ -0,0 +1,24 @@ +# External System Task + +The External System task is used to call external system APIs and integrate with third-party systems. This task type allows DolphinScheduler to interact with external services, execute remote operations, and retrieve results. + +## Introduction + +External System tasks provide a way to execute operations in external systems through REST APIs calls. These tasks can be configured to authenticate with external systems, send requests, and process responses. + +## Configuration + +- **Task Name**: The name of the task. +- **Task Type**: Select "External System" from the task type list. +- **External System**: Select the external system to connect to. +- **API Endpoint**: The API endpoint to call in the external system. +- **Authentication**: Configure authentication parameters for the external system. +- **Request Method**: The HTTP method to use (GET, POST, PUT, DELETE, etc.). +- **Request Parameters**: Parameters to send with the request. +- **Response Processing**: How to process the response from the external system. + +## Example + +![External System Task](../../../../img/tasks/icons/external_system.png) + +This is an example of how to configure an External System task in the workflow. diff --git a/docs/docs/zh/guide/task/external-system.md b/docs/docs/zh/guide/task/external-system.md new file mode 100644 index 000000000000..8c63b808f1fd --- /dev/null +++ b/docs/docs/zh/guide/task/external-system.md @@ -0,0 +1,24 @@ +# 外部系统任务 + +外部系统任务用于调用外部系统API并与第三方系统集成。这种任务类型允许DolphinScheduler与外部服务交互,执行远程操作并检索结果。 + +## 介绍 + +外部系统任务提供了一种通过REST APIs调用与外部系统交互的方式。这些任务可以配置为对第三方系统进行身份验证,发送请求并处理响应。 + +## 配置 + +- **任务名称**: 任务的名称。 +- **任务类型**: 从任务类型列表中选择"外部系统"。 +- **外部系统**: 选择要连接的外部系统。 +- **API端点**: 在外部系统中调用的API端点。 +- **认证**: 配置外部系统的认证参数。 +- **请求方法**: 要使用的HTTP方法(GET、POST、PUT、DELETE等)。 +- **请求参数**: 与请求一起发送的参数。 +- **响应处理**: 如何处理来自外部系统的响应。 + +## 示例 + +![外部系统任务](../../../../img/tasks/icons/external_system.png) + +这是在工作流程中配置外部系统任务的示例。 diff --git a/dolphinscheduler-ui/src/locales/en_US/thirdparty-api-source.ts b/dolphinscheduler-ui/src/locales/en_US/thirdparty-api-source.ts index 4c176e0722b4..d4de7d55af10 100644 --- a/dolphinscheduler-ui/src/locales/en_US/thirdparty-api-source.ts +++ b/dolphinscheduler-ui/src/locales/en_US/thirdparty-api-source.ts @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + export default { // Route titles thirdparty_api_source: 'Thirdparty API Source', diff --git a/dolphinscheduler-ui/src/locales/zh_CN/thirdparty-api-source.ts b/dolphinscheduler-ui/src/locales/zh_CN/thirdparty-api-source.ts index 026fbbfaa03e..15c7e96cb576 100644 --- a/dolphinscheduler-ui/src/locales/zh_CN/thirdparty-api-source.ts +++ b/dolphinscheduler-ui/src/locales/zh_CN/thirdparty-api-source.ts @@ -1,10 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + export default { - // 路由标题 thirdparty_api_source: '第三方系统API', create_thirdparty_api_source: '创建第三方系统API', edit_thirdparty_api_source: '编辑第三方系统API', - // 基本信息 basic_info: '基本信息', id: 'ID', system_name: '系统名称', @@ -17,7 +32,6 @@ export default { create_time: '创建时间', update_time: '更新时间', - // 认证配置 auth_config: '认证配置', auth_type: '认证类型', auth_type_tips: '请选择认证类型', @@ -48,7 +62,6 @@ export default { key: '键名', value: '键值', - // 接口配置 interface_config: '接口配置', input_interface: '查询任务列表接口', input_interface_tips: '请输入接口地址', @@ -81,7 +94,6 @@ export default { millisecond: '毫秒', interface_timeout_description: '设置接口请求超时时间,默认为120000毫秒(2分钟)', - // 轮询配置 polling_config: '轮询配置', success_condition: '成功条件', success_field: '成功字段', @@ -94,7 +106,6 @@ export default { failure_value: '失败值', failure_value_tips: '失败值所有枚举值:CANCELED,FAILED', - // 按钮和操作 cancel: '取消', submit: '确定', test: '测试连接', @@ -104,7 +115,6 @@ export default { edit: '编辑', delete: '删除', - // 消息提示 create_success: '创建成功', edit_success: '编辑成功', delete_success: '删除成功', @@ -115,7 +125,6 @@ export default { test_failed: '测试失败', submit_failed: '提交失败,请检查表单内容', - // 表单验证消息 system_name_required: '系统名称为必填项', service_address_required: '服务地址为必填项', auth_type_required: '认证类型为必选项', diff --git a/dolphinscheduler-ui/src/service/modules/thirdparty-api-source/index.ts b/dolphinscheduler-ui/src/service/modules/thirdparty-api-source/index.ts index 6358515a2f05..8cbb822ff9fc 100644 --- a/dolphinscheduler-ui/src/service/modules/thirdparty-api-source/index.ts +++ b/dolphinscheduler-ui/src/service/modules/thirdparty-api-source/index.ts @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { axios } from '@/service/service' import { ThirdpartyApiSourceReq, diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts index 80eee267556d..c0c21134bb11 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts @@ -17,7 +17,7 @@ import { ref, onMounted, nextTick, Ref } from 'vue' import { useI18n } from 'vue-i18n' -import { queryDataSourceList, queryExternalSystemList, queryExternalSystemTasks } from '@/service/modules/data-source' +import { queryDataSourceList } from '@/service/modules/data-source' import { indexOf, find } from 'lodash' import type { IJsonItem } from '../types' import type { TypeReq } from '@/service/modules/data-source/types' diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-external-system.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-external-system.ts index 9256f8d77219..2749a29bf15b 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-external-system.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-external-system.ts @@ -17,7 +17,7 @@ import { ref, onMounted, nextTick, Ref } from 'vue' import { useI18n } from 'vue-i18n' import { queryDataSourceList, queryExternalSystemTasks } from '@/service/modules/data-source' -import { indexOf, find } from 'lodash' +import { find } from 'lodash' import type { IJsonItem } from '../types' import type { TypeReq } from '@/service/modules/data-source/types' @@ -46,7 +46,7 @@ export function useExternalSystem( value: String(item.id) })) } catch (error) { - console.error('Error fetching data sources:', error) + // Error handling is done by the calling function } } @@ -61,7 +61,7 @@ export function useExternalSystem( value: String(item.id) })) } catch (error) { - console.error('Error fetching external system tasks:', error) + // Error handling is done by the calling function } const taskField = params.taskField || 'task' diff --git a/dolphinscheduler-ui/src/views/thirdparty-api-source/index.module.scss b/dolphinscheduler-ui/src/views/thirdparty-api-source/index.module.scss index 4879d138674a..d32b36e059c6 100644 --- a/dolphinscheduler-ui/src/views/thirdparty-api-source/index.module.scss +++ b/dolphinscheduler-ui/src/views/thirdparty-api-source/index.module.scss @@ -1,4 +1,20 @@ -// 模态框基础样式 +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + .thirdparty-modal { position: relative; width: 700px; @@ -8,7 +24,6 @@ overflow: hidden; } -// 隐藏模态框关闭按钮 .thirdparty-modal :global(.n-modal__close), .thirdparty-modal :global(.n-modal-header__close), .thirdparty-modal :global(.n-modal-header .n-button), @@ -21,7 +36,6 @@ display: none !important; } -// 模态框内容区域 .modal-content { flex: 1; max-height: calc(90vh - 120px); @@ -32,95 +46,6 @@ box-sizing: border-box; } -// 表单基础样式 -.modal-content :global(.n-form) { - width: 100%; - box-sizing: border-box; -} - -.modal-content :global(.n-form-item) { - width: 100%; - margin-bottom: 16px; - box-sizing: border-box; -} - -.modal-content :global(.n-form-item__content) { - display: flex; - align-items: flex-start; - flex-wrap: wrap; - gap: 8px; - width: 100%; - box-sizing: border-box; -} - -.modal-content :global(.n-form-item__label) { - min-width: 120px; - text-align: right; - padding-right: 12px; - box-sizing: border-box; -} - -// 输入框和选择器基础样式 -.modal-content :global(.n-input), -.modal-content :global(.n-select) { - box-sizing: border-box; -} - -// 空间组件 -.modal-content :global(.n-space) { - flex-wrap: wrap; - gap: 8px; - width: 100%; - box-sizing: border-box; -} - -// 动态输入组件 -.modal-content :global(.n-dynamic-input) { - width: 100%; - box-sizing: border-box; -} - -.modal-content :global(.n-dynamic-input .n-dynamic-input-item) { - width: 100%; - margin-bottom: 12px; - display: flex; - align-items: center; - box-sizing: border-box; -} - -.modal-content :global(.n-dynamic-input .n-dynamic-input-item .n-space) { - width: 100%; - justify-content: flex-start; - flex: 1; - box-sizing: border-box; -} - -.modal-content :global(.n-dynamic-input .n-dynamic-input-item .n-button) { - margin-left: 8px; - flex-shrink: 0; - box-sizing: border-box; -} - -.modal-content :global(.n-dynamic-input .n-dynamic-input-item .n-input) { - flex: 1; - min-width: 0; - box-sizing: border-box; -} - -.modal-content :global(.n-dynamic-input .n-dynamic-input-item .n-select) { - flex-shrink: 0; - box-sizing: border-box; -} - -// 字段映射特殊样式 -.modal-content :global(.n-dynamic-input .n-dynamic-input-item .n-space .n-input) { - flex: 1; - width: 50%; - min-width: 200px; - box-sizing: border-box; -} - -// 参数输入框样式 .param-location { width: 100px; min-width: 100px; @@ -136,16 +61,12 @@ min-width: 180px; } - - -// 选择器样式 .method-select, .auth-type-select { width: 120px; min-width: 120px; } -// 条件输入框样式 .condition-field { width: calc(50% - 4px); min-width: 200px; @@ -157,7 +78,6 @@ margin-left: 8px; } -// 提交接口样式 .submit-url { flex: 1; min-width: 300px; @@ -168,7 +88,6 @@ min-width: 120px; } -// 超时时间输入框样式 .timeout-input { width: 200px; } @@ -180,30 +99,12 @@ line-height: 1.4; } -// 键值对输入框样式 .key-input, .value-input { width: 200px; min-width: 150px; } -// Monaco编辑器样式 -.modal-content :global(.monaco-editor) { - height: 200px !important; - min-height: 200px !important; -} - -.modal-content :global(.monaco-editor .monaco-editor-background) { - background-color: #fafafa !important; -} - -// 分割线样式 -.modal-content :global(.n-divider) { - margin: 20px 0; - border-color: #f0f0f0; -} - -// 底部按钮区域 .modal-footer { position: sticky; bottom: 0; @@ -229,7 +130,6 @@ padding: 0 14px; } -// 响应式设计 @media screen and (min-width: 1920px) { .thirdparty-modal { width: 700px; diff --git a/dolphinscheduler-ui/src/views/thirdparty-api-source/index.tsx b/dolphinscheduler-ui/src/views/thirdparty-api-source/index.tsx index 67be57bf18f5..0315e3a513c4 100644 --- a/dolphinscheduler-ui/src/views/thirdparty-api-source/index.tsx +++ b/dolphinscheduler-ui/src/views/thirdparty-api-source/index.tsx @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { defineComponent, ref, onMounted, computed } from 'vue' import { NDataTable, @@ -16,13 +33,11 @@ import Card from '@/components/card' import Search from '@/components/input-search' import { EditOutlined, DeleteOutlined } from '@vicons/antd' import ThirdpartyApiSourceModal from './modal' -import { de } from 'date-fns/locale' export default defineComponent({ name: 'ThirdpartyApiSourceList', setup() { const { t } = useI18n() - const router = useRouter() const tableData = ref([]) const loading = ref(false) @@ -104,7 +119,7 @@ export default defineComponent({ : `${t('datasource.test_connect')} ${t('datasource.success')}` ) } catch (e: any) { - console.log(e) + // Error handling is done by the calling function } } diff --git a/dolphinscheduler-ui/src/views/thirdparty-api-source/modal.tsx b/dolphinscheduler-ui/src/views/thirdparty-api-source/modal.tsx index 772a6dc1fdbc..e71d71d86a8c 100644 --- a/dolphinscheduler-ui/src/views/thirdparty-api-source/modal.tsx +++ b/dolphinscheduler-ui/src/views/thirdparty-api-source/modal.tsx @@ -1,3 +1,20 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + import { defineComponent, PropType, reactive, watch, computed, ref } from 'vue' import type { FormInst } from 'naive-ui' import { useI18n } from 'vue-i18n' @@ -23,7 +40,10 @@ export default defineComponent({ name: 'ThirdpartyApiSourceModal', props: { show: Boolean, - data: Object as PropType, + data: { + type: Object as PropType, + default: () => null + }, operationType: { type: String as PropType<'create' | 'edit'>, default: 'create' @@ -380,7 +400,7 @@ export default defineComponent({ } // Location dropdown options linked with method - const getLocationOptions = (method: string) => { + const getLocationOptions = (_method: string) => { return [ { label: 'Header', value: 'HEADER' }, { label: 'Param', value: 'PARAM' } @@ -395,7 +415,7 @@ export default defineComponent({ closeOnEsc={false} maskClosable={false} preset="card" - class={[styles['thirdparty-modal'], 'dialog-source-modal']} + class={[styles['thirdparty-modal'], 'thirdparty-modal']} title={ isEditMode.value ? t('thirdparty_api_source.edit_thirdparty_api_source') From 7a37ab28868a6c56fab189fe286e8e9ccfbe652d Mon Sep 17 00:00:00 2001 From: yud8 Date: Wed, 7 Jan 2026 15:12:24 +0800 Subject: [PATCH 05/11] [Feature-17501]Third-party System APl Connector fix UI CI error --- .../ExternalSystemParameters.java | 1 + .../externalSystem/ExternalSystemTask.java | 19 +- .../locales/en_US/thirdparty-api-source.ts | 26 +- .../locales/zh_CN/thirdparty-api-source.ts | 15 +- .../src/service/modules/data-source/index.ts | 8 +- .../src/service/modules/data-source/types.ts | 89 + .../modules/thirdparty-api-source/index.ts | 34 - .../modules/thirdparty-api-source/types.ts | 105 - .../src/store/project/task-type.ts | 3 +- .../src/views/datasource/list/detail.tsx | 1520 ++++++++---- .../src/views/datasource/list/types.ts | 2 +- .../src/views/datasource/list/use-detail.ts | 106 +- .../src/views/datasource/list/use-form.ts | 170 +- .../dependencies/dependencies-modal.tsx | 2 +- .../task/components/node/fields/index.ts | 1 - .../components/node/fields/use-datasource.ts | 4 +- .../node/fields/use-external-system.ts | 20 +- .../node/tasks/use-external-system.ts | 1 - .../workflow/components/dag/dag.module.scss | 4 +- .../workflow/definition/tree/index.tsx | 7 +- .../src/views/thirdparty-api-source/index.tsx | 250 -- .../src/views/thirdparty-api-source/modal.tsx | 2028 +++++++++-------- .../views/thirdparty-api-source/use-table.ts | 2 +- 23 files changed, 2575 insertions(+), 1842 deletions(-) delete mode 100644 dolphinscheduler-ui/src/service/modules/thirdparty-api-source/index.ts delete mode 100644 dolphinscheduler-ui/src/service/modules/thirdparty-api-source/types.ts delete mode 100644 dolphinscheduler-ui/src/views/thirdparty-api-source/index.tsx diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemParameters.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemParameters.java index 39442f6ed39c..5aaccfe8a66e 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemParameters.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemParameters.java @@ -74,6 +74,7 @@ public ResourceParametersHelper getResources() { return resources; } + @Override public boolean checkParameters() { // Add validation logic here return true; diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTask.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTask.java index e2852019b3f4..a45485d3c0db 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTask.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTask.java @@ -90,15 +90,16 @@ public void init() { externalSystemParameters = JSONUtils.parseObject( taskExecutionContext.getTaskParams(), ExternalSystemParameters.class); - log.info("Initialize external system task with externalSystemId: {}, externalTaskId: {}, externalTaskName: {}", - externalSystemParameters.getDatasource(), - externalSystemParameters.getExternalTaskId(), - externalSystemParameters.getExternalTaskName()); if (externalSystemParameters == null || !externalSystemParameters.checkParameters()) { throw new RuntimeException("external system task params is not valid"); } + log.info("Initialize external system task with externalSystemId: {}, externalTaskId: {}, externalTaskName: {}", + externalSystemParameters.getDatasource(), + externalSystemParameters.getExternalTaskId(), + externalSystemParameters.getExternalTaskName()); + // Initialize parameter mapping initParameterMap(); initStatusCache(); @@ -131,8 +132,7 @@ public void cancel() throws TaskException { if (isTimeoutFailureEnabled()) { long currentTime = System.currentTimeMillis(); long usedTimeMillis = currentTime - taskStartTime; - long usedTime = (usedTimeMillis + 29999) / 60000; // Over 30 seconds takes 1 minute, less than 30 - // seconds takes 0 minutes + long usedTime = (usedTimeMillis + 59999) / 60000; // Round up to minutes (ceiling division) log.info( "External task timeout check, used time: {}m, timeout: {}m, currentTime: {}, taskStartTime: {}", usedTime, taskExecutionContext.getTaskTimeout() / 60, currentTime, taskStartTime); @@ -187,8 +187,7 @@ private void trackExternalTaskStatus() throws TaskException { if (isTimeoutFailureEnabled()) { long currentTime = System.currentTimeMillis(); long usedTimeMillis = currentTime - taskStartTime; - long usedTime = (usedTimeMillis + 29999) / 60000; // Over 30 seconds takes 1 minute, less than 30 - // seconds takes 0 minutes + long usedTime = (usedTimeMillis + 59999) / 60000; // Round up to minutes (ceiling division) if (usedTime >= taskExecutionContext.getTaskTimeout()) { isTimeout = true; log.error("External task timeout, used time: {}m, timeout: {}m", @@ -502,8 +501,8 @@ private boolean isTimeoutFailureEnabled() { } private OkHttpRequestHeaderContentType getContentType(Map headers) { - if (headers == null || !headers.containsKey(ExternalTaskConstants.CONTENT_TYPE) - || !headers.containsKey(ExternalTaskConstants.CONTENT_TYPE_LOWERCASE)) { + if (headers == null || (!headers.containsKey(ExternalTaskConstants.CONTENT_TYPE) + && !headers.containsKey(ExternalTaskConstants.CONTENT_TYPE_LOWERCASE))) { return OkHttpRequestHeaderContentType.APPLICATION_JSON; } String contentType = headers.get(ExternalTaskConstants.CONTENT_TYPE); diff --git a/dolphinscheduler-ui/src/locales/en_US/thirdparty-api-source.ts b/dolphinscheduler-ui/src/locales/en_US/thirdparty-api-source.ts index d4de7d55af10..03185fbc3494 100644 --- a/dolphinscheduler-ui/src/locales/en_US/thirdparty-api-source.ts +++ b/dolphinscheduler-ui/src/locales/en_US/thirdparty-api-source.ts @@ -82,11 +82,13 @@ export default { param_value_tips: 'Please enter parameter value', extract_response_data: 'Please enter response data jsonPath', extract_field: 'Please enter extract field', - json_path: 'Please enter json path', - header_prefix:'Authorization Token Prefix', + json_path_list: 'Please enter, e.g: $.data[*].id', + json_path: 'Please enter, e.g: $.data.taskInstanceId', + header_prefix: 'Authorization Token Prefix', header_prefix_tips: 'Authorization header before token,e.g.Bearer', system_field_tips: 'Please select system field', request_body: 'Request Body', + request_body_placeholder: 'Please enter JSON format request body', header: 'Header', query: 'Query', get: 'GET', @@ -96,7 +98,8 @@ export default { interface_timeout_tips: 'Please enter interface timeout', millisecond: 'millisecond', - interface_timeout_description: 'Set the interface request timeout, default is 120000 milliseconds (2 minutes)', + interface_timeout_description: + 'Set the interface request timeout, default is 120000 milliseconds (2 minutes)', // Polling Configuration polling_config: 'Polling Configuration', @@ -104,12 +107,14 @@ export default { success_field: 'Success Field', success_field_tips: 'Please enter success field JSONPath,e.g.$.data.status', success_value: 'Success Value', - success_value_tips: 'Please enter all enum values for success,e.g.SUCCESS,FINISHED', + success_value_tips: + 'Please enter all enum values for success,e.g.SUCCESS,FINISHED', failure_condition: 'Failure Condition', failure_field: 'Failure Field', failure_field_tips: 'Please enter failure field JSONPath,e.g.$.data.status', failure_value: 'Failure Value', - failure_value_tips: 'Please enter all enum values for failure,e.g.CANCELED, FAILED', + failure_value_tips: + 'Please enter all enum values for failure,e.g.CANCELED, FAILED', // Buttons and Operations cancel: 'Cancel', @@ -147,12 +152,15 @@ export default { submit_interface_url_required: 'Submit interface address is required', query_interface_url_required: 'Query interface address is required', stop_interface_url_required: 'Stop interface address is required', - success_condition_required: 'Success condition field and value cannot be empty', - failure_condition_required: 'Failure condition field and value cannot be empty', + success_condition_required: + 'Success condition field and value cannot be empty', + failure_condition_required: + 'Failure condition field and value cannot be empty', external_system_required: 'External system is required', external_system_task_required: 'External system task is required', id_jsonpath_required: 'ID field and JSONPath is required', name_jsonpath_required: 'Name field and JSONPath is required', - taskinstanceid_jsonpath_required: 'TaskInstanceId field and JSONPath is required' -} \ No newline at end of file + taskinstanceid_jsonpath_required: + 'TaskInstanceId field and JSONPath is required' +} diff --git a/dolphinscheduler-ui/src/locales/zh_CN/thirdparty-api-source.ts b/dolphinscheduler-ui/src/locales/zh_CN/thirdparty-api-source.ts index 15c7e96cb576..04e58317c1fa 100644 --- a/dolphinscheduler-ui/src/locales/zh_CN/thirdparty-api-source.ts +++ b/dolphinscheduler-ui/src/locales/zh_CN/thirdparty-api-source.ts @@ -78,12 +78,13 @@ export default { param_value_tips: '请输入参数值', extract_response_data: '提取响应并存储变量', extract_field: '请输入提取参数', - json_path_list: '请输入,例如:$.data[*].id', - json_path: '请输入,例如$.data.taskInstanceId', - header_prefix:'Authorization Token前缀', + json_path_list: '请输入,例如:$.data[*].id', + json_path: '请输入,例如:$.data.taskInstanceId', + header_prefix: 'Authorization Token前缀', header_prefix_tips: 'Authorization Token前缀,e.g.Bearer', system_field_tips: '请选择系统字段', request_body: '请求Body', + request_body_placeholder: '请输入JSON格式的请求体', header: '请求头', query: '查询参数', get: 'GET', @@ -92,7 +93,8 @@ export default { interface_timeout: '接口超时时间', interface_timeout_tips: '请输入接口超时时间', millisecond: '毫秒', - interface_timeout_description: '设置接口请求超时时间,默认为120000毫秒(2分钟)', + interface_timeout_description: + '设置接口请求超时时间,默认为120000毫秒(2分钟)', polling_config: '轮询配置', success_condition: '成功条件', @@ -145,6 +147,5 @@ export default { external_system_required: '第三方系统不能为空', external_system_task_required: '第三方系统任务不能为空', id_jsonpath_required: '字段id以及JSONPath为必填项', - name_jsonpath_required: '字段name以及JSONPath为必填项', - -} \ No newline at end of file + name_jsonpath_required: '字段name以及JSONPath为必填项' +} diff --git a/dolphinscheduler-ui/src/service/modules/data-source/index.ts b/dolphinscheduler-ui/src/service/modules/data-source/index.ts index 2518c4477d11..426e74bd161c 100644 --- a/dolphinscheduler-ui/src/service/modules/data-source/index.ts +++ b/dolphinscheduler-ui/src/service/modules/data-source/index.ts @@ -168,17 +168,14 @@ export function getDatasourceTableColumnsById( }) } - export function queryExternalSystemList(): any { return axios({ url: '/external-systems/queryExternalSystemList', - method: 'get', + method: 'get' }) } -export function queryExternalSystemTasks( - externalSystemId: number -): any { +export function queryExternalSystemTasks(externalSystemId: number): any { return axios({ url: '/external-systems/queryExternalSystemTasks', method: 'get', @@ -187,4 +184,3 @@ export function queryExternalSystemTasks( } }) } - diff --git a/dolphinscheduler-ui/src/service/modules/data-source/types.ts b/dolphinscheduler-ui/src/service/modules/data-source/types.ts index cd6badccb4c6..9460dff3a6ca 100644 --- a/dolphinscheduler-ui/src/service/modules/data-source/types.ts +++ b/dolphinscheduler-ui/src/service/modules/data-source/types.ts @@ -44,6 +44,7 @@ type IDataBase = | 'K8S' | 'ALIYUN_SERVERLESS_SPARK' | 'DOLPHINDB' + | 'THIRDPARTY_SYSTEM_CONNECTOR' type IDataBaseLabel = | 'MYSQL' @@ -101,6 +102,94 @@ interface IDataSource { accessKeySecret?: string regionId?: string endpoint?: string + // THIRDPARTY_SYSTEM_CONNECTOR fields + serviceAddress?: string + interfaceTimeout?: number + authConfig?: { + authType: string + basicUsername?: string + basicPassword?: string + jwtToken?: string + oauth2TokenUrl?: string + oauth2ClientId?: string + oauth2ClientSecret?: string + oauth2GrantType?: string + oauth2Username?: string + oauth2Password?: string + headerPrefix?: string + authMappings?: { + key: string + value: string + }[] + } + selectInterface?: { + url: string + method: string + parameters?: { + paramName: string + paramValue: string + location: string + }[] + body: string + responseParameters?: { + key: string + jsonPath: string + disabled?: boolean + }[] + } + submitInterface?: { + url: string + method: string + parameters?: { + paramName: string + paramValue: string + location: string + }[] + body: string + responseParameters?: { + key: string + jsonPath: string + disabled?: boolean + }[] + } + pollStatusInterface?: { + url: string + method: string + parameters?: { + paramName: string + paramValue: string + location: string + }[] + body: string + pollingSuccessConfig: { + successField: string + successValue: string + } + pollingFailureConfig: { + failureField: string + failureValue: string + } + responseParameters?: { + key: string + jsonPath: string + disabled?: boolean + }[] + } + stopInterface?: { + url: string + method: string + parameters?: { + paramName: string + paramValue: string + location: string + }[] + body: string + responseParameters?: { + key: string + jsonPath: string + disabled?: boolean + }[] + } } interface ListReq { diff --git a/dolphinscheduler-ui/src/service/modules/thirdparty-api-source/index.ts b/dolphinscheduler-ui/src/service/modules/thirdparty-api-source/index.ts deleted file mode 100644 index 8cbb822ff9fc..000000000000 --- a/dolphinscheduler-ui/src/service/modules/thirdparty-api-source/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { axios } from '@/service/service' -import { - ThirdpartyApiSourceReq, - ThirdpartyApiSource, -} from './types' - -export function queryThirdpartyApiSourceListPaging(params: Partial): Promise { - return axios({ - url: '/external-systems', - method: 'get', - params - }) -} - - - - diff --git a/dolphinscheduler-ui/src/service/modules/thirdparty-api-source/types.ts b/dolphinscheduler-ui/src/service/modules/thirdparty-api-source/types.ts deleted file mode 100644 index 58818b439bbe..000000000000 --- a/dolphinscheduler-ui/src/service/modules/thirdparty-api-source/types.ts +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -interface ThirdpartyApiSourceReq { - pageNo: number - pageSize: number - searchVal?: string -} - -interface ResponseMapping { - token?: string - taskInstanceId?: string - taskInstanceJobId?: string - type: string - expected_value: string -} - -interface Header { - [key: string]: string -} - -interface Condition { - path: string - expected_value: string -} - -interface Authentication { - url: string - method: string - queryParams: Record - body?: Record - responseMapping: ResponseMapping - headers?: Record[] -} - -interface SubmitTask { - url: string - method: string - queryParams: Record - body?: Record - responseMapping: ResponseMapping -} - -interface Polling { - url: string - method: string - queryParams: Record - successConditions: Condition[] - failureConditions: Condition[] -} - -interface Kill { - url: string - method: string - queryParams: Record - body?: Record - responseMapping: ResponseMapping -} - -interface ThirdpartyApiSource { - id?: number - name: string - authentication: Authentication - submit_task: SubmitTask - polling: Polling - kill: Kill - createTime?: string - updateTime?: string -} - -interface ThirdpartyApiSourceRes { - totalList: ThirdpartyApiSource[] - total: number - totalPage: number - pageSize: number - currentPage: number - start: number -} - -export { - ThirdpartyApiSourceReq, - ThirdpartyApiSource, - ThirdpartyApiSourceRes, - Authentication, - SubmitTask, - Polling, - Kill, - ResponseMapping, - Header, - Condition -} \ No newline at end of file diff --git a/dolphinscheduler-ui/src/store/project/task-type.ts b/dolphinscheduler-ui/src/store/project/task-type.ts index 48d4c2678b18..635b90f016d1 100644 --- a/dolphinscheduler-ui/src/store/project/task-type.ts +++ b/dolphinscheduler-ui/src/store/project/task-type.ts @@ -126,8 +126,7 @@ export const TASK_TYPES_MAP = { helperLinkDisable: true, taskExecuteType: 'STREAM' }, - - NAL_SYSTEM : { + EXTERNAL_SYSTEM: { alias: 'EXTERNAL_SYSTEM', helperLinkDisable: true }, diff --git a/dolphinscheduler-ui/src/views/datasource/list/detail.tsx b/dolphinscheduler-ui/src/views/datasource/list/detail.tsx index 8961b42fa26d..63ae865e9a83 100644 --- a/dolphinscheduler-ui/src/views/datasource/list/detail.tsx +++ b/dolphinscheduler-ui/src/views/datasource/list/detail.tsx @@ -116,23 +116,25 @@ const DetailModal = defineComponent({ props.show && props.id && setFieldsValue(await queryById(props.id)) } ), - - // Monitor authType change, update headerPrefix - watch( - () => state.detailForm.authConfig?.authType, - (newAuthType) => { - if (state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && state.detailForm.authConfig) { - if (newAuthType === 'BASIC_AUTH') { - state.detailForm.authConfig.headerPrefix = 'Basic' - } else if (newAuthType === 'JWT' || newAuthType === 'OAUTH2') { - state.detailForm.authConfig.headerPrefix = 'Bearer' - } else { - state.detailForm.authConfig.headerPrefix = '' + // Monitor authType change, update headerPrefix + watch( + () => state.detailForm.authConfig?.authType, + (newAuthType) => { + if ( + state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && + state.detailForm.authConfig + ) { + if (newAuthType === 'BASIC_AUTH') { + state.detailForm.authConfig.headerPrefix = 'Basic' + } else if (newAuthType === 'JWT' || newAuthType === 'OAUTH2') { + state.detailForm.authConfig.headerPrefix = 'Bearer' + } else { + state.detailForm.authConfig.headerPrefix = '' + } } - } - }, - { immediate: true } - ) + }, + { immediate: true } + ) watch( () => props.selectType, @@ -819,7 +821,9 @@ const DetailModal = defineComponent({ > - + { + if (detailForm.authConfig) { + detailForm.authConfig.authType = value + } + }} options={[ - { label: t('thirdparty_api_source.basic_auth'), value: 'BASIC_AUTH' }, - { label: t('thirdparty_api_source.oauth2'), value: 'OAUTH2' }, - { label: t('thirdparty_api_source.jwt'), value: 'JWT' } + { + label: t('thirdparty_api_source.basic_auth'), + value: 'BASIC_AUTH' + }, + { + label: t('thirdparty_api_source.oauth2'), + value: 'OAUTH2' + }, + { + label: t('thirdparty_api_source.jwt'), + value: 'JWT' + } ]} /> { + if (detailForm.authConfig) { + detailForm.authConfig.headerPrefix = value + } + }} + placeholder={t( + 'thirdparty_api_source.header_prefix_tips' + )} /> - {detailForm.authConfig.authType === 'BASIC_AUTH' && ( + {detailForm.authConfig?.authType === 'BASIC_AUTH' && ( <> { + if (detailForm.authConfig) { + detailForm.authConfig.basicUsername = value + } + }} + placeholder={t( + 'thirdparty_api_source.username_tips' + )} /> { + if (detailForm.authConfig) { + detailForm.authConfig.basicPassword = value + } + }} type='password' showPasswordOn='click' - placeholder={t('thirdparty_api_source.password_tips')} + placeholder={t( + 'thirdparty_api_source.password_tips' + )} /> )} - {detailForm.authConfig.authType === 'OAUTH2' && ( + {detailForm.authConfig?.authType === 'OAUTH2' && ( <> { + if (detailForm.authConfig) { + detailForm.authConfig.oauth2TokenUrl = value + } + }} + placeholder={t( + 'thirdparty_api_source.oauth2_token_url_tips' + )} /> { + if (detailForm.authConfig) { + detailForm.authConfig.oauth2ClientId = value + } + }} + placeholder={t( + 'thirdparty_api_source.oauth2_client_id_tips' + )} /> { + if (detailForm.authConfig) { + detailForm.authConfig.oauth2ClientSecret = value + } + }} + placeholder={t( + 'thirdparty_api_source.oauth2_client_secret_tips' + )} /> { + if (detailForm.authConfig) { + detailForm.authConfig.oauth2GrantType = value + } + }} + placeholder={t( + 'thirdparty_api_source.oauth2_grant_type_tips' + )} /> { + if (detailForm.authConfig) { + detailForm.authConfig.oauth2Username = value + } + }} + placeholder={t( + 'thirdparty_api_source.oauth2_username_tips' + )} /> { + if (detailForm.authConfig) { + detailForm.authConfig.oauth2Password = value + } + }} type='password' showPasswordOn='click' - placeholder={t('thirdparty_api_source.oauth2_password_tips')} + placeholder={t( + 'thirdparty_api_source.oauth2_password_tips' + )} /> )} - {detailForm.authConfig.authType === 'JWT' && ( + {detailForm.authConfig?.authType === 'JWT' && ( { + if (detailForm.authConfig) { + detailForm.authConfig.jwtToken = value + } + }} + placeholder={t( + 'thirdparty_api_source.jwt_token_tips' + )} /> )} {/* additional params */} - +
{/* add button */} { + if (!detailForm.authConfig) { + detailForm.authConfig = { + authType: 'BASIC_AUTH', + basicUsername: '', + basicPassword: '', + jwtToken: '', + oauth2TokenUrl: '', + oauth2ClientId: '', + oauth2ClientSecret: '', + oauth2GrantType: '', + oauth2Username: '', + oauth2Password: '', + headerPrefix: 'Basic', + authMappings: [] + } + } if (!detailForm.authConfig.authMappings) { detailForm.authConfig.authMappings = [] } - detailForm.authConfig.authMappings.push({ key: '', value: '' }) + detailForm.authConfig.authMappings.push({ + key: '', + value: '' + }) }} style={{ marginBottom: '10px' }} > {t('thirdparty_api_source.add_param')} - + {/* param list */} - {detailForm.authConfig.authMappings && detailForm.authConfig.authMappings.map((param: { key: string; value: string }, index: number) => ( -
- - - { - detailForm.authConfig.authMappings.splice(index, 1) - }} - style={{ width: '20%', marginLeft: '10px' }} - size="small" - > - {t('thirdparty_api_source.delete')} - -
- ))} + {detailForm.authConfig?.authMappings && + detailForm.authConfig.authMappings.map( + ( + param: { key: string; value: string }, + index: number + ) => ( +
+ + (param.key = value) + } + placeholder={t('thirdparty_api_source.key')} + style={{ width: '40%' }} + /> + + (param.value = value) + } + placeholder={t('thirdparty_api_source.value')} + style={{ width: '40%', marginLeft: '10px' }} + /> + { + detailForm.authConfig?.authMappings?.splice( + index, + 1 + ) + }} + style={{ width: '20%', marginLeft: '10px' }} + size='small' + > + {t('thirdparty_api_source.delete')} + +
+ ) + )}
-
+
{ + if (detailForm.selectInterface) { + detailForm.selectInterface.url = value + } + }} + placeholder={t( + 'thirdparty_api_source.input_interface_tips' + )} style={{ flex: 1 }} /> { + if (detailForm.selectInterface) { + detailForm.selectInterface.method = value + } + }} options={[ - { label: t('thirdparty_api_source.get'), value: 'GET' }, - { label: t('thirdparty_api_source.post'), value: 'POST' }, - { label: t('thirdparty_api_source.put'), value: 'PUT' } + { + label: t('thirdparty_api_source.get'), + value: 'GET' + }, + { + label: t('thirdparty_api_source.post'), + value: 'POST' + }, + { + label: t('thirdparty_api_source.put'), + value: 'PUT' + } ]} style={{ width: '120px', marginLeft: '10px' }} /> @@ -1022,127 +1185,253 @@ const DetailModal = defineComponent({ {/* add Button*/} { + if (!detailForm.selectInterface) { + detailForm.selectInterface = { + url: '', + method: 'GET', + parameters: [], + body: '', + responseParameters: [] + } + } if (!detailForm.selectInterface.parameters) { detailForm.selectInterface.parameters = [] } - detailForm.selectInterface.parameters.push({ paramName: '', paramValue: '', location: 'HEADER' }) + detailForm.selectInterface.parameters.push({ + paramName: '', + paramValue: '', + location: 'HEADER' + }) }} style={{ marginBottom: '10px' }} > {t('thirdparty_api_source.add_param')} - + {/* parameter list */} - {detailForm.selectInterface.parameters && detailForm.selectInterface.parameters.map((param: { paramName: string; paramValue: string; location: string }, index: number) => ( -
- - - - { - detailForm.selectInterface.parameters.splice(index, 1) - }} - style={{ marginLeft: '10px' }} - > - {t('thirdparty_api_source.delete')} - -
- ))} + {detailForm.selectInterface?.parameters && + detailForm.selectInterface.parameters.map( + ( + param: { + paramName: string + paramValue: string + location: string + }, + index: number + ) => ( +
+ + (param.location = value) + } + options={[ + { label: 'Header', value: 'HEADER' }, + { label: 'Param', value: 'PARAM' } + ]} + placeholder={t( + 'thirdparty_api_source.param_location_tips' + )} + style={{ width: '120px' }} + /> + + (param.paramName = value) + } + placeholder={t( + 'thirdparty_api_source.param_name_tips' + )} + style={{ flex: 1, marginLeft: '10px' }} + /> + + (param.paramValue = value) + } + placeholder={t( + 'thirdparty_api_source.param_value_tips' + )} + style={{ flex: 1, marginLeft: '10px' }} + /> + { + detailForm.selectInterface?.parameters?.splice( + index, + 1 + ) + }} + style={{ marginLeft: '10px' }} + > + {t('thirdparty_api_source.delete')} + +
+ ) + )}
- {(detailForm.selectInterface.method === 'POST' || detailForm.selectInterface.method === 'PUT') && ( - + {(detailForm.selectInterface?.method === 'POST' || + detailForm.selectInterface?.method === 'PUT') && ( + { + if (detailForm.selectInterface) { + detailForm.selectInterface.body = value + } + }} + type='textarea' autosize={{ minRows: 4, maxRows: 10 }} - placeholder="请输入JSON格式的请求体" + placeholder='请输入JSON格式的请求体' /> )} - +
{/* add Button */} { - if (!detailForm.selectInterface.responseParameters) { + if (!detailForm.selectInterface) { + detailForm.selectInterface = { + url: '', + method: 'GET', + parameters: [], + body: '', + responseParameters: [] + } + } + if ( + !detailForm.selectInterface.responseParameters + ) { detailForm.selectInterface.responseParameters = [] } - detailForm.selectInterface.responseParameters.push({ key: '', jsonPath: '', disabled: false }) + detailForm.selectInterface.responseParameters.push({ + key: '', + jsonPath: '', + disabled: false + }) }} style={{ marginBottom: '10px' }} > {t('thirdparty_api_source.add_extract_field')} - + {/* responseParameters */} - {detailForm.selectInterface.responseParameters && detailForm.selectInterface.responseParameters.map((param: { key: string; jsonPath: string; disabled: boolean }, index: number) => ( -
- - - { - detailForm.selectInterface.responseParameters.splice(index, 1) - }} - style={{ marginLeft: '10px' }} - > - {t('thirdparty_api_source.delete')} - -
- ))} + {detailForm.selectInterface?.responseParameters && + detailForm.selectInterface.responseParameters.map( + ( + param: { + key: string + jsonPath: string + disabled?: boolean + }, + index: number + ) => ( +
+ + (param.key = value) + } + placeholder={t( + 'thirdparty_api_source.extract_field' + )} + style={{ flex: 1 }} + disabled={param.disabled} + /> + + (param.jsonPath = value) + } + placeholder={t( + 'thirdparty_api_source.json_path_list' + )} + style={{ flex: 1, marginLeft: '10px' }} + /> + { + detailForm.selectInterface?.responseParameters?.splice( + index, + 1 + ) + }} + style={{ marginLeft: '10px' }} + > + {t('thirdparty_api_source.delete')} + +
+ ) + )}
-
+
{ + if (detailForm.submitInterface) { + detailForm.submitInterface.url = value + } + }} + placeholder={t( + 'thirdparty_api_source.submit_interface_tips' + )} style={{ flex: 1 }} /> { + if (detailForm.submitInterface) { + detailForm.submitInterface.method = value + } + }} options={[ - { label: t('thirdparty_api_source.get'), value: 'GET' }, - { label: t('thirdparty_api_source.post'), value: 'POST' }, - { label: t('thirdparty_api_source.put'), value: 'PUT' } + { + label: t('thirdparty_api_source.get'), + value: 'GET' + }, + { + label: t('thirdparty_api_source.post'), + value: 'POST' + }, + { + label: t('thirdparty_api_source.put'), + value: 'PUT' + } ]} style={{ width: '120px', marginLeft: '10px' }} /> @@ -1153,127 +1442,250 @@ const DetailModal = defineComponent({ {/* add button */} { + if (!detailForm.submitInterface) { + detailForm.submitInterface = { + url: '', + method: 'POST', + parameters: [], + body: '', + responseParameters: [] + } + } if (!detailForm.submitInterface.parameters) { detailForm.submitInterface.parameters = [] } - detailForm.submitInterface.parameters.push({ paramName: '', paramValue: '', location: 'HEADER' }) + detailForm.submitInterface.parameters.push({ + paramName: '', + paramValue: '', + location: 'HEADER' + }) }} style={{ marginBottom: '10px' }} > {t('thirdparty_api_source.add_param')} - + {/* parameter list */} - {detailForm.submitInterface.parameters && detailForm.submitInterface.parameters.map((param: { paramName: string; paramValue: string; location: string }, index: number) => ( -
- - - - { - detailForm.submitInterface.parameters.splice(index, 1) - }} - style={{ marginLeft: '10px' }} - > - {t('thirdparty_api_source.delete')} - -
- ))} + {detailForm.submitInterface?.parameters && + detailForm.submitInterface.parameters.map( + ( + param: { + paramName: string + paramValue: string + location: string + }, + index: number + ) => ( +
+ + (param.location = value) + } + options={[ + { label: 'Header', value: 'HEADER' }, + { label: 'Param', value: 'PARAM' } + ]} + placeholder={t( + 'thirdparty_api_source.param_location_tips' + )} + style={{ width: '120px' }} + /> + + (param.paramName = value) + } + placeholder={t( + 'thirdparty_api_source.param_name_tips' + )} + style={{ flex: 1, marginLeft: '10px' }} + /> + + (param.paramValue = value) + } + placeholder={t( + 'thirdparty_api_source.param_value_tips' + )} + style={{ flex: 1, marginLeft: '10px' }} + /> + { + detailForm.submitInterface?.parameters?.splice( + index, + 1 + ) + }} + style={{ marginLeft: '10px' }} + > + {t('thirdparty_api_source.delete')} + +
+ ) + )}
- {(detailForm.submitInterface.method === 'POST' || detailForm.submitInterface.method === 'PUT') && ( - + {(detailForm.submitInterface?.method === 'POST' || + detailForm.submitInterface?.method === 'PUT') && ( + { + if (detailForm.submitInterface) { + detailForm.submitInterface.body = value + } + }} + type='textarea' autosize={{ minRows: 4, maxRows: 10 }} - placeholder="请输入JSON格式的请求体" + placeholder='请输入JSON格式的请求体' /> )} - +
{/* add button */} { - if (!detailForm.submitInterface.responseParameters) { + if (!detailForm.submitInterface) { + detailForm.submitInterface = { + url: '', + method: 'POST', + parameters: [], + body: '', + responseParameters: [] + } + } + if ( + !detailForm.submitInterface.responseParameters + ) { detailForm.submitInterface.responseParameters = [] } - detailForm.submitInterface.responseParameters.push({ key: '', jsonPath: '', disabled: false }) + detailForm.submitInterface.responseParameters.push({ + key: '', + jsonPath: '', + disabled: false + }) }} style={{ marginBottom: '10px' }} > {t('thirdparty_api_source.add_extract_field')} - + {/* param list */} - {detailForm.submitInterface.responseParameters && detailForm.submitInterface.responseParameters.map((param: { key: string; jsonPath: string; disabled: boolean }, index: number) => ( -
- - - { - detailForm.submitInterface.responseParameters.splice(index, 1) - }} - > - {t('thirdparty_api_source.delete')} - -
- ))} + {detailForm.submitInterface?.responseParameters && + detailForm.submitInterface.responseParameters.map( + ( + param: { + key: string + jsonPath: string + disabled?: boolean + }, + index: number + ) => ( +
+ + (param.key = value) + } + placeholder={t( + 'thirdparty_api_source.extract_field' + )} + style={{ width: '180px' }} + disabled={param.disabled} + /> + + (param.jsonPath = value) + } + placeholder={t( + 'thirdparty_api_source.json_path' + )} + style={{ width: '180px' }} + disabled={param.disabled} + /> + { + detailForm.submitInterface?.responseParameters?.splice( + index, + 1 + ) + }} + > + {t('thirdparty_api_source.delete')} + +
+ ) + )}
-
+
{ + if (detailForm.pollStatusInterface) { + detailForm.pollStatusInterface.url = value + } + }} + placeholder={t( + 'thirdparty_api_source.query_interface_tips' + )} style={{ flex: 1 }} /> { + if (detailForm.pollStatusInterface) { + detailForm.pollStatusInterface.method = value + } + }} options={[ - { label: t('thirdparty_api_source.get'), value: 'GET' }, - { label: t('thirdparty_api_source.post'), value: 'POST' }, - { label: t('thirdparty_api_source.put'), value: 'PUT' } + { + label: t('thirdparty_api_source.get'), + value: 'GET' + }, + { + label: t('thirdparty_api_source.post'), + value: 'POST' + }, + { + label: t('thirdparty_api_source.put'), + value: 'PUT' + } ]} style={{ width: '120px', marginLeft: '10px' }} /> @@ -1284,134 +1696,320 @@ const DetailModal = defineComponent({ {/* add button */} { + if (!detailForm.pollStatusInterface) { + detailForm.pollStatusInterface = { + url: '', + method: 'GET', + parameters: [], + body: '', + pollingSuccessConfig: { + successField: '', + successValue: '' + }, + pollingFailureConfig: { + failureField: '', + failureValue: '' + }, + responseParameters: [] + } + } if (!detailForm.pollStatusInterface.parameters) { detailForm.pollStatusInterface.parameters = [] } - detailForm.pollStatusInterface.parameters.push({ paramName: '', paramValue: '', location: 'HEADER' }) + detailForm.pollStatusInterface.parameters.push({ + paramName: '', + paramValue: '', + location: 'HEADER' + }) }} style={{ marginBottom: '10px' }} > {t('thirdparty_api_source.add_param')} - + {/* param list */} - {detailForm.pollStatusInterface.parameters && detailForm.pollStatusInterface.parameters.map((param: { paramName: string; paramValue: string; location: string }, index: number) => ( -
- - - - { - detailForm.pollStatusInterface.parameters.splice(index, 1) - }} - style={{ marginLeft: '10px' }} - > - {t('thirdparty_api_source.delete')} - -
- ))} + {detailForm.pollStatusInterface?.parameters && + detailForm.pollStatusInterface.parameters.map( + ( + param: { + paramName: string + paramValue: string + location: string + }, + index: number + ) => ( +
+ + (param.location = value) + } + options={[ + { label: 'Header', value: 'HEADER' }, + { label: 'Param', value: 'PARAM' } + ]} + placeholder={t( + 'thirdparty_api_source.param_location_tips' + )} + style={{ width: '120px' }} + /> + + (param.paramName = value) + } + placeholder={t( + 'thirdparty_api_source.param_name_tips' + )} + style={{ flex: 1, marginLeft: '10px' }} + /> + + (param.paramValue = value) + } + placeholder={t( + 'thirdparty_api_source.param_value_tips' + )} + style={{ flex: 1, marginLeft: '10px' }} + /> + { + detailForm.pollStatusInterface?.parameters?.splice( + index, + 1 + ) + }} + style={{ marginLeft: '10px' }} + > + {t('thirdparty_api_source.delete')} + +
+ ) + )}
- {(detailForm.pollStatusInterface.method === 'POST' || detailForm.pollStatusInterface.method === 'PUT') && ( - + {(detailForm.pollStatusInterface?.method === 'POST' || + detailForm.pollStatusInterface?.method === 'PUT') && ( + { + if (detailForm.pollStatusInterface) { + detailForm.pollStatusInterface.body = value + } + }} + type='textarea' autosize={{ minRows: 4, maxRows: 10 }} - placeholder="请输入JSON格式的请求体" + placeholder='请输入JSON格式的请求体' /> )} - +
{/* add button */} { - if (!detailForm.pollStatusInterface.responseParameters) { - detailForm.pollStatusInterface.responseParameters = [] + if (!detailForm.pollStatusInterface) { + detailForm.pollStatusInterface = { + url: '', + method: 'GET', + parameters: [], + body: '', + pollingSuccessConfig: { + successField: '', + successValue: '' + }, + pollingFailureConfig: { + failureField: '', + failureValue: '' + }, + responseParameters: [] + } + } + if ( + !detailForm.pollStatusInterface.responseParameters + ) { + detailForm.pollStatusInterface.responseParameters = + [] } - detailForm.pollStatusInterface.responseParameters.push({ key: '', jsonPath: '', disabled: false }) + detailForm.pollStatusInterface.responseParameters.push( + { key: '', jsonPath: '', disabled: false } + ) }} style={{ marginBottom: '10px' }} > {t('thirdparty_api_source.add_extract_field')} - + {/* param list */} - {detailForm.pollStatusInterface.responseParameters && detailForm.pollStatusInterface.responseParameters.map((param: { key: string; jsonPath: string; disabled: boolean }, index: number) => ( -
- - - { - detailForm.pollStatusInterface.responseParameters.splice(index, 1) - }} - style={{ marginLeft: '10px' }} - > - {t('thirdparty_api_source.delete')} - -
- ))} + {detailForm.pollStatusInterface?.responseParameters && + detailForm.pollStatusInterface.responseParameters.map( + ( + param: { + key: string + jsonPath: string + disabled?: boolean + }, + index: number + ) => ( +
+ + (param.key = value) + } + placeholder={t( + 'thirdparty_api_source.extract_field' + )} + style={{ flex: 1 }} + disabled={param.disabled} + /> + + (param.jsonPath = value) + } + placeholder={t( + 'thirdparty_api_source.json_path' + )} + style={{ flex: 1, marginLeft: '10px' }} + /> + { + detailForm.pollStatusInterface?.responseParameters?.splice( + index, + 1 + ) + }} + style={{ marginLeft: '10px' }} + > + {t('thirdparty_api_source.delete')} + +
+ ) + )}
- -
+ +
{ + if ( + detailForm.pollStatusInterface + ?.pollingSuccessConfig + ) { + detailForm.pollStatusInterface.pollingSuccessConfig.successField = + value + } + }} + placeholder={t( + 'thirdparty_api_source.success_field_tips' + )} style={{ flex: 1 }} /> { + if ( + detailForm.pollStatusInterface + ?.pollingSuccessConfig + ) { + detailForm.pollStatusInterface.pollingSuccessConfig.successValue = + value + } + }} + placeholder={t( + 'thirdparty_api_source.success_value_tips' + )} style={{ flex: 1, marginLeft: '10px' }} />
- -
+ +
{ + if ( + detailForm.pollStatusInterface + ?.pollingFailureConfig + ) { + detailForm.pollStatusInterface.pollingFailureConfig.failureField = + value + } + }} + placeholder={t( + 'thirdparty_api_source.failure_field_tips' + )} style={{ flex: 1 }} /> { + if ( + detailForm.pollStatusInterface + ?.pollingFailureConfig + ) { + detailForm.pollStatusInterface.pollingFailureConfig.failureValue = + value + } + }} + placeholder={t( + 'thirdparty_api_source.failure_value_tips' + )} style={{ flex: 1, marginLeft: '10px' }} />
@@ -1420,19 +2018,46 @@ const DetailModal = defineComponent({ label={t('thirdparty_api_source.stop_interface')} path='stopInterface.url' > -
+
{ + if (detailForm.stopInterface) { + detailForm.stopInterface.url = value + } + }} + placeholder={t( + 'thirdparty_api_source.stop_interface_tips' + )} style={{ flex: 1 }} /> { + if (detailForm.stopInterface) { + detailForm.stopInterface.method = value + } + }} options={[ - { label: t('thirdparty_api_source.get'), value: 'GET' }, - { label: t('thirdparty_api_source.post'), value: 'POST' }, - { label: t('thirdparty_api_source.put'), value: 'PUT' } + { + label: t('thirdparty_api_source.get'), + value: 'GET' + }, + { + label: t('thirdparty_api_source.post'), + value: 'POST' + }, + { + label: t('thirdparty_api_source.put'), + value: 'PUT' + } ]} style={{ width: '120px', marginLeft: '10px' }} /> @@ -1443,108 +2068,205 @@ const DetailModal = defineComponent({ {/* add button */} { + if (!detailForm.stopInterface) { + detailForm.stopInterface = { + url: '', + method: 'POST', + parameters: [], + body: '', + responseParameters: [] + } + } if (!detailForm.stopInterface.parameters) { detailForm.stopInterface.parameters = [] } - detailForm.stopInterface.parameters.push({ paramName: '', paramValue: '', location: 'HEADER' }) + detailForm.stopInterface.parameters.push({ + paramName: '', + paramValue: '', + location: 'HEADER' + }) }} style={{ marginBottom: '10px' }} > {t('thirdparty_api_source.add_param')} - + {/* param list */} - {detailForm.stopInterface.parameters && detailForm.stopInterface.parameters.map((param: { paramName: string; paramValue: string; location: string }, index: number) => ( -
- - - - { - detailForm.stopInterface.parameters.splice(index, 1) - }} - style={{ marginLeft: '10px' }} - > - {t('thirdparty_api_source.delete')} - -
- ))} + {detailForm.stopInterface?.parameters && + detailForm.stopInterface.parameters.map( + ( + param: { + paramName: string + paramValue: string + location: string + }, + index: number + ) => ( +
+ + (param.location = value) + } + options={[ + { label: 'Header', value: 'HEADER' }, + { label: 'Param', value: 'PARAM' } + ]} + placeholder={t( + 'thirdparty_api_source.param_location_tips' + )} + style={{ width: '120px' }} + /> + + (param.paramName = value) + } + placeholder={t( + 'thirdparty_api_source.param_name_tips' + )} + style={{ flex: 1, marginLeft: '10px' }} + /> + + (param.paramValue = value) + } + placeholder={t( + 'thirdparty_api_source.param_value_tips' + )} + style={{ flex: 1, marginLeft: '10px' }} + /> + { + detailForm.stopInterface?.parameters?.splice( + index, + 1 + ) + }} + style={{ marginLeft: '10px' }} + > + {t('thirdparty_api_source.delete')} + +
+ ) + )}
- {(detailForm.stopInterface.method === 'POST' || detailForm.stopInterface.method === 'PUT') && ( - + {(detailForm.stopInterface?.method === 'POST' || + detailForm.stopInterface?.method === 'PUT') && ( + { + if (detailForm.stopInterface) { + detailForm.stopInterface.body = value + } + }} + type='textarea' autosize={{ minRows: 4, maxRows: 10 }} - placeholder="请输入JSON格式的请求体" + placeholder='请输入JSON格式的请求体' /> )} - +
{/* add button */} { + if (!detailForm.stopInterface) { + detailForm.stopInterface = { + url: '', + method: 'POST', + parameters: [], + body: '', + responseParameters: [] + } + } if (!detailForm.stopInterface.responseParameters) { detailForm.stopInterface.responseParameters = [] } - detailForm.stopInterface.responseParameters.push({ key: '', jsonPath: '', disabled: false }) + detailForm.stopInterface.responseParameters.push({ + key: '', + jsonPath: '', + disabled: false + }) }} style={{ marginBottom: '10px' }} > {t('thirdparty_api_source.add_extract_field')} - + {/* param list */} - {detailForm.stopInterface.responseParameters && detailForm.stopInterface.responseParameters.map((param: { key: string; jsonPath: string; disabled: boolean }, index: number) => ( -
- - - { - detailForm.stopInterface.responseParameters.splice(index, 1) - }} - style={{ marginLeft: '10px' }} - > - {t('thirdparty_api_source.delete')} - -
- ))} + {detailForm.stopInterface?.responseParameters && + detailForm.stopInterface.responseParameters.map( + ( + param: { + key: string + jsonPath: string + disabled?: boolean + }, + index: number + ) => ( +
+ + (param.key = value) + } + placeholder={t( + 'thirdparty_api_source.extract_field' + )} + style={{ flex: 1 }} + disabled={param.disabled} + /> + + (param.jsonPath = value) + } + placeholder={t( + 'thirdparty_api_source.json_path' + )} + style={{ flex: 1, marginLeft: '10px' }} + /> + { + detailForm.stopInterface?.responseParameters?.splice( + index, + 1 + ) + }} + style={{ marginLeft: '10px' }} + > + {t('thirdparty_api_source.delete')} + +
+ ) + )}
diff --git a/dolphinscheduler-ui/src/views/datasource/list/types.ts b/dolphinscheduler-ui/src/views/datasource/list/types.ts index 85b9e5159dd6..8dbc7c9cfceb 100644 --- a/dolphinscheduler-ui/src/views/datasource/list/types.ts +++ b/dolphinscheduler-ui/src/views/datasource/list/types.ts @@ -108,4 +108,4 @@ export { IDataBaseOption, IDataBaseOptionKeys, TableColumns -} \ No newline at end of file +} diff --git a/dolphinscheduler-ui/src/views/datasource/list/use-detail.ts b/dolphinscheduler-ui/src/views/datasource/list/use-detail.ts index e17924722856..9e66478485b3 100644 --- a/dolphinscheduler-ui/src/views/datasource/list/use-detail.ts +++ b/dolphinscheduler-ui/src/views/datasource/list/use-detail.ts @@ -44,12 +44,14 @@ export function useDetail(getFieldsValue: Function) { } if (values.type === 'THIRDPARTY_SYSTEM_CONNECTOR') { - if (params.authConfig && params.authConfig.authMappings) { - params.authConfig.authMappings = params.authConfig.authMappings.filter( - (mapping: { key: string; value: string }) => mapping.key || mapping.value - ) + if (params.authConfig?.authMappings) { + params.authConfig.authMappings = + params.authConfig?.authMappings?.filter( + (mapping: { key: string; value: string }) => + mapping.key || mapping.value + ) } - + if (!params.selectInterface) { params.selectInterface = { url: '', @@ -63,42 +65,48 @@ export function useDetail(getFieldsValue: Function) { } } else { if (params.selectInterface.parameters) { - params.selectInterface.parameters = params.selectInterface.parameters.filter( - (param: { paramName: string; paramValue: string }) => param.paramName || param.paramValue - ) + params.selectInterface.parameters = + params.selectInterface.parameters.filter( + (param: { paramName: string; paramValue: string }) => + param.paramName || param.paramValue + ) } - + if (params.selectInterface.responseParameters) { - params.selectInterface.responseParameters = params.selectInterface.responseParameters.filter( - (param: { key: string; jsonPath: string }) => param.key || param.jsonPath - ) + params.selectInterface.responseParameters = + params.selectInterface.responseParameters.filter( + (param: { key: string; jsonPath: string }) => + param.key || param.jsonPath + ) } } - + if (!params.submitInterface) { params.submitInterface = { url: '', method: 'POST', parameters: [], body: '', - responseParameters: [ - { key: 'taskInstanceId', jsonPath: '' } - ] + responseParameters: [{ key: 'taskInstanceId', jsonPath: '' }] } } else { if (params.submitInterface.parameters) { - params.submitInterface.parameters = params.submitInterface.parameters.filter( - (param: { paramName: string; paramValue: string }) => param.paramName || param.paramValue - ) + params.submitInterface.parameters = + params.submitInterface.parameters.filter( + (param: { paramName: string; paramValue: string }) => + param.paramName || param.paramValue + ) } - + if (params.submitInterface.responseParameters) { - params.submitInterface.responseParameters = params.submitInterface.responseParameters.filter( - (param: { key: string; jsonPath: string }) => param.key || param.jsonPath - ) + params.submitInterface.responseParameters = + params.submitInterface.responseParameters.filter( + (param: { key: string; jsonPath: string }) => + param.key || param.jsonPath + ) } } - + if (!params.pollStatusInterface) { params.pollStatusInterface = { url: '', @@ -117,32 +125,36 @@ export function useDetail(getFieldsValue: Function) { } } else { if (params.pollStatusInterface.parameters) { - params.pollStatusInterface.parameters = params.pollStatusInterface.parameters.filter( - (param: { paramName: string; paramValue: string }) => param.paramName || param.paramValue - ) + params.pollStatusInterface.parameters = + params.pollStatusInterface.parameters.filter( + (param: { paramName: string; paramValue: string }) => + param.paramName || param.paramValue + ) } - + if (!params.pollStatusInterface.pollingSuccessConfig) { params.pollStatusInterface.pollingSuccessConfig = { successField: '', successValue: '' } } - + if (!params.pollStatusInterface.pollingFailureConfig) { params.pollStatusInterface.pollingFailureConfig = { failureField: '', failureValue: '' } } - + if (params.pollStatusInterface.responseParameters) { - params.pollStatusInterface.responseParameters = params.pollStatusInterface.responseParameters.filter( - (param: { key: string; jsonPath: string }) => param.key || param.jsonPath - ) + params.pollStatusInterface.responseParameters = + params.pollStatusInterface.responseParameters.filter( + (param: { key: string; jsonPath: string }) => + param.key || param.jsonPath + ) } } - + if (!params.stopInterface) { params.stopInterface = { url: '', @@ -153,18 +165,22 @@ export function useDetail(getFieldsValue: Function) { } } else { if (params.stopInterface.parameters) { - params.stopInterface.parameters = params.stopInterface.parameters.filter( - (param: { paramName: string; paramValue: string }) => param.paramName || param.paramValue - ) + params.stopInterface.parameters = + params.stopInterface.parameters.filter( + (param: { paramName: string; paramValue: string }) => + param.paramName || param.paramValue + ) } - + if (params.stopInterface.responseParameters) { - params.stopInterface.responseParameters = params.stopInterface.responseParameters.filter( - (param: { key: string; jsonPath: string }) => param.key || param.jsonPath - ) + params.stopInterface.responseParameters = + params.stopInterface.responseParameters.filter( + (param: { key: string; jsonPath: string }) => + param.key || param.jsonPath + ) } } - + if (!params.authConfig) { params.authConfig = { authType: 'BASIC_AUTH', @@ -181,9 +197,9 @@ export function useDetail(getFieldsValue: Function) { authMappings: [] } } - - delete params.userName; - delete params.password; + + delete params.userName + delete params.password } return params diff --git a/dolphinscheduler-ui/src/views/datasource/list/use-form.ts b/dolphinscheduler-ui/src/views/datasource/list/use-form.ts index a415b0f4d6b3..7e8a34d694cb 100644 --- a/dolphinscheduler-ui/src/views/datasource/list/use-form.ts +++ b/dolphinscheduler-ui/src/views/datasource/list/use-form.ts @@ -84,15 +84,14 @@ export function useForm(id?: number) { method: 'POST', parameters: [], body: '', - responseParameters: [ - { key: 'taskInstanceId', jsonPath: '' } - ] + responseParameters: [{ key: 'taskInstanceId', jsonPath: '' }] }, pollStatusInterface: { url: '', method: 'GET', parameters: [], body: '', + responseParameters: [], pollingSuccessConfig: { successField: '', successValue: '' @@ -106,7 +105,8 @@ export function useForm(id?: number) { url: '', method: 'POST', parameters: [], - body: '' + body: '', + responseParameters: [] } } as IDataSourceDetail @@ -131,6 +131,7 @@ export function useForm(id?: number) { showAccessKeySecret: false, showRegionId: false, showEndpoint: false, + showPrivateKey: false, rules: { name: { trigger: ['input'], @@ -271,15 +272,23 @@ export function useForm(id?: number) { serviceAddress: { trigger: ['input'], validator() { - if (state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && !state.detailForm.serviceAddress) { - return new Error(t('thirdparty_api_source.service_address_required')) + if ( + state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && + !state.detailForm.serviceAddress + ) { + return new Error( + t('thirdparty_api_source.service_address_required') + ) } } }, 'authConfig.authType': { trigger: ['change'], validator() { - if (state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && !state.detailForm.authConfig?.authType) { + if ( + state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && + !state.detailForm.authConfig?.authType + ) { return new Error(t('thirdparty_api_source.auth_type_required')) } } @@ -318,7 +327,9 @@ export function useForm(id?: number) { state.detailForm.authConfig?.authType === 'OAUTH2' && !state.detailForm.authConfig?.oauth2TokenUrl ) { - return new Error(t('thirdparty_api_source.oauth2_token_url_required')) + return new Error( + t('thirdparty_api_source.oauth2_token_url_required') + ) } return true } @@ -331,7 +342,9 @@ export function useForm(id?: number) { state.detailForm.authConfig?.authType === 'OAUTH2' && !state.detailForm.authConfig?.oauth2ClientId ) { - return new Error(t('thirdparty_api_source.oauth2_client_id_required')) + return new Error( + t('thirdparty_api_source.oauth2_client_id_required') + ) } return true } @@ -344,7 +357,9 @@ export function useForm(id?: number) { state.detailForm.authConfig?.authType === 'OAUTH2' && !state.detailForm.authConfig?.oauth2ClientSecret ) { - return new Error(t('thirdparty_api_source.oauth2_client_secret_required')) + return new Error( + t('thirdparty_api_source.oauth2_client_secret_required') + ) } return true } @@ -357,7 +372,9 @@ export function useForm(id?: number) { state.detailForm.authConfig?.authType === 'OAUTH2' && !state.detailForm.authConfig?.oauth2GrantType ) { - return new Error(t('thirdparty_api_source.oauth2_grant_type_required')) + return new Error( + t('thirdparty_api_source.oauth2_grant_type_required') + ) } return true } @@ -382,7 +399,9 @@ export function useForm(id?: number) { state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && !state.detailForm.selectInterface?.url ) { - return new Error(t('thirdparty_api_source.input_interface_url_required')) + return new Error( + t('thirdparty_api_source.input_interface_url_required') + ) } } }, @@ -393,7 +412,9 @@ export function useForm(id?: number) { state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && !state.detailForm.submitInterface?.url ) { - return new Error(t('thirdparty_api_source.submit_interface_url_required')) + return new Error( + t('thirdparty_api_source.submit_interface_url_required') + ) } } }, @@ -404,7 +425,9 @@ export function useForm(id?: number) { state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && !state.detailForm.pollStatusInterface?.url ) { - return new Error(t('thirdparty_api_source.query_interface_url_required')) + return new Error( + t('thirdparty_api_source.query_interface_url_required') + ) } } }, @@ -415,7 +438,9 @@ export function useForm(id?: number) { state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && !state.detailForm.stopInterface?.url ) { - return new Error(t('thirdparty_api_source.stop_interface_url_required')) + return new Error( + t('thirdparty_api_source.stop_interface_url_required') + ) } } } @@ -563,13 +588,16 @@ export function useForm(id?: number) { headerPrefix: 'Basic', authMappings: [] } - } else if (!state.detailForm.authConfig.authMappings) { + } else if (!state.detailForm.authConfig?.authMappings) { state.detailForm.authConfig.authMappings = [] } } // init THIRDPARTY_SYSTEM_CONNECTOR authConfig - if (type === 'THIRDPARTY_SYSTEM_CONNECTOR' && !state.detailForm.authConfig) { + if ( + type === 'THIRDPARTY_SYSTEM_CONNECTOR' && + !state.detailForm.authConfig + ) { state.detailForm.authConfig = { authType: 'BASIC_AUTH', basicUsername: '', @@ -611,10 +639,44 @@ export function useForm(id?: number) { } const setFieldsValue = (values: IDataSource) => { + // 处理第三方系统连接器相关的接口参数,确保数组类型正确 + const processedValues = { + ...values, + other: values.other ? JSON.stringify(values.other) : values.other, + selectInterface: values.selectInterface + ? { + ...values.selectInterface, + parameters: values.selectInterface.parameters || [], + responseParameters: values.selectInterface.responseParameters || [] + } + : undefined, + submitInterface: values.submitInterface + ? { + ...values.submitInterface, + parameters: values.submitInterface.parameters || [], + responseParameters: values.submitInterface.responseParameters || [] + } + : undefined, + pollStatusInterface: values.pollStatusInterface + ? { + ...values.pollStatusInterface, + parameters: values.pollStatusInterface.parameters || [], + responseParameters: + values.pollStatusInterface.responseParameters || [] + } + : undefined, + stopInterface: values.stopInterface + ? { + ...values.stopInterface, + parameters: values.stopInterface.parameters || [], + responseParameters: values.stopInterface.responseParameters || [] + } + : undefined + } + state.detailForm = { ...state.detailForm, - ...values, - other: values.other ? JSON.stringify(values.other) : values.other + ...processedValues } // check THIRDPARTY_SYSTEM_CONNECTOR has right authConfig @@ -647,7 +709,9 @@ export function useForm(id?: number) { oauth2Username: values.authConfig?.oauth2Username || '', oauth2Password: values.authConfig?.oauth2Password || '', headerPrefix: values.authConfig?.headerPrefix || 'Basic', - authMappings: values.authConfig?.authMappings ? [...values.authConfig.authMappings] : [] + authMappings: values.authConfig?.authMappings + ? [...values.authConfig.authMappings] + : [] } } @@ -666,12 +730,16 @@ export function useForm(id?: number) { state.detailForm.selectInterface = { url: values.selectInterface?.url || '', method: values.selectInterface?.method || 'GET', - parameters: values.selectInterface?.parameters ? [...values.selectInterface.parameters] : [], + parameters: values.selectInterface?.parameters + ? [...values.selectInterface.parameters] + : [], body: values.selectInterface?.body || '', - responseParameters: values.selectInterface?.responseParameters ? [...values.selectInterface.responseParameters] : [ - { key: 'id', jsonPath: '' }, - { key: 'name', jsonPath: '' } - ] + responseParameters: values.selectInterface?.responseParameters + ? [...values.selectInterface.responseParameters] + : [ + { key: 'id', jsonPath: '' }, + { key: 'name', jsonPath: '' } + ] } } @@ -681,19 +749,19 @@ export function useForm(id?: number) { method: 'POST', parameters: [], body: '', - responseParameters: [ - { key: 'taskInstanceId', jsonPath: '' } - ] + responseParameters: [{ key: 'taskInstanceId', jsonPath: '' }] } } else { state.detailForm.submitInterface = { url: values.submitInterface?.url || '', method: values.submitInterface?.method || 'POST', - parameters: values.submitInterface?.parameters ? [...values.submitInterface.parameters] : [], + parameters: values.submitInterface?.parameters + ? [...values.submitInterface.parameters] + : [], body: values.submitInterface?.body || '', - responseParameters: values.submitInterface?.responseParameters ? [...values.submitInterface.responseParameters] : [ - { key: 'taskInstanceId', jsonPath: '' } - ] + responseParameters: values.submitInterface?.responseParameters + ? [...values.submitInterface.responseParameters] + : [{ key: 'taskInstanceId', jsonPath: '' }] } } @@ -717,17 +785,29 @@ export function useForm(id?: number) { state.detailForm.pollStatusInterface = { url: values.pollStatusInterface?.url || '', method: values.pollStatusInterface?.method || 'GET', - parameters: values.pollStatusInterface?.parameters ? [...values.pollStatusInterface.parameters] : [], + parameters: values.pollStatusInterface?.parameters + ? [...values.pollStatusInterface.parameters] + : [], body: values.pollStatusInterface?.body || '', pollingSuccessConfig: { - successField: values.pollStatusInterface?.pollingSuccessConfig?.successField || '', - successValue: values.pollStatusInterface?.pollingSuccessConfig?.successValue || '' + successField: + values.pollStatusInterface?.pollingSuccessConfig?.successField || + '', + successValue: + values.pollStatusInterface?.pollingSuccessConfig?.successValue || + '' }, pollingFailureConfig: { - failureField: values.pollStatusInterface?.pollingFailureConfig?.failureField || '', - failureValue: values.pollStatusInterface?.pollingFailureConfig?.failureValue || '' + failureField: + values.pollStatusInterface?.pollingFailureConfig?.failureField || + '', + failureValue: + values.pollStatusInterface?.pollingFailureConfig?.failureValue || + '' }, - responseParameters: values.pollStatusInterface?.responseParameters ? [...values.pollStatusInterface.responseParameters] : [] + responseParameters: values.pollStatusInterface?.responseParameters + ? [...values.pollStatusInterface.responseParameters] + : [] } } @@ -743,14 +823,18 @@ export function useForm(id?: number) { state.detailForm.stopInterface = { url: values.stopInterface?.url || '', method: values.stopInterface?.method || 'POST', - parameters: values.stopInterface?.parameters ? [...values.stopInterface.parameters] : [], + parameters: values.stopInterface?.parameters + ? [...values.stopInterface.parameters] + : [], body: values.stopInterface?.body || '', - responseParameters: values.stopInterface?.responseParameters ? [...values.stopInterface.responseParameters] : [] + responseParameters: values.stopInterface?.responseParameters + ? [...values.stopInterface.responseParameters] + : [] } } - delete state.detailForm.userName; - delete state.detailForm.password; + delete state.detailForm.userName + delete state.detailForm.password } } @@ -919,4 +1003,4 @@ export const datasourceTypeList: IDataBaseOption[] = Object.values( ).map((item) => { item.class = 'options-datasource-type' return item -}) \ No newline at end of file +}) diff --git a/dolphinscheduler-ui/src/views/projects/components/dependencies/dependencies-modal.tsx b/dolphinscheduler-ui/src/views/projects/components/dependencies/dependencies-modal.tsx index 207673a2a837..e405f720a346 100644 --- a/dolphinscheduler-ui/src/views/projects/components/dependencies/dependencies-modal.tsx +++ b/dolphinscheduler-ui/src/views/projects/components/dependencies/dependencies-modal.tsx @@ -59,7 +59,7 @@ export default defineComponent({ } const cancelToHandle = () => { - ctx.emit('update:show', showRef) + ctx.emit('update:show', showRef.value) } const renderDownstreamDependencies = () => { diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts index 3c7b6ea9b1bd..946965aedf88 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts @@ -53,7 +53,6 @@ export { useTaskDefinition } from './use-task-definition' export { useJavaTaskMainJar } from './use-java-task-main-jar' export { useExternalSystem } from './use-external-system' - export { useShell } from './use-shell' export { useSpark } from './use-spark' export { useMr } from './use-mr' diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts index c0c21134bb11..bfaaf8ef22cd 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts @@ -34,7 +34,9 @@ export function useDatasource( const { t } = useI18n() const options = ref([] as { label: string; value: string }[]) - const datasourceOptions = ref([] as { label: string; value: number | string }[]) + const datasourceOptions = ref( + [] as { label: string; value: number | string }[] + ) const datasourceTypes = [ { diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-external-system.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-external-system.ts index 2749a29bf15b..0a11ba7edd03 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-external-system.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-external-system.ts @@ -16,10 +16,16 @@ */ import { ref, onMounted, nextTick, Ref } from 'vue' import { useI18n } from 'vue-i18n' -import { queryDataSourceList, queryExternalSystemTasks } from '@/service/modules/data-source' +import { + queryDataSourceList, + queryExternalSystemTasks +} from '@/service/modules/data-source' import { find } from 'lodash' import type { IJsonItem } from '../types' -import type { TypeReq } from '@/service/modules/data-source/types' +import type { + TypeReq, + IDataBase as unusedIDataBase +} from '@/service/modules/data-source/types' export function useExternalSystem( model: { [field: string]: any }, @@ -39,7 +45,7 @@ export function useExternalSystem( const parameters = { type: 'THIRDPARTY_SYSTEM_CONNECTOR' } as TypeReq - + const res = await queryDataSourceList(parameters) datasourceOptions.value = res.map((item: any) => ({ label: item.name, @@ -84,7 +90,7 @@ export function useExternalSystem( const onTaskChange = (value: string) => { if (value) { - const taskItem = taskOptions.value.find(item => item.value === value) + const taskItem = taskOptions.value.find((item) => item.value === value) if (taskItem) { model.externalTaskName = taskItem.label // Set the name based on the selected task } @@ -129,10 +135,12 @@ export function useExternalSystem( required: true, validator(unuse: any, value) { if (!value) { - return new Error(t('thirdparty_api_source.external_system_task_required')) + return new Error( + t('thirdparty_api_source.external_system_task_required') + ) } } } } ] -} \ No newline at end of file +} diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/use-external-system.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/use-external-system.ts index 260de545045f..52a0b2b435cd 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/use-external-system.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/tasks/use-external-system.ts @@ -15,7 +15,6 @@ * limitations under the License. */ - import { reactive } from 'vue' import * as Fields from '../fields/index' import type { IJsonItem, INodeData, ITaskData } from '../types' diff --git a/dolphinscheduler-ui/src/views/projects/workflow/components/dag/dag.module.scss b/dolphinscheduler-ui/src/views/projects/workflow/components/dag/dag.module.scss index ef65c69d7d56..d2b5e81f93b6 100644 --- a/dolphinscheduler-ui/src/views/projects/workflow/components/dag/dag.module.scss +++ b/dolphinscheduler-ui/src/views/projects/workflow/components/dag/dag.module.scss @@ -230,8 +230,8 @@ $bgLight: #ffffff; &.icon-flink_stream { background-image: url('/images/task-icons/flink_hover.png'); } - &.icon-external_stream { - background-image: url('/images/task-icons/external_stream_hover.png'); + &.icon-external_system { + background-image: url('/images/task-icons/external_system_hover.png'); } &.icon-mr { background-image: url('/images/task-icons/mr_hover.png'); diff --git a/dolphinscheduler-ui/src/views/projects/workflow/definition/tree/index.tsx b/dolphinscheduler-ui/src/views/projects/workflow/definition/tree/index.tsx index 7098e2a8b679..8bf9fa90f27a 100644 --- a/dolphinscheduler-ui/src/views/projects/workflow/definition/tree/index.tsx +++ b/dolphinscheduler-ui/src/views/projects/workflow/definition/tree/index.tsx @@ -157,10 +157,13 @@ export default defineComponent({ taskType: 'FLINK_STREAM', color: '#d68f5b', image: `${import.meta.env.BASE_URL}images/task-icons/flink.png` - },{ + }, + { taskType: 'EXTERNAL_SYSTEM', color: '#d68f5b', - image: `${import.meta.env.BASE_URL}images/task-icons/external_system.png` + image: `${ + import.meta.env.BASE_URL + }images/task-icons/external_system.png` } ]) diff --git a/dolphinscheduler-ui/src/views/thirdparty-api-source/index.tsx b/dolphinscheduler-ui/src/views/thirdparty-api-source/index.tsx deleted file mode 100644 index 0315e3a513c4..000000000000 --- a/dolphinscheduler-ui/src/views/thirdparty-api-source/index.tsx +++ /dev/null @@ -1,250 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { defineComponent, ref, onMounted, computed } from 'vue' -import { - NDataTable, - NButton, - NSpace, - NPopconfirm, - NIcon, - NPagination, - NTooltip -} from 'naive-ui' -import { useI18n } from 'vue-i18n' -import { useRouter } from 'vue-router' -import { deleteThirdpartyApiSource, queryThirdpartyApiSourceListPaging, createThirdpartyApiSource, updateThirdpartyApiSource, getThirdpartyApiSourceById, testThirdpartyApiSourceConnection } from '@/service/modules/thirdparty-api-source' -import type { ThirdpartyApiSource } from '@/service/modules/thirdparty-api-source/types' -import Card from '@/components/card' -import Search from '@/components/input-search' -import { EditOutlined, DeleteOutlined } from '@vicons/antd' -import ThirdpartyApiSourceModal from './modal' - -export default defineComponent({ - name: 'ThirdpartyApiSourceList', - setup() { - const { t } = useI18n() - - const tableData = ref([]) - const loading = ref(false) - const searchVal = ref('') - const page = ref(1) - const pageSize = ref(10) - const itemCount = ref(0) - const showModal = ref(false) - const editData = ref(null) - const operationType = ref<'create' | 'edit'>('create') - - // 获取真实接口数据 - const getTableData = async () => { - loading.value = true - try { - const res = await queryThirdpartyApiSourceListPaging({ - pageNo: page.value, - pageSize: pageSize.value, - searchVal: searchVal.value || undefined - }) as any - if(res) { - tableData.value = (res.totalList || []) as ThirdpartyApiSource[] - itemCount.value = res.total || 0 - } - } finally { - loading.value = false - } - } - - const handleDelete = async (row: ThirdpartyApiSource) => { - await deleteThirdpartyApiSource(row.id!) - await getTableData() - } - - const changePage = (p: number) => { - page.value = p - getTableData() - } - const changePageSize = (ps: number) => { - page.value = 1 - pageSize.value = ps - getTableData() - } - - const handleCreate = () => { - editData.value = null - operationType.value = 'create' - showModal.value = true - } - const handleEdit = async (row: any) => { - // 获取详情 - const detail = await getThirdpartyApiSourceById(row.id) - editData.value = detail - operationType.value = 'edit' - showModal.value = true - } - const handleModalClose = () => { - showModal.value = false - editData.value = null - operationType.value = 'create' - } - const handleModalSubmit = async (data: any) => { - const res = data.id ? await updateThirdpartyApiSource(data.id, data) : await createThirdpartyApiSource(data) - if(res) { - window.$message.success(data.id ? t('message.edit.success') : t('message.create.success')) - } else { - window.$message.error(data.id ? t('message.edit.failed') : t('message.create.failed')) - } - showModal.value = false - editData.value = null - getTableData() - } - const handleModalTest = async (data: any) => { - try { - const res = await testThirdpartyApiSourceConnection(data) - window.$message.success( - res && res.msg - ? res.msg - : `${t('datasource.test_connect')} ${t('datasource.success')}` - ) - } catch (e: any) { - // Error handling is done by the calling function - } - } - - const columns = computed(() => [ - { - title: t('thirdparty_api_source.id'), - key: 'id' - }, - { - title: t('thirdparty_api_source.system_name'), - key: 'name' - }, - { - title: t('thirdparty_api_source.create_time'), - key: 'createTime', - render: (row: any) => row.createTime ? row.createTime : '-' - }, - { - title: t('thirdparty_api_source.update_time'), - key: 'updateTime', - render: (row: any) => row.updateTime ? row.updateTime : '-' - }, - { - title: t('datasource.operation'), - key: 'actions', - render: (row: ThirdpartyApiSource) => { - return ( - - - {{ - trigger: () => ( - handleEdit(row)} - > - - - ), - default: () => t('thirdparty_api_source.edit') - }} - - - {{ - trigger: () => ( - handleDelete(row)}> - {{ - trigger: () => ( - - - - ), - default: () => t('datasource.delete_confirm') - }} - - ), - default: () => t('thirdparty_api_source.delete') - }} - - - ) - } - } - ]) - - onMounted(() => { - getTableData() - }) - - return () => ( - - - - - {t('thirdparty_api_source.create_thirdparty_api_source')} - - - - - {t('thirdparty_api_source.search')} - - - - - - - - - - - - - - - ) - } -}) \ No newline at end of file diff --git a/dolphinscheduler-ui/src/views/thirdparty-api-source/modal.tsx b/dolphinscheduler-ui/src/views/thirdparty-api-source/modal.tsx index e71d71d86a8c..9f7b9dfd2995 100644 --- a/dolphinscheduler-ui/src/views/thirdparty-api-source/modal.tsx +++ b/dolphinscheduler-ui/src/views/thirdparty-api-source/modal.tsx @@ -34,7 +34,7 @@ import { NIcon } from 'naive-ui' import MonacoEditor from '@/components/monaco-editor' -import { InfoCircleOutlined } from '@vicons/antd'; +import { InfoCircleOutlined } from '@vicons/antd' export default defineComponent({ name: 'ThirdpartyApiSourceModal', @@ -97,9 +97,7 @@ export default defineComponent({ method: 'POST', parameters: [] as any[], body: '', - responseParameters: [ - { key: 'taskInstanceId', jsonPath: '' } - ] + responseParameters: [{ key: 'taskInstanceId', jsonPath: '' }] }, pollStatusInterface: { url: '', @@ -126,10 +124,18 @@ export default defineComponent({ // Form validation rules const rules = { serviceAddress: [ - { required: true, message: t('thirdparty_api_source.service_address_required'), trigger: 'blur' } + { + required: true, + message: t('thirdparty_api_source.service_address_required'), + trigger: 'blur' + } ], 'authConfig.authType': [ - { required: true, message: t('thirdparty_api_source.auth_type_required'), trigger: 'change' } + { + required: true, + message: t('thirdparty_api_source.auth_type_required'), + trigger: 'change' + } ], 'authConfig.basicUsername': [ { @@ -157,7 +163,9 @@ export default defineComponent({ { validator: (rule: any, value: any) => { if (form.authConfig.authType === 'OAUTH2' && !value) { - return new Error(t('thirdparty_api_source.oauth2_token_url_required')) + return new Error( + t('thirdparty_api_source.oauth2_token_url_required') + ) } return true }, @@ -168,7 +176,9 @@ export default defineComponent({ { validator: (rule: any, value: any) => { if (form.authConfig.authType === 'OAUTH2' && !value) { - return new Error(t('thirdparty_api_source.oauth2_client_id_required')) + return new Error( + t('thirdparty_api_source.oauth2_client_id_required') + ) } return true }, @@ -179,7 +189,9 @@ export default defineComponent({ { validator: (rule: any, value: any) => { if (form.authConfig.authType === 'OAUTH2' && !value) { - return new Error(t('thirdparty_api_source.oauth2_client_secret_required')) + return new Error( + t('thirdparty_api_source.oauth2_client_secret_required') + ) } return true }, @@ -190,7 +202,9 @@ export default defineComponent({ { validator: (rule: any, value: any) => { if (form.authConfig.authType === 'OAUTH2' && !value) { - return new Error(t('thirdparty_api_source.oauth2_grant_type_required')) + return new Error( + t('thirdparty_api_source.oauth2_grant_type_required') + ) } return true }, @@ -209,22 +223,40 @@ export default defineComponent({ } ], 'selectInterface.url': [ - { required: true, message: t('thirdparty_api_source.input_interface_url_required'), trigger: ['blur', 'change'] } + { + required: true, + message: t('thirdparty_api_source.input_interface_url_required'), + trigger: ['blur', 'change'] + } ], 'submitInterface.url': [ - { required: true, message: t('thirdparty_api_source.submit_interface_url_required'), trigger: ['blur', 'change'] } + { + required: true, + message: t('thirdparty_api_source.submit_interface_url_required'), + trigger: ['blur', 'change'] + } ], 'pollStatusInterface.url': [ - { required: true, message: t('thirdparty_api_source.query_interface_url_required'), trigger: ['blur', 'change'] } + { + required: true, + message: t('thirdparty_api_source.query_interface_url_required'), + trigger: ['blur', 'change'] + } ], 'stopInterface.url': [ - { required: true, message: t('thirdparty_api_source.stop_interface_url_required'), trigger: ['blur', 'change'] } + { + required: true, + message: t('thirdparty_api_source.stop_interface_url_required'), + trigger: ['blur', 'change'] + } ], 'pollStatusInterface.pollingSuccessConfig': [ { validator: (rule: any, value: any) => { if (!value.successField || !value.successValue) { - return new Error(t('thirdparty_api_source.success_condition_required')) + return new Error( + t('thirdparty_api_source.success_condition_required') + ) } return true }, @@ -235,7 +267,9 @@ export default defineComponent({ { validator: (rule: any, value: any) => { if (!value.failureField || !value.failureValue) { - return new Error(t('thirdparty_api_source.failure_condition_required')) + return new Error( + t('thirdparty_api_source.failure_condition_required') + ) } return true }, @@ -252,7 +286,9 @@ export default defineComponent({ return new Error(t('thirdparty_api_source.id_jsonpath_required')) } if (!nameField || !nameField.jsonPath) { - return new Error(t('thirdparty_api_source.name_jsonpath_required')) + return new Error( + t('thirdparty_api_source.name_jsonpath_required') + ) } return true }, @@ -297,9 +333,7 @@ export default defineComponent({ method: 'POST', parameters: [] as any[], body: '', - responseParameters: [ - { key: 'taskInstanceId', jsonPath: '' } - ] + responseParameters: [{ key: 'taskInstanceId', jsonPath: '' }] }, pollStatusInterface: { url: '', @@ -309,13 +343,18 @@ export default defineComponent({ pollingSuccessConfig: { successField: '', successValue: '' }, pollingFailureConfig: { failureField: '', failureValue: '' } }, - stopInterface: { url: '', method: 'POST', parameters: [] as any[], body: '' } + stopInterface: { + url: '', + method: 'POST', + parameters: [] as any[], + body: '' + } }) // Function to reset form data const resetForm = () => { const initialState = getInitialFormState() - Object.keys(form).forEach(key => { + Object.keys(form).forEach((key) => { delete (form as any)[key] }) Object.assign(form, initialState) @@ -325,36 +364,43 @@ export default defineComponent({ // Save original edit data for testing connection const originalEditData = ref(null) // Listen for modal visibility and data changes - watch([() => props.show, () => props.data, () => props.operationType], ([show, data, operationType]) => { - if (show) { - if (data && operationType === 'edit') { - originalEditData.value = JSON.parse(JSON.stringify(data)) - resetForm() - const editData = originalEditData.value - // Use data returned from backend completely - Object.assign(form, editData) - } else { - originalEditData.value = null - resetForm() - // Only set default values in create mode - form.authConfig.authType = 'BASIC_AUTH' - form.authConfig.headerPrefix = 'Basic' + watch( + [() => props.show, () => props.data, () => props.operationType], + ([show, data, operationType]) => { + if (show) { + if (data && operationType === 'edit') { + originalEditData.value = JSON.parse(JSON.stringify(data)) + resetForm() + const editData = originalEditData.value + // Use data returned from backend completely + Object.assign(form, editData) + } else { + originalEditData.value = null + resetForm() + // Only set default values in create mode + form.authConfig.authType = 'BASIC_AUTH' + form.authConfig.headerPrefix = 'Basic' + } } - } - }, { immediate: true }) + }, + { immediate: true } + ) - watch(() => form.authConfig.authType, (newAuthType) => { - // Only automatically set headerPrefix in create mode - if (!isEditMode.value) { - if (newAuthType === 'BASIC_AUTH') { - form.authConfig.headerPrefix = 'Basic' - } else if (newAuthType === 'JWT' || newAuthType === 'OAUTH2') { - form.authConfig.headerPrefix = 'Bearer' - } else { - form.authConfig.headerPrefix = '' + watch( + () => form.authConfig.authType, + (newAuthType) => { + // Only automatically set headerPrefix in create mode + if (!isEditMode.value) { + if (newAuthType === 'BASIC_AUTH') { + form.authConfig.headerPrefix = 'Basic' + } else if (newAuthType === 'JWT' || newAuthType === 'OAUTH2') { + form.authConfig.headerPrefix = 'Bearer' + } else { + form.authConfig.headerPrefix = '' + } } } - }) + ) const handleClose = () => { resetForm() @@ -362,12 +408,14 @@ export default defineComponent({ } const handleSubmit = () => { - (formRef.value as any)?.validate((errors: any) => { + ;(formRef.value as any)?.validate((errors: any) => { if (!errors) { if (isEditMode.value && originalEditData.value) { - const submitData = JSON.parse(JSON.stringify(originalEditData.value)) + const submitData = JSON.parse( + JSON.stringify(originalEditData.value) + ) const initialState = getInitialFormState() - Object.keys(initialState).forEach(key => { + Object.keys(initialState).forEach((key) => { if (form.hasOwnProperty(key)) { submitData[key] = (form as any)[key] } @@ -381,12 +429,12 @@ export default defineComponent({ } const handleTest = () => { - (formRef.value as any)?.validate((errors: any) => { + ;(formRef.value as any)?.validate((errors: any) => { if (!errors) { if (isEditMode.value && originalEditData.value) { const testData = JSON.parse(JSON.stringify(originalEditData.value)) const initialState = getInitialFormState() - Object.keys(initialState).forEach(key => { + Object.keys(initialState).forEach((key) => { if (form.hasOwnProperty(key)) { testData[key] = (form as any)[key] } @@ -400,904 +448,1052 @@ export default defineComponent({ } // Location dropdown options linked with method - const getLocationOptions = (_method: string) => { + const getLocationOptions = (unusedMethod: string) => { return [ { label: 'Header', value: 'HEADER' }, { label: 'Param', value: 'PARAM' } ] } - return () => ( - -
- - - - - + return () => ( + +
+ + + + - {/* Interface timeout */} - - - {{ - suffix: () => t('thirdparty_api_source.millisecond') - }} - -
- {t('thirdparty_api_source.interface_timeout_description')} -
-
+ {/* Interface timeout */} + + + {{ + suffix: () => t('thirdparty_api_source.millisecond') + }} + +
+ {t('thirdparty_api_source.interface_timeout_description')} +
+
- + - {/* authType */} - - {{ - label: () => ( - - {t('thirdparty_api_source.auth_type')} - - {{ - trigger: () => ( - - - - ), - default: () => t('thirdparty_api_source.auth_type_detail_info') - }} - - - ), - default: () => ( - - ) - }} - + {/* authType */} + + {{ + label: () => ( + + {t('thirdparty_api_source.auth_type')} + + {{ + trigger: () => ( + + + + ), + default: () => + t('thirdparty_api_source.auth_type_detail_info') + }} + + + ), + default: () => ( + + ) + }} + - - - + + + - {/* BASIC_AUTH */} - - - - - - + {/* BASIC_AUTH */} + + + + + + - {/* OAUTH2 */} - - {{ - label: () => ( - - {t('thirdparty_api_source.oauth2_token_url')} - - {{ - trigger: () => ( - - - - ), - default: () => t('thirdparty_api_source.oauth2_url_info') - }} - - - ), - default: () => ( - - ) - }} - + {/* OAUTH2 */} + + {{ + label: () => ( + + {t('thirdparty_api_source.oauth2_token_url')} + + {{ + trigger: () => ( + + + + ), + default: () => + t('thirdparty_api_source.oauth2_url_info') + }} + + + ), + default: () => ( + + ) + }} + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - {/* JWT */} - - - + {/* JWT */} + + + - {/* additional params */} - - ({ key: '', value: '' })} - style={{ width: '100%' }} - > - {{ - default: ({ value }: { value: { key: string; value: string } }) => ( - - - - - ) - }} - - + {/* additional params */} + + ({ key: '', value: '' })} + style={{ width: '100%' }} + > + {{ + default: ({ + value + }: { + value: { key: string; value: string } + }) => ( + + + + + ) + }} + + - + - {/* selectInterface */} - - {{ - label: () => ( - - {t('thirdparty_api_source.input_interface')} - - {{ - trigger: () => ( - - - - ), - default: () => t('thirdparty_api_source.input_interface_detail_info') - }} - - - ), - default: () => ( - <> - formRef.value?.validate?.()} - /> - - - ) - }} - + {/* selectInterface */} + + {{ + label: () => ( + + {t('thirdparty_api_source.input_interface')} + + {{ + trigger: () => ( + + + + ), + default: () => + t('thirdparty_api_source.input_interface_detail_info') + }} + + + ), + default: () => ( + <> + formRef.value?.validate?.()} + /> + + + ) + }} + - - ({ paramName: '', paramValue: '', location: 'HEADER' })} - style={{ width: '100%' }} - > - {{ - default: ({ - value - }: { - value: { paramName: string; paramValue: string; location: string } - }) => ( - - - - - - ) - }} - - + + ({ + paramName: '', + paramValue: '', + location: 'HEADER' + })} + style={{ width: '100%' }} + > + {{ + default: ({ + value + }: { + value: { + paramName: string + paramValue: string + location: string + } + }) => ( + + + + + + ) + }} + + - {(form.selectInterface.method === 'POST' || form.selectInterface.method === 'PUT') && ( - - {{ - label: () => ( - - {t('thirdparty_api_source.request_body')} - - {{ - trigger: () => ( - - - - ), - default: () => t('thirdparty_api_source.input_interface_body_info') - }} - - - ), - default: () => ( - - ) - }} - - )} + {(form.selectInterface.method === 'POST' || + form.selectInterface.method === 'PUT') && ( + + {{ + label: () => ( + + {t('thirdparty_api_source.request_body')} + + {{ + trigger: () => ( + + + + ), + default: () => + t('thirdparty_api_source.input_interface_body_info') + }} + + + ), + default: () => ( + + ) + }} + + )} - - {{ - label: () => ( - - {t('thirdparty_api_source.extract_response_data')} - - {{ - trigger: () => ( - - - - ), - default: () => t('thirdparty_api_source.input_interface_extract_info') - }} - - - ), - default: () => ( - ({ key: '', jsonPath: '', disabled: false })} - style={{ width: '100%' }} - > - {{ - default: ({ - value - }: { - value: { key: string; jsonPath: string; disabled: boolean } - }) => ( - - - - - ) - }} - - ) - }} - + + {{ + label: () => ( + + + {t('thirdparty_api_source.extract_response_data')} + + + {{ + trigger: () => ( + + + + ), + default: () => + t( + 'thirdparty_api_source.input_interface_extract_info' + ) + }} + + + ), + default: () => ( + ({ + key: '', + jsonPath: '', + disabled: false + })} + style={{ width: '100%' }} + > + {{ + default: ({ + value + }: { + value: { + key: string + jsonPath: string + disabled: boolean + } + }) => ( + + + + + ) + }} + + ) + }} + - + - {/* submitInterface */} - - {{ - label: () => ( - - {t('thirdparty_api_source.submit_interface')} - - {{ - trigger: () => ( - - - - ), - default: () => t('thirdparty_api_source.submit_interface_detail_info') - }} - - - ), - default: () => ( - <> - formRef.value?.validate?.()} - /> - - - ) - }} - + {/* submitInterface */} + + {{ + label: () => ( + + {t('thirdparty_api_source.submit_interface')} + + {{ + trigger: () => ( + + + + ), + default: () => + t( + 'thirdparty_api_source.submit_interface_detail_info' + ) + }} + + + ), + default: () => ( + <> + formRef.value?.validate?.()} + /> + + + ) + }} + - - ({ paramName: '', paramValue: '', location: 'HEADER' })} - style={{ width: '100%' }} - > - {{ - default: ({ - value - }: { - value: { paramName: string; paramValue: string; location: string } - }) => ( - - - - - - ) - }} - - + + ({ + paramName: '', + paramValue: '', + location: 'HEADER' + })} + style={{ width: '100%' }} + > + {{ + default: ({ + value + }: { + value: { + paramName: string + paramValue: string + location: string + } + }) => ( + + + + + + ) + }} + + - {(form.submitInterface.method === 'POST' || form.submitInterface.method === 'PUT') && ( - - {{ - label: () => ( - - {t('thirdparty_api_source.request_body')} - - {{ - trigger: () => ( - - - - ), - default: () => t('thirdparty_api_source.submit_interface_body_info') - }} - - - ), - default: () => ( - - ) - }} - - )} + {(form.submitInterface.method === 'POST' || + form.submitInterface.method === 'PUT') && ( + + {{ + label: () => ( + + {t('thirdparty_api_source.request_body')} + + {{ + trigger: () => ( + + + + ), + default: () => + t( + 'thirdparty_api_source.submit_interface_body_info' + ) + }} + + + ), + default: () => ( + + ) + }} + + )} - - {{ - label: () => ( - - {t('thirdparty_api_source.extract_response_data')} - - {{ - trigger: () => ( - - - - ), - default: () => t('thirdparty_api_source.submit_interface_extract_info') - }} - - - ), - default: () => ( - ({ key: '', jsonPath: '', disabled: false })} - style={{ width: '100%' }} - > - {{ - default: ({ - value - }: { - value: { key: string; jsonPath: string; disabled: boolean } - }) => ( - - - - - ) - }} - - ) - }} - + + {{ + label: () => ( + + + {t('thirdparty_api_source.extract_response_data')} + + + {{ + trigger: () => ( + + + + ), + default: () => + t( + 'thirdparty_api_source.submit_interface_extract_info' + ) + }} + + + ), + default: () => ( + ({ + key: '', + jsonPath: '', + disabled: false + })} + style={{ width: '100%' }} + > + {{ + default: ({ + value + }: { + value: { + key: string + jsonPath: string + disabled: boolean + } + }) => ( + + + + + ) + }} + + ) + }} + - + - {/* pollStatusInterface */} - - {{ - label: () => ( - - {t('thirdparty_api_source.query_interface')} - - {{ - trigger: () => ( - - - - ), - default: () => t('thirdparty_api_source.query_interface_detail_info') - }} - - - ), - default: () => ( - <> - formRef.value?.validate?.()} - /> - - - ) - }} - + {/* pollStatusInterface */} + + {{ + label: () => ( + + {t('thirdparty_api_source.query_interface')} + + {{ + trigger: () => ( + + + + ), + default: () => + t('thirdparty_api_source.query_interface_detail_info') + }} + + + ), + default: () => ( + <> + formRef.value?.validate?.()} + /> + + + ) + }} + - - ({ paramName: '', paramValue: '', location: 'HEADER' })} - style={{ width: '100%' }} - > - {{ - default: ({ - value - }: { - value: { paramName: string; paramValue: string; location: string } - }) => ( - - - - - - ) - }} - - + + ({ + paramName: '', + paramValue: '', + location: 'HEADER' + })} + style={{ width: '100%' }} + > + {{ + default: ({ + value + }: { + value: { + paramName: string + paramValue: string + location: string + } + }) => ( + + + + + + ) + }} + + - {(form.pollStatusInterface.method === 'POST' || - form.pollStatusInterface.method === 'PUT') && ( - - {{ - label: () => ( - - {t('thirdparty_api_source.request_body')} - - {{ - trigger: () => ( - - - - ), - default: () => t('thirdparty_api_source.query_interface_body_info') - }} - - - ), - default: () => ( - - ) - }} - - )} + {(form.pollStatusInterface.method === 'POST' || + form.pollStatusInterface.method === 'PUT') && ( + + {{ + label: () => ( + + {t('thirdparty_api_source.request_body')} + + {{ + trigger: () => ( + + + + ), + default: () => + t('thirdparty_api_source.query_interface_body_info') + }} + + + ), + default: () => ( + + ) + }} + + )} - - {{ - label: () => ( - - {t('thirdparty_api_source.success_condition')} - - {{ - trigger: () => ( - - - - ), - default: () => t('thirdparty_api_source.query_interface_success_info') - }} - - - ), - default: () => ( - <> - formRef.value?.validate?.()} - /> - formRef.value?.validate?.()} - /> - - ) - }} - + + {{ + label: () => ( + + {t('thirdparty_api_source.success_condition')} + + {{ + trigger: () => ( + + + + ), + default: () => + t( + 'thirdparty_api_source.query_interface_success_info' + ) + }} + + + ), + default: () => ( + <> + formRef.value?.validate?.()} + /> + formRef.value?.validate?.()} + /> + + ) + }} + - - {{ - label: () => ( - - {t('thirdparty_api_source.failure_condition')} - - {{ - trigger: () => ( - - - - ), - default: () => t('thirdparty_api_source.query_interface_failed_info') - }} - - - ), - default: () => ( - <> - formRef.value?.validate?.()} - /> - formRef.value?.validate?.()} - /> - - ) - }} - + + {{ + label: () => ( + + {t('thirdparty_api_source.failure_condition')} + + {{ + trigger: () => ( + + + + ), + default: () => + t('thirdparty_api_source.query_interface_failed_info') + }} + + + ), + default: () => ( + <> + formRef.value?.validate?.()} + /> + formRef.value?.validate?.()} + /> + + ) + }} + - + - {/* stopInterface */} - - {{ - label: () => ( - - {t('thirdparty_api_source.stop_interface')} - - {{ - trigger: () => ( - - - - ), - default: () => t('thirdparty_api_source.stop_interface_detail_info') - }} - - - ), - default: () => ( - <> - formRef.value?.validate?.()} - /> - - - ) - }} - + {/* stopInterface */} + + {{ + label: () => ( + + {t('thirdparty_api_source.stop_interface')} + + {{ + trigger: () => ( + + + + ), + default: () => + t('thirdparty_api_source.stop_interface_detail_info') + }} + + + ), + default: () => ( + <> + formRef.value?.validate?.()} + /> + + + ) + }} + - - ({ paramName: '', paramValue: '', location: 'HEADER' })} - style={{ width: '100%' }} - > - {{ - default: ({ - value - }: { - value: { paramName: string; paramValue: string; location: string } - }) => ( - - - - - - ) - }} - - + + ({ + paramName: '', + paramValue: '', + location: 'HEADER' + })} + style={{ width: '100%' }} + > + {{ + default: ({ + value + }: { + value: { + paramName: string + paramValue: string + location: string + } + }) => ( + + + + + + ) + }} + + - {(form.stopInterface.method === 'POST' || - form.stopInterface.method === 'PUT') && ( - - {{ - label: () => ( - - {t('thirdparty_api_source.request_body')} - - {{ - trigger: () => ( - - - - ), - default: () => t('thirdparty_api_source.stop_interface_body_info') - }} - - - ), - default: () => ( - - ) - }} - - )} -
-
+ {(form.stopInterface.method === 'POST' || + form.stopInterface.method === 'PUT') && ( + + {{ + label: () => ( + + {t('thirdparty_api_source.request_body')} + + {{ + trigger: () => ( + + + + ), + default: () => + t('thirdparty_api_source.stop_interface_body_info') + }} + + + ), + default: () => ( + + ) + }} + + )} +
+
-
- - - {t('thirdparty_api_source.cancel')} - - - {t('thirdparty_api_source.test')} - - - {t('thirdparty_api_source.submit')} - - -
-
- ) +
+ + + {t('thirdparty_api_source.cancel')} + + + {t('thirdparty_api_source.test')} + + + {t('thirdparty_api_source.submit')} + + +
+ + ) } }) diff --git a/dolphinscheduler-ui/src/views/thirdparty-api-source/use-table.ts b/dolphinscheduler-ui/src/views/thirdparty-api-source/use-table.ts index 1862c95e953f..976bc55ca283 100644 --- a/dolphinscheduler-ui/src/views/thirdparty-api-source/use-table.ts +++ b/dolphinscheduler-ui/src/views/thirdparty-api-source/use-table.ts @@ -69,4 +69,4 @@ export function useTable() { } return { data, changePage, changePageSize, deleteRecord, updateList } -} \ No newline at end of file +} From deed2ba23da99763ea93ccddfd070327d796ddc3 Mon Sep 17 00:00:00 2001 From: yud8 Date: Wed, 7 Jan 2026 15:19:41 +0800 Subject: [PATCH 06/11] [Feature-17501]Third-party System APl Connector fix UI CI error2 --- dolphinscheduler-ui/src/views/datasource/list/use-form.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/dolphinscheduler-ui/src/views/datasource/list/use-form.ts b/dolphinscheduler-ui/src/views/datasource/list/use-form.ts index 7e8a34d694cb..4ead40b0fff5 100644 --- a/dolphinscheduler-ui/src/views/datasource/list/use-form.ts +++ b/dolphinscheduler-ui/src/views/datasource/list/use-form.ts @@ -639,7 +639,6 @@ export function useForm(id?: number) { } const setFieldsValue = (values: IDataSource) => { - // 处理第三方系统连接器相关的接口参数,确保数组类型正确 const processedValues = { ...values, other: values.other ? JSON.stringify(values.other) : values.other, From 0690673aba6b5d9fa2d69583c28dcd326b07af45 Mon Sep 17 00:00:00 2001 From: yud8 Date: Wed, 7 Jan 2026 15:20:33 +0800 Subject: [PATCH 07/11] [Feature-17501]Third-party System APl Connector fix CI error -revert format changes --- dolphinscheduler-ui/src/views/datasource/list/types.ts | 2 +- .../projects/components/dependencies/dependencies-modal.tsx | 2 +- .../src/views/projects/task/components/node/fields/index.ts | 1 + .../projects/task/components/node/fields/use-datasource.ts | 4 +--- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/dolphinscheduler-ui/src/views/datasource/list/types.ts b/dolphinscheduler-ui/src/views/datasource/list/types.ts index 8dbc7c9cfceb..85b9e5159dd6 100644 --- a/dolphinscheduler-ui/src/views/datasource/list/types.ts +++ b/dolphinscheduler-ui/src/views/datasource/list/types.ts @@ -108,4 +108,4 @@ export { IDataBaseOption, IDataBaseOptionKeys, TableColumns -} +} \ No newline at end of file diff --git a/dolphinscheduler-ui/src/views/projects/components/dependencies/dependencies-modal.tsx b/dolphinscheduler-ui/src/views/projects/components/dependencies/dependencies-modal.tsx index e405f720a346..207673a2a837 100644 --- a/dolphinscheduler-ui/src/views/projects/components/dependencies/dependencies-modal.tsx +++ b/dolphinscheduler-ui/src/views/projects/components/dependencies/dependencies-modal.tsx @@ -59,7 +59,7 @@ export default defineComponent({ } const cancelToHandle = () => { - ctx.emit('update:show', showRef.value) + ctx.emit('update:show', showRef) } const renderDownstreamDependencies = () => { diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts index 946965aedf88..3c7b6ea9b1bd 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts @@ -53,6 +53,7 @@ export { useTaskDefinition } from './use-task-definition' export { useJavaTaskMainJar } from './use-java-task-main-jar' export { useExternalSystem } from './use-external-system' + export { useShell } from './use-shell' export { useSpark } from './use-spark' export { useMr } from './use-mr' diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts index bfaaf8ef22cd..c0c21134bb11 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts @@ -34,9 +34,7 @@ export function useDatasource( const { t } = useI18n() const options = ref([] as { label: string; value: string }[]) - const datasourceOptions = ref( - [] as { label: string; value: number | string }[] - ) + const datasourceOptions = ref([] as { label: string; value: number | string }[]) const datasourceTypes = [ { From 3ad54730c160091ae7f19fa334de474a5a144c79 Mon Sep 17 00:00:00 2001 From: yud8 Date: Tue, 27 Jan 2026 10:18:52 +0800 Subject: [PATCH 08/11] [Feature-17501]Third-party System APl Connector fix UI CI error removal(task) PYTORCH --- dolphinscheduler-ui/src/store/project/task-type.ts | 4 ---- dolphinscheduler-ui/src/store/project/types.ts | 1 - .../src/views/projects/task/constants/task-type.ts | 4 ---- 3 files changed, 9 deletions(-) diff --git a/dolphinscheduler-ui/src/store/project/task-type.ts b/dolphinscheduler-ui/src/store/project/task-type.ts index 635b90f016d1..77d1ba616cd9 100644 --- a/dolphinscheduler-ui/src/store/project/task-type.ts +++ b/dolphinscheduler-ui/src/store/project/task-type.ts @@ -130,10 +130,6 @@ export const TASK_TYPES_MAP = { alias: 'EXTERNAL_SYSTEM', helperLinkDisable: true }, - PYTORCH: { - alias: 'Pytorch', - helperLinkDisable: true - }, HIVECLI: { alias: 'HIVECLI', helperLinkDisable: true diff --git a/dolphinscheduler-ui/src/store/project/types.ts b/dolphinscheduler-ui/src/store/project/types.ts index ce87bc079e4e..bd71c9a160d3 100644 --- a/dolphinscheduler-ui/src/store/project/types.ts +++ b/dolphinscheduler-ui/src/store/project/types.ts @@ -50,7 +50,6 @@ type TaskType = | 'CHUNJUN' | 'FLINK_STREAM' | 'EXTERNAL_SYSTEM' - | 'PYTORCH' | 'HIVECLI' | 'DMS' | 'DATASYNC' diff --git a/dolphinscheduler-ui/src/views/projects/task/constants/task-type.ts b/dolphinscheduler-ui/src/views/projects/task/constants/task-type.ts index 0fd91734ff63..79045898fcff 100644 --- a/dolphinscheduler-ui/src/views/projects/task/constants/task-type.ts +++ b/dolphinscheduler-ui/src/views/projects/task/constants/task-type.ts @@ -160,10 +160,6 @@ export const TASK_TYPES_MAP = { alias: 'EXTERNAL_SYSTEM', helperLinkDisable: true }, - PYTORCH: { - alias: 'Pytorch', - helperLinkDisable: true - }, HIVECLI: { alias: 'HIVECLI', helperLinkDisable: true From daa86ceeba6bdbc792e8376592f48fff55990ffb Mon Sep 17 00:00:00 2001 From: yud8 Date: Tue, 27 Jan 2026 10:21:18 +0800 Subject: [PATCH 09/11] [Feature-17501]Third-party System APl Connector fix UI CI error pnpm run lint --- dolphinscheduler-ui/src/views/datasource/list/types.ts | 2 +- .../projects/components/dependencies/dependencies-modal.tsx | 2 +- .../src/views/projects/task/components/node/fields/index.ts | 1 - .../projects/task/components/node/fields/use-datasource.ts | 4 +++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/dolphinscheduler-ui/src/views/datasource/list/types.ts b/dolphinscheduler-ui/src/views/datasource/list/types.ts index 85b9e5159dd6..8dbc7c9cfceb 100644 --- a/dolphinscheduler-ui/src/views/datasource/list/types.ts +++ b/dolphinscheduler-ui/src/views/datasource/list/types.ts @@ -108,4 +108,4 @@ export { IDataBaseOption, IDataBaseOptionKeys, TableColumns -} \ No newline at end of file +} diff --git a/dolphinscheduler-ui/src/views/projects/components/dependencies/dependencies-modal.tsx b/dolphinscheduler-ui/src/views/projects/components/dependencies/dependencies-modal.tsx index 207673a2a837..e405f720a346 100644 --- a/dolphinscheduler-ui/src/views/projects/components/dependencies/dependencies-modal.tsx +++ b/dolphinscheduler-ui/src/views/projects/components/dependencies/dependencies-modal.tsx @@ -59,7 +59,7 @@ export default defineComponent({ } const cancelToHandle = () => { - ctx.emit('update:show', showRef) + ctx.emit('update:show', showRef.value) } const renderDownstreamDependencies = () => { diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts index 3c7b6ea9b1bd..946965aedf88 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/index.ts @@ -53,7 +53,6 @@ export { useTaskDefinition } from './use-task-definition' export { useJavaTaskMainJar } from './use-java-task-main-jar' export { useExternalSystem } from './use-external-system' - export { useShell } from './use-shell' export { useSpark } from './use-spark' export { useMr } from './use-mr' diff --git a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts index c0c21134bb11..bfaaf8ef22cd 100644 --- a/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts +++ b/dolphinscheduler-ui/src/views/projects/task/components/node/fields/use-datasource.ts @@ -34,7 +34,9 @@ export function useDatasource( const { t } = useI18n() const options = ref([] as { label: string; value: string }[]) - const datasourceOptions = ref([] as { label: string; value: number | string }[]) + const datasourceOptions = ref( + [] as { label: string; value: number | string }[] + ) const datasourceTypes = [ { From ca52f4a8e912d8a39a7ed46f5b3c83318f9c9b9f Mon Sep 17 00:00:00 2001 From: yud8 Date: Tue, 27 Jan 2026 16:45:00 +0800 Subject: [PATCH 10/11] [Feature-17501]Third-party System APl Connector add unit test --- .../controller/ExternalSystemController.java | 9 +- .../api/service/ExternalSystemService.java | 3 +- .../impl/ExternalSystemServiceImpl.java | 3 +- .../param/PollingInterfaceInfo.java | 2 + .../externalSystem/ExternalSystemTask.java | 5 +- .../ExternalSystemTaskTest.java | 290 ++++++++++++++++++ .../src/service/modules/data-source/index.ts | 7 - 7 files changed, 299 insertions(+), 20 deletions(-) create mode 100644 dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/test/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTaskTest.java diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/ExternalSystemController.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/ExternalSystemController.java index d66fdd731755..f60a6d89c9cd 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/ExternalSystemController.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/controller/ExternalSystemController.java @@ -22,23 +22,19 @@ import org.apache.dolphinscheduler.api.exceptions.ApiException; import org.apache.dolphinscheduler.api.service.ExternalSystemService; import org.apache.dolphinscheduler.api.utils.Result; -import org.apache.dolphinscheduler.common.constants.Constants; import org.apache.dolphinscheduler.dao.entity.ExternalSystemTaskQuery; -import org.apache.dolphinscheduler.dao.entity.User; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestAttribute; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @Tag(name = "EXTERNAL_SYSTEM_TAG") @@ -53,10 +49,9 @@ public class ExternalSystemController extends BaseController { @GetMapping(value = "/queryExternalSystemTasks") @ResponseStatus(HttpStatus.OK) @ApiException(QUERY_EXTERNAL_SYSTEM_ERROR) - public Result> queryExternalSystemTasks(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser, - @RequestParam("externalSystemId") Integer externalSystemId) { + public Result> queryExternalSystemTasks(@RequestParam("externalSystemId") Integer externalSystemId) { List result = - externalSystemService.queryExternalSystemTasks(loginUser, externalSystemId); + externalSystemService.queryExternalSystemTasks(externalSystemId); return Result.success(result); } diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/ExternalSystemService.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/ExternalSystemService.java index ec60b64e467b..a7f571d7f6ec 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/ExternalSystemService.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/ExternalSystemService.java @@ -18,12 +18,11 @@ package org.apache.dolphinscheduler.api.service; import org.apache.dolphinscheduler.dao.entity.ExternalSystemTaskQuery; -import org.apache.dolphinscheduler.dao.entity.User; import java.util.List; public interface ExternalSystemService { - List queryExternalSystemTasks(User loginUser, int externalSystemId); + List queryExternalSystemTasks(int externalSystemId); } diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ExternalSystemServiceImpl.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ExternalSystemServiceImpl.java index 462931aa8fe6..6c130cf23fa3 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ExternalSystemServiceImpl.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/service/impl/ExternalSystemServiceImpl.java @@ -28,7 +28,6 @@ import org.apache.dolphinscheduler.common.utils.OkHttpUtils; import org.apache.dolphinscheduler.dao.entity.DataSource; import org.apache.dolphinscheduler.dao.entity.ExternalSystemTaskQuery; -import org.apache.dolphinscheduler.dao.entity.User; import org.apache.dolphinscheduler.dao.mapper.DataSourceMapper; import org.apache.dolphinscheduler.plugin.datasource.api.utils.PasswordUtils; import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.AuthenticationUtils; @@ -228,7 +227,7 @@ private String getHiddenPassword() { } @Override - public List queryExternalSystemTasks(User loginUser, int externalSystemId) { + public List queryExternalSystemTasks(int externalSystemId) { DataSource dataSource = dataSourceMapper.selectById(externalSystemId); ThirdPartySystemConnectorConnectionParam baseExternalSystemParam = diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/PollingInterfaceInfo.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/PollingInterfaceInfo.java index 6970981138bd..c83bcf9635c6 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/PollingInterfaceInfo.java +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/PollingInterfaceInfo.java @@ -18,8 +18,10 @@ package org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param; import lombok.Data; +import lombok.EqualsAndHashCode; @Data +@EqualsAndHashCode(callSuper = true) public class PollingInterfaceInfo extends InterfaceInfo { private PollingSuccessConfig pollingSuccessConfig; diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTask.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTask.java index a45485d3c0db..c42b72f6a888 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTask.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTask.java @@ -250,7 +250,8 @@ private String pollTaskStatus() throws TaskException { Object failureStatusObj = JsonPath.read(response.getBody(), failureField); log.info("PollTaskStatus successfully, external task instance success status: {}, failure status: {}", - successStatusObj.toString(), failureStatusObj.toString()); + successStatusObj != null ? successStatusObj.toString() : "null", + failureStatusObj != null ? failureStatusObj.toString() : "null"); if (successStatusObj != null) { return successStatusObj.toString(); @@ -426,7 +427,7 @@ private void parseSubmitResponse(List responseParameters, } parameterMap.put(key, value.toString().replace("\"", "")); - log.info("Parsed parameter {}: {}", key, value.toString()); + log.info("Parsed parameter {}: {}", key, value != null ? value.toString() : "null"); } } catch (Exception e) { diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/test/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTaskTest.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/test/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTaskTest.java new file mode 100644 index 000000000000..ee91296d8bdd --- /dev/null +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/test/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTaskTest.java @@ -0,0 +1,290 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.task.externalSystem; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; + +import org.apache.dolphinscheduler.common.model.OkHttpRequestHeaders; +import org.apache.dolphinscheduler.common.model.OkHttpResponse; +import org.apache.dolphinscheduler.common.utils.JSONUtils; +import org.apache.dolphinscheduler.common.utils.OkHttpUtils; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.AuthenticationUtils; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.AuthConfig; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.InterfaceInfo; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.PollingFailureConfig; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.PollingInterfaceInfo; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.PollingSuccessConfig; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.ResponseParameter; +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.ThirdPartySystemConnectorConnectionParam; +import org.apache.dolphinscheduler.plugin.task.api.TaskCallBack; +import org.apache.dolphinscheduler.plugin.task.api.TaskConstants; +import org.apache.dolphinscheduler.plugin.task.api.TaskExecutionContext; +import org.apache.dolphinscheduler.plugin.task.api.enums.TaskTimeoutStrategy; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class ExternalSystemTaskTest { + + private TaskExecutionContext taskExecutionContext; + private ExternalSystemParameters externalSystemParameters; + private ThirdPartySystemConnectorConnectionParam thirdPartyParams; + private ExternalSystemTask externalSystemTask; + + @BeforeEach + public void setUp() throws Exception { + taskExecutionContext = mock(TaskExecutionContext.class); + externalSystemParameters = new ExternalSystemParameters(); + externalSystemParameters.setDatasource(1); + externalSystemParameters.setExternalTaskId("task-123"); + externalSystemParameters.setExternalTaskName("Test Task"); + + thirdPartyParams = new ThirdPartySystemConnectorConnectionParam(); + AuthConfig authConfig = new AuthConfig(); + authConfig.setHeaderPrefix("Bearer"); + thirdPartyParams.setAuthConfig(authConfig); + + InterfaceInfo submitInterface = new InterfaceInfo(); + submitInterface.setUrl("http://test.com/api/tasks"); + submitInterface.setMethod(InterfaceInfo.HttpMethod.POST); + submitInterface.setBody("{\"taskName\":\"${externalTaskName}\"}"); + submitInterface.setParameters(Collections.emptyList()); + submitInterface.setResponseParameters(Arrays.asList( + createResponseParameter("taskId", "$.taskId"), + createResponseParameter("status", "$.status"))); + thirdPartyParams.setSubmitInterface(submitInterface); + + PollingInterfaceInfo pollInterface = new PollingInterfaceInfo(); + pollInterface.setUrl("http://test.com/api/tasks/${taskId}/status"); + pollInterface.setMethod(InterfaceInfo.HttpMethod.GET); + pollInterface.setParameters(Collections.emptyList()); + pollInterface.setResponseParameters(Collections.emptyList()); + + PollingSuccessConfig successConfig = new PollingSuccessConfig(); + successConfig.setSuccessField("$.status"); + successConfig.setSuccessValue("COMPLETED,SUCCESS"); + pollInterface.setPollingSuccessConfig(successConfig); + + PollingFailureConfig failureConfig = new PollingFailureConfig(); + failureConfig.setFailureField("$.status"); + failureConfig.setFailureValue("FAILED,ERROR"); + pollInterface.setPollingFailureConfig(failureConfig); + + thirdPartyParams.setPollStatusInterface(pollInterface); + + InterfaceInfo stopInterface = new InterfaceInfo(); + stopInterface.setUrl("http://test.com/api/tasks/${taskId}/stop"); + stopInterface.setMethod(InterfaceInfo.HttpMethod.POST); + stopInterface.setParameters(Collections.emptyList()); + thirdPartyParams.setStopInterface(stopInterface); + + String paramsJson = JSONUtils.toJsonString(externalSystemParameters); + Mockito.lenient().when(taskExecutionContext.getTaskParams()).thenReturn(paramsJson); + Mockito.lenient().when(taskExecutionContext.getTaskTimeout()).thenReturn(300000); + Mockito.lenient().when(taskExecutionContext.getTaskTimeoutStrategy()).thenReturn(TaskTimeoutStrategy.FAILED); + + org.apache.dolphinscheduler.plugin.task.api.parameters.resource.ResourceParametersHelper resourceHelper = + mock(org.apache.dolphinscheduler.plugin.task.api.parameters.resource.ResourceParametersHelper.class); + + org.apache.dolphinscheduler.plugin.task.api.parameters.resource.DataSourceParameters dataSourceParams = + mock(org.apache.dolphinscheduler.plugin.task.api.parameters.resource.DataSourceParameters.class); + Mockito.lenient().when(dataSourceParams.getConnectionParams()) + .thenReturn(JSONUtils.toJsonString(thirdPartyParams)); + + Mockito.lenient().when(resourceHelper.getResourceParameters( + org.apache.dolphinscheduler.plugin.task.api.enums.ResourceType.DATASOURCE, + externalSystemParameters.getDatasource())).thenReturn(dataSourceParams); + + Mockito.lenient().when(taskExecutionContext.getResourceParametersHelper()).thenReturn(resourceHelper); + + try (MockedStatic mockedAuth = Mockito.mockStatic(AuthenticationUtils.class)) { + mockedAuth + .when(() -> AuthenticationUtils + .authenticateAndGetToken(any(ThirdPartySystemConnectorConnectionParam.class))) + .thenReturn("mocked-token"); + + externalSystemTask = new ExternalSystemTask(taskExecutionContext); + + java.lang.reflect.Field parametersField = + ExternalSystemTask.class.getDeclaredField("externalSystemParameters"); + parametersField.setAccessible(true); + parametersField.set(externalSystemTask, externalSystemParameters); + + java.lang.reflect.Field baseParamsField = + ExternalSystemTask.class.getDeclaredField("baseExternalSystemParams"); + baseParamsField.setAccessible(true); + baseParamsField.set(externalSystemTask, thirdPartyParams); + + externalSystemTask.init(); + } + } + + private ResponseParameter createResponseParameter(String key, String jsonPath) { + ResponseParameter param = new ResponseParameter(); + param.setKey(key); + param.setJsonPath(jsonPath); + return param; + } + + @Test + public void testInit() { + try (MockedStatic mockedJsonUtils = Mockito.mockStatic(JSONUtils.class)) { + mockedJsonUtils.when(() -> JSONUtils.parseObject(any(String.class), eq(ExternalSystemParameters.class))) + .thenReturn(externalSystemParameters); + + externalSystemTask.init(); + + Assertions.assertNotNull(externalSystemTask.getParameters()); + } + } + + @Test + public void testSubmitExternalTask() throws Exception { + try (MockedStatic mockedOkHttp = Mockito.mockStatic(OkHttpUtils.class)) { + OkHttpResponse submitResponse = mock(OkHttpResponse.class); + when(submitResponse.getStatusCode()).thenReturn(200); + when(submitResponse.getBody()) + .thenReturn("{\"taskId\": \"12345\", \"status\": \"SUBMITTED\", \"name\": \"Test Task\"}"); + + OkHttpResponse runningResponse = mock(OkHttpResponse.class); + when(runningResponse.getStatusCode()).thenReturn(200); + when(runningResponse.getBody()).thenReturn("{\"status\": \"RUNNING\"}"); + + OkHttpResponse completedResponse = mock(OkHttpResponse.class); + when(completedResponse.getStatusCode()).thenReturn(200); + when(completedResponse.getBody()).thenReturn("{\"status\": \"COMPLETED\"}"); + + mockedOkHttp.when(() -> OkHttpUtils.post(any(String.class), any(OkHttpRequestHeaders.class), + any(Map.class), any(Map.class), anyInt(), anyInt(), anyInt())) + .thenReturn(submitResponse); + + mockedOkHttp.when(() -> OkHttpUtils.get(any(String.class), any(OkHttpRequestHeaders.class), + any(Map.class), anyInt(), anyInt(), anyInt())) + .thenReturn(runningResponse) + .thenReturn(runningResponse) + .thenReturn(completedResponse); + + TaskCallBack taskCallBack = mock(TaskCallBack.class); + + Assertions.assertDoesNotThrow(() -> { + externalSystemTask.handle(taskCallBack); + }); + + mockedOkHttp.verify(() -> OkHttpUtils.get(any(String.class), any(OkHttpRequestHeaders.class), + any(Map.class), anyInt(), anyInt(), anyInt()), times(3)); + } + } + + @Test + public void testSubmitExternalTaskWithSuccessStatus() throws Exception { + try (MockedStatic mockedOkHttp = Mockito.mockStatic(OkHttpUtils.class)) { + OkHttpResponse submitResponse = mock(OkHttpResponse.class); + when(submitResponse.getStatusCode()).thenReturn(200); + when(submitResponse.getBody()).thenReturn( + "{\"taskId\": \"12345\", \"taskInstanceId\": \"instance-123\", \"status\": \"SUBMITTED\", \"name\": \"Test Task\"}"); + + OkHttpResponse runningResponse1 = mock(OkHttpResponse.class); + when(runningResponse1.getStatusCode()).thenReturn(200); + when(runningResponse1.getBody()).thenReturn("{\"status\": \"RUNNING\"}"); + + OkHttpResponse runningResponse2 = mock(OkHttpResponse.class); + when(runningResponse2.getStatusCode()).thenReturn(200); + when(runningResponse2.getBody()).thenReturn("{\"status\": \"RUNNING\"}"); + + OkHttpResponse successResponse = mock(OkHttpResponse.class); + when(successResponse.getStatusCode()).thenReturn(200); + when(successResponse.getBody()).thenReturn("{\"status\": \"SUCCESS\"}"); + + mockedOkHttp.when(() -> OkHttpUtils.post(any(String.class), any(OkHttpRequestHeaders.class), + any(Map.class), any(Map.class), anyInt(), anyInt(), anyInt())) + .thenReturn(submitResponse); + + mockedOkHttp.when(() -> OkHttpUtils.get(any(String.class), any(OkHttpRequestHeaders.class), + any(Map.class), anyInt(), anyInt(), anyInt())) + .thenReturn(runningResponse1) + .thenReturn(runningResponse2) + .thenReturn(successResponse); + + TaskCallBack taskCallBack = mock(TaskCallBack.class); + + Assertions.assertDoesNotThrow(() -> { + externalSystemTask.handle(taskCallBack); + }); + + mockedOkHttp.verify(() -> OkHttpUtils.get(any(String.class), any(OkHttpRequestHeaders.class), + any(Map.class), anyInt(), anyInt(), anyInt()), times(3)); + + Assertions.assertEquals(TaskConstants.EXIT_CODE_SUCCESS, externalSystemTask.getExitStatusCode()); + } + } + + @Test + public void testTimeoutCheckEnabled() { + try { + java.lang.reflect.Method method = ExternalSystemTask.class.getDeclaredMethod("isTimeoutFailureEnabled"); + method.setAccessible(true); + boolean result = (boolean) method.invoke(externalSystemTask); + Assertions.assertTrue(result); + } catch (Exception e) { + Assertions.fail("Failed to invoke isTimeoutFailureEnabled method: " + e.getMessage()); + } + } + + @Test + public void testReplaceParameterPlaceholders() { + Map parameterMap = new HashMap<>(); + parameterMap.put("externalTaskId", "task-123"); + parameterMap.put("externalTaskName", "Test Task"); + + try { + java.lang.reflect.Field field = ExternalSystemTask.class.getDeclaredField("parameterMap"); + field.setAccessible(true); + field.set(externalSystemTask, parameterMap); + } catch (Exception e) { + Assertions.fail("Failed to set parameterMap field"); + } + + String template = "http://test.com/api/tasks/${externalTaskId}/${externalTaskName}"; + try { + java.lang.reflect.Method method = + ExternalSystemTask.class.getDeclaredMethod("replaceParameterPlaceholders", String.class); + method.setAccessible(true); + String result = (String) method.invoke(externalSystemTask, template); + + Assertions.assertEquals("http://test.com/api/tasks/task-123/Test Task", result); + } catch (Exception e) { + Assertions.fail("Failed to invoke replaceParameterPlaceholders method: " + e.getMessage()); + } + } +} diff --git a/dolphinscheduler-ui/src/service/modules/data-source/index.ts b/dolphinscheduler-ui/src/service/modules/data-source/index.ts index 426e74bd161c..dfa805b25ac2 100644 --- a/dolphinscheduler-ui/src/service/modules/data-source/index.ts +++ b/dolphinscheduler-ui/src/service/modules/data-source/index.ts @@ -168,13 +168,6 @@ export function getDatasourceTableColumnsById( }) } -export function queryExternalSystemList(): any { - return axios({ - url: '/external-systems/queryExternalSystemList', - method: 'get' - }) -} - export function queryExternalSystemTasks(externalSystemId: number): any { return axios({ url: '/external-systems/queryExternalSystemTasks', From c735e14847bb733892495bbe63f32f7487dafea3 Mon Sep 17 00:00:00 2001 From: yud8 Date: Wed, 28 Jan 2026 10:09:25 +0800 Subject: [PATCH 11/11] [Feature-17501]Third-party System APl Connector fix sonarqubecloud --- .../externalSystem/ExternalSystemTask.java | 5 ++- .../src/views/datasource/list/detail.tsx | 38 +++++++++---------- 2 files changed, 22 insertions(+), 21 deletions(-) diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTask.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTask.java index c42b72f6a888..8f1f0a830bda 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTask.java +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTask.java @@ -112,9 +112,10 @@ public void handle(TaskCallBack taskCallBack) throws TaskException { submitExternalTask(); TimeUnit.SECONDS.sleep(10); trackExternalTaskStatus(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new TaskException("Task interrupted", e); } catch (Exception e) { - log.error("external system task error", e); - setExitStatusCode(TaskConstants.EXIT_CODE_FAILURE); throw new TaskException("Execute external system task failed", e); } } diff --git a/dolphinscheduler-ui/src/views/datasource/list/detail.tsx b/dolphinscheduler-ui/src/views/datasource/list/detail.tsx index 63ae865e9a83..acfb94c86ea0 100644 --- a/dolphinscheduler-ui/src/views/datasource/list/detail.tsx +++ b/dolphinscheduler-ui/src/views/datasource/list/detail.tsx @@ -115,26 +115,26 @@ const DetailModal = defineComponent({ )) props.show && props.id && setFieldsValue(await queryById(props.id)) } - ), - // Monitor authType change, update headerPrefix - watch( - () => state.detailForm.authConfig?.authType, - (newAuthType) => { - if ( - state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && - state.detailForm.authConfig - ) { - if (newAuthType === 'BASIC_AUTH') { - state.detailForm.authConfig.headerPrefix = 'Basic' - } else if (newAuthType === 'JWT' || newAuthType === 'OAUTH2') { - state.detailForm.authConfig.headerPrefix = 'Bearer' - } else { - state.detailForm.authConfig.headerPrefix = '' - } + ) + // Monitor authType change, update headerPrefix + watch( + () => state.detailForm.authConfig?.authType, + (newAuthType) => { + if ( + state.detailForm.type === 'THIRDPARTY_SYSTEM_CONNECTOR' && + state.detailForm.authConfig + ) { + if (newAuthType === 'BASIC_AUTH') { + state.detailForm.authConfig.headerPrefix = 'Basic' + } else if (newAuthType === 'JWT' || newAuthType === 'OAUTH2') { + state.detailForm.authConfig.headerPrefix = 'Bearer' + } else { + state.detailForm.authConfig.headerPrefix = '' } - }, - { immediate: true } - ) + } + }, + { immediate: true } + ) watch( () => props.selectType,

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 z^1g)zD}XkqCazure-resourcemanager-datafactory + + com.jayway.jsonpath + json-path + + com.azure azure-identity diff --git a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/enums/Status.java b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/enums/Status.java index daf7026e782e..a22b7c439fe6 100644 --- a/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/enums/Status.java +++ b/dolphinscheduler-api/src/main/java/org/apache/dolphinscheduler/api/enums/Status.java @@ -73,6 +73,40 @@ public enum Status { VERIFY_DATASOURCE_NAME_FAILURE(10039, "verify datasource name failure", "验证数据源名称失败"), UNAUTHORIZED_DATASOURCE(10040, "unauthorized datasource", "未经授权的数据源"), AUTHORIZED_DATA_SOURCE(10041, "authorized data source", "授权数据源失败"), + CREATE_EXTERNAL_SYSTEM_ERROR(11042, "create external system error", "创建第三方系统错误"), + UPDATE_EXTERNAL_SYSTEM_ERROR(11043, "update external system error", "更新第三方系统错误"), + QUERY_EXTERNAL_SYSTEM_ERROR(11044, "query external system error", "查询第三方系统错误"), + DELETE_EXTERNAL_SYSTEM_ERROR(11045, "delete external system error", "删除第三方系统错误"), + TEST_EXTERNAL_SYSTEM_CONNECTION_ERROR(11046, "connect external system failure", "建立第三方系统连接失败"), + EXTERNAL_SYSTEM_NOT_EXIST(11047, "external system not exist", "第三方系统不存在"), + EXTERNAL_SYSTEM_NAME_EXIST(11048, "external name exist", "第三方系统名称已存在"), + EXTERNAL_SYSTEM_CONNECT_AUTH_FAILED(11049, "external connect failed", "第三方系统连接失败,检查认证信息"), + EXTERNAL_SYSTEM_CONNECT_SELECT_FAILED(11049, "external connect failed", "第三方系统连接失败,检查查询任务列表接口信息"), + UNAUTHORIZED_EXTERNAL_SYSTEM(10050, "unauthorized datasource", "未经授权的第三方系统"), + AUTHORIZED_EXTERNAL_SYSTEM(10051, "authorized data source", "授权第三方系统失败"), + EXTERNAL_SYSTEM_NAME_EMPTY(11052, "external system name is empty", "第三方系统名称为空"), + EXTERNAL_SYSTEM_SERVICE_ADDRESS_EMPTY(11053, "external system service address is empty", "第三方系统服务地址为空"), + EXTERNAL_SYSTEM_AUTH_CONFIG_EMPTY(11054, "external system auth config is empty", "第三方系统认证配置为空"), + EXTERNAL_SYSTEM_AUTH_CONFIG_TYPE_EMPTY(11055, "external system auth config type is empty", "第三方系统认证类型为空"), + EXTERNAL_SYSTEM_BASIC_USERNAME_EMPTY(11056, "external system basic username is empty", "第三方系统基础认证用户名为空"), + EXTERNAL_SYSTEM_BASIC_PASSWORD_EMPTY(11057, "external system basic password is empty", "第三方系统基础认证密码为空"), + EXTERNAL_SYSTEM_JWT_TOKEN_EMPTY(11058, "external system jwt token is empty", "第三方系统JWT令牌为空"), + EXTERNAL_SYSTEM_OAUTH2_TOKEN_URL_EMPTY(11059, "external system oauth2 token url is empty", "第三方系统OAuth2令牌URL为空"), + EXTERNAL_SYSTEM_OAUTH2_CLIENT_ID_EMPTY(11060, "external system oauth2 client id is empty", "第三方系统OAuth2客户端ID为空"), + EXTERNAL_SYSTEM_OAUTH2_CLIENT_SECRET_EMPTY(11061, "external system oauth2 client secret is empty", + "第三方系统OAuth2客户端密钥为空"), + EXTERNAL_SYSTEM_OAUTH2_GRANT_TYPE_EMPTY(11062, "external system oauth2 grant type is empty", "第三方系统OAuth2授权类型为空"), + EXTERNAL_SYSTEM_OAUTH2_USERNAME_EMPTY(11063, "external system oauth2 username is empty", "第三方系统OAuth2用户名为空"), + EXTERNAL_SYSTEM_OAUTH2_PASSWORD_EMPTY(11064, "external system oauth2 password is empty", "第三方系统OAuth2密码为空"), + EXTERNAL_SYSTEM_AUTH_TYPE_UNSUPPORTED(11065, "external system auth type is unsupported", "第三方系统认证类型不支持"), + EXTERNAL_SYSTEM_SELECT_INTERFACE_EMPTY(11066, "external system select interface is empty", "第三方系统查询接口配置为空"), + EXTERNAL_SYSTEM_SUBMIT_INTERFACE_EMPTY(11067, "external system submit interface is empty", "第三方系统提交接口配置为空"), + EXTERNAL_SYSTEM_POLL_STATUS_INTERFACE_EMPTY(11068, "external system poll status interface is empty", + "第三方系统轮询状态接口配置为空"), + EXTERNAL_SYSTEM_STOP_INTERFACE_EMPTY(11069, "external system stop interface is empty", "第三方系统停止接口配置为空"), + EXTERNAL_SYSTEM_INTERFACE_URL_EMPTY(11070, "external system interface url is empty", "第三方系统接口URL为空"), + EXTERNAL_SYSTEM_INTERFACE_METHOD_EMPTY(11071, "external system interface method is empty", "第三方系统接口方法为空"), + EXTERNAL_SYSTEM_NAME_TOO_LONG(11071, "external system name too long", "第三方系统接口名称太长"), LOGIN_SUCCESS(10042, "login success", "登录成功"), USER_LOGIN_FAILURE(10043, "user login failure", "用户登录失败"), LIST_WORKERS_ERROR(10044, "list workers error", "查询worker列表错误"), @@ -120,6 +154,7 @@ public enum Status { GRANT_PROJECT_ERROR(10094, "grant project error", "授权项目错误"), GRANT_RESOURCE_ERROR(10095, "grant resource error", "授权资源错误"), GRANT_DATASOURCE_ERROR(10097, "grant datasource error", "授权数据源错误"), + GRANT_EXTERNALSYSTEM_ERROR(100971, "grant external system error", "授权第三方系统错误"), GET_USER_INFO_ERROR(10098, "get user info error", "获取用户信息错误"), USER_LIST_ERROR(10099, "user list error", "查询用户列表错误"), VERIFY_USERNAME_ERROR(10100, "verify username error", "用户名验证错误"), diff --git a/dolphinscheduler-api/src/main/resources/task-type-config.yaml b/dolphinscheduler-api/src/main/resources/task-type-config.yaml index 2a31023e4c77..328c43db93f7 100644 --- a/dolphinscheduler-api/src/main/resources/task-type-config.yaml +++ b/dolphinscheduler-api/src/main/resources/task-type-config.yaml @@ -31,6 +31,7 @@ task: - 'FLINK_STREAM' - 'HIVECLI' - 'REMOTESHELL' + - 'EXTERNAL_SYSTEM' cloud: - 'EMR' - 'K8S' diff --git a/dolphinscheduler-bom/pom.xml b/dolphinscheduler-bom/pom.xml index 9173d3efe180..45f178957313 100644 --- a/dolphinscheduler-bom/pom.xml +++ b/dolphinscheduler-bom/pom.xml @@ -124,6 +124,7 @@ 0.10.1 2.1.4 0.3.2 + 0.11.5 2.0.1 3.17.3 1.3 @@ -1038,6 +1039,23 @@ jdbc ${dolphindb-jdbc.version} + + + io.jsonwebtoken + jjwt-api + ${io-jsonwebtoken.version} + + + io.jsonwebtoken + jjwt-impl + ${io-jsonwebtoken.version} + + + io.jsonwebtoken + jjwt-jackson + ${io-jsonwebtoken.version} + + diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java index 03742f875f77..74f2653d78cd 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/constants/Constants.java @@ -212,6 +212,11 @@ public final class Constants { public static final String DEFAULT = "default"; public static final String PASSWORD = "password"; + public static final String BASICPASSWORD = "basicPassword"; + public static final String OAUTH2CLIENTSECRET = "oauth2ClientSecret"; + public static final String JWTTOKEN = "jwtToken"; + public static final String OAUTH2PASSWORD = "oauth2Password"; + public static final String XXXXXX = "******"; public static final String NULL = "NULL"; public static final String THREAD_NAME_MASTER_SERVER = "Master-Server"; diff --git a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/OkHttpUtils.java b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/OkHttpUtils.java index d942901974b2..6d5c446b9b79 100644 --- a/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/OkHttpUtils.java +++ b/dolphinscheduler-common/src/main/java/org/apache/dolphinscheduler/common/utils/OkHttpUtils.java @@ -99,6 +99,27 @@ public class OkHttpUtils { } } + public static @NonNull OkHttpResponse postFormBody(@NonNull String url, + @Nullable OkHttpRequestHeaders okHttpRequestHeaders, + @Nullable Map requestParamsMap, + @Nullable RequestBody formBody, + int connectTimeout, + int writeTimeout, + int readTimeout) throws IOException { + OkHttpClient client = getHttpClient(connectTimeout, writeTimeout, readTimeout); + String finalUrl = addUrlParams(requestParamsMap, url); + Request.Builder requestBuilder = new Request.Builder().url(finalUrl); + addHeader(okHttpRequestHeaders.getHeaders(), requestBuilder); + Request request = requestBuilder + .post(formBody) // 明确使用POST方法 + .build(); + try (Response response = client.newCall(request).execute()) { + return new OkHttpResponse(response.code(), getResponseBody(response)); + } catch (Exception e) { + throw new RuntimeException(String.format("Post request execute failed, url: %s", url), e); + } + } + /** * http put request * @param connectTimeout connect timeout in milliseconds diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-all/pom.xml b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-all/pom.xml index 7da30a85affb..fd31e93c3246 100644 --- a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-all/pom.xml +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-all/pom.xml @@ -168,5 +168,10 @@ dolphinscheduler-datasource-dolphindb ${project.version} + + org.apache.dolphinscheduler + dolphinscheduler-datasource-thirdpartysystemconnector + ${project.version} + diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/pom.xml b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/pom.xml new file mode 100644 index 000000000000..c93f7775b6ac --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/pom.xml @@ -0,0 +1,71 @@ + + + + 4.0.0 + + org.apache.dolphinscheduler + dolphinscheduler-datasource-plugin + dev-SNAPSHOT + + + dolphinscheduler-datasource-thirdpartysystemconnector + jar + ${project.artifactId} + + + thirdpartysystemconnector + + + + + org.apache.dolphinscheduler + dolphinscheduler-datasource-api + provided + + + + org.apache.dolphinscheduler + dolphinscheduler-common + provided + + + + org.apache.dolphinscheduler + dolphinscheduler-spi + provided + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + + shade + + package + + + + + + diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/ThirdPartySystemConnectorAdHocDataSourceClient.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/ThirdPartySystemConnectorAdHocDataSourceClient.java new file mode 100644 index 000000000000..77e07bfb03f9 --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/ThirdPartySystemConnectorAdHocDataSourceClient.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector; + +import org.apache.dolphinscheduler.plugin.datasource.api.client.BaseAdHocDataSourceClient; +import org.apache.dolphinscheduler.spi.datasource.BaseConnectionParam; +import org.apache.dolphinscheduler.spi.enums.DbType; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ThirdPartySystemConnectorAdHocDataSourceClient extends BaseAdHocDataSourceClient { + + public ThirdPartySystemConnectorAdHocDataSourceClient(BaseConnectionParam baseConnectionParam, DbType dbType) { + super(baseConnectionParam, dbType); + } +} diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/ThirdPartySystemConnectorClientWrapper.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/ThirdPartySystemConnectorClientWrapper.java new file mode 100644 index 000000000000..7e5a68103116 --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/ThirdPartySystemConnectorClientWrapper.java @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector; + +import org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param.AuthConfig; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ThirdPartySystemConnectorClientWrapper implements AutoCloseable { + + private final String serviceAddress; + private final AuthConfig authConfig; + + public ThirdPartySystemConnectorClientWrapper(String serviceAddress, AuthConfig authConfig) { + this.serviceAddress = serviceAddress; + this.authConfig = authConfig; + } + + public boolean checkConnect() { + log.info("Checking connectivity to third party system at: {}", serviceAddress); + return true; + } + + @Override + public void close() { + log.info("Closing third party system connector client"); + } +} diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/ThirdPartySystemConnectorDataSourceChannel.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/ThirdPartySystemConnectorDataSourceChannel.java new file mode 100644 index 000000000000..a7089c2f23dd --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/ThirdPartySystemConnectorDataSourceChannel.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector; + +import org.apache.dolphinscheduler.spi.datasource.AdHocDataSourceClient; +import org.apache.dolphinscheduler.spi.datasource.BaseConnectionParam; +import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; +import org.apache.dolphinscheduler.spi.datasource.PooledDataSourceClient; +import org.apache.dolphinscheduler.spi.enums.DbType; + +public class ThirdPartySystemConnectorDataSourceChannel implements DataSourceChannel { + + @Override + public AdHocDataSourceClient createAdHocDataSourceClient(BaseConnectionParam baseConnectionParam, DbType dbType) { + throw new UnsupportedOperationException("ThirdPartySystemConnector AdHocDataSourceClient is not supported"); + } + + @Override + public PooledDataSourceClient createPooledDataSourceClient(BaseConnectionParam baseConnectionParam, DbType dbType) { + throw new UnsupportedOperationException("ThirdPartySystemConnector PooledDataSourceClient is not supported"); + } +} diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/ThirdPartySystemConnectorDataSourceChannelFactory.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/ThirdPartySystemConnectorDataSourceChannelFactory.java new file mode 100644 index 000000000000..9eba1357ba66 --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/ThirdPartySystemConnectorDataSourceChannelFactory.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector; + +import org.apache.dolphinscheduler.spi.datasource.DataSourceChannel; +import org.apache.dolphinscheduler.spi.datasource.DataSourceChannelFactory; +import org.apache.dolphinscheduler.spi.enums.DbType; + +import com.google.auto.service.AutoService; + +@AutoService(DataSourceChannelFactory.class) +public class ThirdPartySystemConnectorDataSourceChannelFactory implements DataSourceChannelFactory { + + @Override + public String getName() { + return DbType.THIRDPARTY_SYSTEM_CONNECTOR.getName(); + } + + @Override + public DataSourceChannel create() { + return new ThirdPartySystemConnectorDataSourceChannel(); + } + +} diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/ThirdPartySystemConnectorPooledDataSourceClient.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/ThirdPartySystemConnectorPooledDataSourceClient.java new file mode 100644 index 000000000000..8d473a7f7e7d --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/ThirdPartySystemConnectorPooledDataSourceClient.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector; + +import org.apache.dolphinscheduler.plugin.datasource.api.client.BasePooledDataSourceClient; +import org.apache.dolphinscheduler.spi.datasource.BaseConnectionParam; +import org.apache.dolphinscheduler.spi.enums.DbType; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ThirdPartySystemConnectorPooledDataSourceClient extends BasePooledDataSourceClient { + + public ThirdPartySystemConnectorPooledDataSourceClient(BaseConnectionParam baseConnectionParam, DbType dbType) { + super(baseConnectionParam, dbType); + } +} diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/AuthConfig.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/AuthConfig.java new file mode 100644 index 000000000000..d8d4794a4a28 --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/AuthConfig.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param; + +import java.util.List; + +import lombok.Data; + +@Data +public class AuthConfig { + + private String authType; // authType:BASIC, JWT, OAUTH2 + private String headerPrefix; // headerPrefix:BASIC, JWT, OAUTH2 + + // === (Basic Auth) === + private String basicUsername; + private String basicPassword; + + // === JWT === + private String jwtToken; + + // === OAuth2 === + private String oauth2TokenUrl; + private String oauth2ClientId; + private String oauth2ClientSecret; + private String oauth2GrantType; // e.g., "client_credentials", "password" + private String oauth2Username; // only password mode + private String oauth2Password; // only password mode + + // === other authMappings === + private List authMappings; +} diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/AuthMapping.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/AuthMapping.java new file mode 100644 index 000000000000..c8c5a3a49a6e --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/AuthMapping.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param; + +import lombok.Data; + +@Data +public class AuthMapping { + + private String key; + private String value; +} diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/InterfaceInfo.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/InterfaceInfo.java new file mode 100644 index 000000000000..0eb6bdb14528 --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/InterfaceInfo.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param; + +import java.util.List; + +import lombok.Data; + +@Data +public class InterfaceInfo { + + private String url; + private String method; + private String body; + private List parameters; + private List responseParameters; +} diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/PollingFailureConfig.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/PollingFailureConfig.java new file mode 100644 index 000000000000..ad7016ae888b --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/PollingFailureConfig.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param; + +import lombok.Data; + +@Data +public class PollingFailureConfig { + + private String failureField; + private String failureValue; +} diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/PollingInterfaceInfo.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/PollingInterfaceInfo.java new file mode 100644 index 000000000000..6970981138bd --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/PollingInterfaceInfo.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param; + +import lombok.Data; + +@Data +public class PollingInterfaceInfo extends InterfaceInfo { + + private PollingSuccessConfig pollingSuccessConfig; + private PollingFailureConfig pollingFailureConfig; +} diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/PollingSuccessConfig.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/PollingSuccessConfig.java new file mode 100644 index 000000000000..11f7874785b3 --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/PollingSuccessConfig.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param; + +import lombok.Data; + +@Data +public class PollingSuccessConfig { + + private String successField; + private String successValue; +} diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/RequestParameter.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/RequestParameter.java new file mode 100644 index 000000000000..9b931c3bc1fe --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/RequestParameter.java @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param; + +import lombok.Data; + +@Data +public class RequestParameter { + + private String paramName; + private String paramValue; + private String location; // header,param,body +} diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ResponseParameter.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ResponseParameter.java new file mode 100644 index 000000000000..939cda72b973 --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ResponseParameter.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param; + +import lombok.Data; + +@Data +public class ResponseParameter { + + private String key; + private String jsonPath; +} diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorConnectionParam.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorConnectionParam.java new file mode 100644 index 000000000000..e71173a025b8 --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorConnectionParam.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param; + +import org.apache.dolphinscheduler.spi.datasource.ConnectionParam; + +import lombok.Data; + +import com.fasterxml.jackson.annotation.JsonInclude; + +@Data +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ThirdPartySystemConnectorConnectionParam implements ConnectionParam { + + private String systemName; + + private String serviceAddress; + + private AuthConfig authConfig; + + private InterfaceInfo selectInterface; + private InterfaceInfo submitInterface; + private PollingInterfaceInfo pollStatusInterface; + private InterfaceInfo stopInterface; + + private int interfaceTimeout = 120000; +} diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorDataSourceParamDTO.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorDataSourceParamDTO.java new file mode 100644 index 000000000000..de53919ae19e --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorDataSourceParamDTO.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param; + +import org.apache.dolphinscheduler.plugin.datasource.api.datasource.BaseDataSourceParamDTO; +import org.apache.dolphinscheduler.spi.enums.DbType; + +import lombok.Data; +import lombok.EqualsAndHashCode; + +@Data +@EqualsAndHashCode(callSuper = true) +public class ThirdPartySystemConnectorDataSourceParamDTO extends BaseDataSourceParamDTO { + + private String systemName; + + private String serviceAddress; + + private AuthConfig authConfig; + + private InterfaceInfo selectInterface; + private InterfaceInfo submitInterface; + private PollingInterfaceInfo pollStatusInterface; + private InterfaceInfo stopInterface; + + private int interfaceTimeout = 120000; + + @Override + public DbType getType() { + return DbType.THIRDPARTY_SYSTEM_CONNECTOR; + } +} diff --git a/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorDataSourceProcessor.java b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorDataSourceProcessor.java new file mode 100644 index 000000000000..0d53de1f2e40 --- /dev/null +++ b/dolphinscheduler-datasource-plugin/dolphinscheduler-datasource-thirdpartysystemconnector/src/main/java/org/apache/dolphinscheduler/plugin/datasource/thirdpartysystemconnector/param/ThirdPartySystemConnectorDataSourceProcessor.java @@ -0,0 +1,301 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.datasource.thirdpartysystemconnector.param; + +import static org.apache.dolphinscheduler.common.constants.Constants.HTTP_CONNECT_TIMEOUT; + +import org.apache.dolphinscheduler.common.model.OkHttpRequestHeaderContentType; +import org.apache.dolphinscheduler.common.model.OkHttpRequestHeaders; +import org.apache.dolphinscheduler.common.model.OkHttpResponse; +import org.apache.dolphinscheduler.common.utils.JSONUtils; +import org.apache.dolphinscheduler.common.utils.OkHttpUtils; +import org.apache.dolphinscheduler.plugin.datasource.api.datasource.AbstractDataSourceProcessor; +import org.apache.dolphinscheduler.plugin.datasource.api.datasource.BaseDataSourceParamDTO; +import org.apache.dolphinscheduler.plugin.datasource.api.datasource.DataSourceProcessor; +import org.apache.dolphinscheduler.spi.datasource.ConnectionParam; +import org.apache.dolphinscheduler.spi.enums.DbType; + +import org.apache.commons.lang3.StringUtils; + +import java.sql.Connection; +import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; + +import lombok.extern.slf4j.Slf4j; +import okhttp3.FormBody; +import okhttp3.RequestBody; + +import com.fasterxml.jackson.databind.JsonNode; +import com.google.auto.service.AutoService; + +@AutoService(DataSourceProcessor.class) +@Slf4j +public class ThirdPartySystemConnectorDataSourceProcessor extends AbstractDataSourceProcessor { + + @Override + public BaseDataSourceParamDTO castDatasourceParamDTO(String paramJson) { + return JSONUtils.parseObject(paramJson, ThirdPartySystemConnectorDataSourceParamDTO.class); + } + + @Override + public void checkDatasourceParam(BaseDataSourceParamDTO datasourceParamDTO) { + ThirdPartySystemConnectorDataSourceParamDTO thirdPartySystemConnectorParamDTO = + (ThirdPartySystemConnectorDataSourceParamDTO) datasourceParamDTO; + + if (StringUtils.isEmpty(thirdPartySystemConnectorParamDTO.getServiceAddress())) { + throw new IllegalArgumentException("third party system connector datasource param is not valid"); + } + } + + @Override + public String getDatasourceUniqueId(ConnectionParam connectionParam, DbType dbType) { + ThirdPartySystemConnectorConnectionParam baseConnectionParam = + (ThirdPartySystemConnectorConnectionParam) connectionParam; + return MessageFormat.format("{0}@{1}", dbType.getName(), baseConnectionParam.getServiceAddress()); + } + + @Override + public BaseDataSourceParamDTO createDatasourceParamDTO(String connectionJson) { + ThirdPartySystemConnectorConnectionParam connectionParams = + (ThirdPartySystemConnectorConnectionParam) createConnectionParams(connectionJson); + + ThirdPartySystemConnectorDataSourceParamDTO thirdPartySystemConnectorDataSourceParamDTO = + new ThirdPartySystemConnectorDataSourceParamDTO(); + + thirdPartySystemConnectorDataSourceParamDTO.setSystemName(connectionParams.getSystemName()); + thirdPartySystemConnectorDataSourceParamDTO.setServiceAddress(connectionParams.getServiceAddress()); + thirdPartySystemConnectorDataSourceParamDTO.setAuthConfig(connectionParams.getAuthConfig()); + thirdPartySystemConnectorDataSourceParamDTO.setSelectInterface(connectionParams.getSelectInterface()); + thirdPartySystemConnectorDataSourceParamDTO.setSubmitInterface(connectionParams.getSubmitInterface()); + thirdPartySystemConnectorDataSourceParamDTO.setPollStatusInterface(connectionParams.getPollStatusInterface()); + thirdPartySystemConnectorDataSourceParamDTO.setStopInterface(connectionParams.getStopInterface()); + thirdPartySystemConnectorDataSourceParamDTO.setInterfaceTimeout(connectionParams.getInterfaceTimeout()); + + return thirdPartySystemConnectorDataSourceParamDTO; + } + + @Override + public ThirdPartySystemConnectorConnectionParam createConnectionParams(BaseDataSourceParamDTO datasourceParam) { + ThirdPartySystemConnectorDataSourceParamDTO thirdPartySystemConnectorDataSourceParamDTO = + (ThirdPartySystemConnectorDataSourceParamDTO) datasourceParam; + + ThirdPartySystemConnectorConnectionParam thirdPartySystemConnectorConnectionParam = + new ThirdPartySystemConnectorConnectionParam(); + + thirdPartySystemConnectorConnectionParam.setSystemName( + thirdPartySystemConnectorDataSourceParamDTO.getSystemName()); + thirdPartySystemConnectorConnectionParam.setServiceAddress( + thirdPartySystemConnectorDataSourceParamDTO.getServiceAddress()); + thirdPartySystemConnectorConnectionParam.setAuthConfig( + thirdPartySystemConnectorDataSourceParamDTO.getAuthConfig()); + thirdPartySystemConnectorConnectionParam.setSelectInterface( + thirdPartySystemConnectorDataSourceParamDTO.getSelectInterface()); + thirdPartySystemConnectorConnectionParam.setSubmitInterface( + thirdPartySystemConnectorDataSourceParamDTO.getSubmitInterface()); + thirdPartySystemConnectorConnectionParam.setPollStatusInterface( + thirdPartySystemConnectorDataSourceParamDTO.getPollStatusInterface()); + thirdPartySystemConnectorConnectionParam.setStopInterface( + thirdPartySystemConnectorDataSourceParamDTO.getStopInterface()); + thirdPartySystemConnectorConnectionParam.setInterfaceTimeout( + thirdPartySystemConnectorDataSourceParamDTO.getInterfaceTimeout()); + + return thirdPartySystemConnectorConnectionParam; + } + + @Override + public ConnectionParam createConnectionParams(String connectionJson) { + return JSONUtils.parseObject(connectionJson, ThirdPartySystemConnectorConnectionParam.class); + } + + @Override + public String getDatasourceDriver() { + return ""; + } + + @Override + public String getValidationQuery() { + return ""; + } + + @Override + public String getJdbcUrl(ConnectionParam connectionParam) { + return ""; + } + + @Override + public Connection getConnection(ConnectionParam connectionParam) { + return null; + } + + @Override + public boolean checkDataSourceConnectivity(ConnectionParam connectionParam) { + ThirdPartySystemConnectorConnectionParam baseConnectionParam = + (ThirdPartySystemConnectorConnectionParam) connectionParam; + + try { + OkHttpResponse response = callSelectInterface(baseConnectionParam); + if (response.getStatusCode() == 200) { + return true; + } + return false; + } catch (Exception e) { + log.error("connect error, e:{}", e.getMessage()); + return false; + } + } + + private OkHttpResponse callSelectInterface(ThirdPartySystemConnectorConnectionParam baseConnectionParam) { + try { + InterfaceInfo selectConfig = baseConnectionParam.getSelectInterface(); + + // 替换参数占位符 + String url = selectConfig.getUrl(); + + OkHttpRequestHeaders headers = new OkHttpRequestHeaders(); + headers.setOkHttpRequestHeaderContentType(OkHttpRequestHeaderContentType.APPLICATION_JSON); + + Map headerMap = new HashMap<>(); + Map requestBody = new HashMap<>(); + Map requestParams = new HashMap<>(); + + // 获取认证token + String token = authenticateAndGetToken(baseConnectionParam); + + headerMap.put("Authorization", token); + + // 处理参数 + if (selectConfig.getParameters() != null) { + for (RequestParameter param : selectConfig.getParameters()) { + String value = param.getParamValue(); + + switch (param.getLocation()) { + case "HEADER": + headerMap.put(param.getParamName(), value); + break; + case "BODY": + if ("body".equals(param.getParamName())) { + requestBody = JSONUtils.parseObject(value, Map.class); + } + break; + case "PARAM": + requestParams.put(param.getParamName(), value); + break; + } + } + } + + if (!headerMap.isEmpty()) { + headers.setHeaders(headerMap); + } + + OkHttpResponse response; + if ("POST".equals(selectConfig.getMethod())) { + response = OkHttpUtils.post(url, headers, requestParams, requestBody, + HTTP_CONNECT_TIMEOUT, HTTP_CONNECT_TIMEOUT, HTTP_CONNECT_TIMEOUT); + } else if ("PUT".equals(selectConfig.getMethod())) { + response = OkHttpUtils.put(url, headers, requestBody, + HTTP_CONNECT_TIMEOUT, HTTP_CONNECT_TIMEOUT, HTTP_CONNECT_TIMEOUT); + } else { + response = OkHttpUtils.get(url, headers, requestParams, + HTTP_CONNECT_TIMEOUT, HTTP_CONNECT_TIMEOUT, HTTP_CONNECT_TIMEOUT); + } + return response; + + } catch (Exception e) { + log.error("select task failed", e); + throw new RuntimeException("select task failed", e); + } + } + + private String authenticateAndGetToken(ThirdPartySystemConnectorConnectionParam baseConnectionParam) throws Exception { + AuthConfig authConfig = baseConnectionParam.getAuthConfig(); + if (authConfig == null) { + throw new RuntimeException("AuthConfig is not provided"); + } + + switch (authConfig.getAuthType()) { + case "BASIC": + // 基础认证 + String auth = authConfig.getBasicUsername() + ":" + authConfig.getBasicPassword(); + String encoding = java.util.Base64.getEncoder().encodeToString(auth.getBytes()); + return encoding; + case "JWT": + // JWT认证 + return authConfig.getJwtToken(); + case "OAUTH2": + // OAuth2认证 + return getOAuth2Token(baseConnectionParam); + default: + throw new RuntimeException("Unsupported auth type: " + authConfig.getAuthType()); + } + } + + private String getOAuth2Token(ThirdPartySystemConnectorConnectionParam baseConnectionParam) throws Exception { + AuthConfig authConfig = baseConnectionParam.getAuthConfig(); + + OkHttpRequestHeaders headers = new OkHttpRequestHeaders(); + headers.setHeaders(new HashMap<>()); + headers.setOkHttpRequestHeaderContentType(OkHttpRequestHeaderContentType.APPLICATION_FORM_URLENCODED); + + FormBody.Builder formBodyBuilder = new FormBody.Builder() + .add("client_id", authConfig.getOauth2ClientId()) + .add("client_secret", authConfig.getOauth2ClientSecret()) + .add("username", authConfig.getOauth2Username()) + .add("password", authConfig.getOauth2Password()) + .add("grant_type", authConfig.getOauth2GrantType()); + + // 添加 authMappings 中的参数 + if (authConfig.getAuthMappings() != null) { + for (AuthMapping authMapping : authConfig.getAuthMappings()) { + formBodyBuilder.add(authMapping.getKey(), authMapping.getValue()); + } + } + + RequestBody formBody = formBodyBuilder.build(); + + OkHttpResponse response = OkHttpUtils.postFormBody( + baseConnectionParam.getServiceAddress() + authConfig.getOauth2TokenUrl(), + headers, + null, + formBody, + HTTP_CONNECT_TIMEOUT, HTTP_CONNECT_TIMEOUT, HTTP_CONNECT_TIMEOUT); + + if (response.getStatusCode() != 200) { + throw new RuntimeException("Authentication failed: " + response.getBody()); + } + + JsonNode authResult = JSONUtils.parseObject(response.getBody(), JsonNode.class); + if (authResult.has("access_token")) { + log.info("Authentication successful, token obtained"); + return authResult.get("access_token").asText(); + } else { + throw new RuntimeException("Failed to get access token from response"); + } + } + + @Override + public DbType getDbType() { + return DbType.THIRDPARTY_SYSTEM_CONNECTOR; + } + + @Override + public DataSourceProcessor create() { + return new ThirdPartySystemConnectorDataSourceProcessor(); + } +} diff --git a/dolphinscheduler-datasource-plugin/pom.xml b/dolphinscheduler-datasource-plugin/pom.xml index 66be77400e67..22ad429f4c84 100644 --- a/dolphinscheduler-datasource-plugin/pom.xml +++ b/dolphinscheduler-datasource-plugin/pom.xml @@ -58,6 +58,7 @@ dolphinscheduler-datasource-hana dolphinscheduler-datasource-aliyunserverlessspark dolphinscheduler-datasource-dolphindb + dolphinscheduler-datasource-thirdpartysystemconnector diff --git a/dolphinscheduler-spi/src/main/java/org/apache/dolphinscheduler/spi/enums/DbType.java b/dolphinscheduler-spi/src/main/java/org/apache/dolphinscheduler/spi/enums/DbType.java index 6abe98db3cad..2e33e0addbb0 100644 --- a/dolphinscheduler-spi/src/main/java/org/apache/dolphinscheduler/spi/enums/DbType.java +++ b/dolphinscheduler-spi/src/main/java/org/apache/dolphinscheduler/spi/enums/DbType.java @@ -61,7 +61,8 @@ public enum DbType { K8S(26, "k8s", "k8s"), ALIYUN_SERVERLESS_SPARK(27, "aliyun_serverless_spark", "aliyun serverless spark"), - DOLPHINDB(28, "dolphindb", "dolphindb"); + DOLPHINDB(28, "dolphindb", "dolphindb"), + THIRDPARTY_SYSTEM_CONNECTOR(29, "thirdparty_system_connector", "thirdparty system connector"); private static final Map DB_TYPE_MAP = Arrays.stream(DbType.values()).collect(toMap(DbType::getCode, Functions.identity())); diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-all/pom.xml b/dolphinscheduler-task-plugin/dolphinscheduler-task-all/pom.xml index 4fee472507df..a45c3a93f708 100644 --- a/dolphinscheduler-task-plugin/dolphinscheduler-task-all/pom.xml +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-all/pom.xml @@ -222,6 +222,11 @@ dolphinscheduler-task-aliyunserverlessspark ${project.version} + + org.apache.dolphinscheduler + dolphinscheduler-task-external-system + ${project.version} + diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/pom.xml b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/pom.xml new file mode 100644 index 000000000000..1e35830587a1 --- /dev/null +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/pom.xml @@ -0,0 +1,103 @@ + + + + 4.0.0 + + org.apache.dolphinscheduler + dolphinscheduler-task-plugin + dev-SNAPSHOT + + + dolphinscheduler-task-external-system + jar + + + task.external-system + + + + + org.apache.dolphinscheduler + dolphinscheduler-task-api + ${project.version} + provided + + + + org.apache.dolphinscheduler + dolphinscheduler-common + ${project.version} + provided + + + + org.apache.dolphinscheduler + dolphinscheduler-datasource-api + ${project.version} + provided + + + + org.apache.dolphinscheduler + dolphinscheduler-datasource-ssh + ${project.version} + + + + org.apache.httpcomponents + httpcore + + + + com.squareup.okhttp3 + mockwebserver + test + + + junit + junit + + + + + + com.jayway.jsonpath + json-path + 2.7.0 + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + + + + shade + + package + + + + + + diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/AuthenticationUtils.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/AuthenticationUtils.java new file mode 100644 index 000000000000..7c10c65e88e8 --- /dev/null +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/AuthenticationUtils.java @@ -0,0 +1,123 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.task.externalSystem; + +import org.apache.dolphinscheduler.common.model.OkHttpRequestHeaderContentType; +import org.apache.dolphinscheduler.common.model.OkHttpRequestHeaders; +import org.apache.dolphinscheduler.common.model.OkHttpResponse; +import org.apache.dolphinscheduler.common.utils.JSONUtils; +import org.apache.dolphinscheduler.common.utils.OkHttpUtils; +import org.apache.dolphinscheduler.plugin.task.api.TaskException; + +import java.util.HashMap; + +import lombok.extern.slf4j.Slf4j; +import okhttp3.FormBody; +import okhttp3.RequestBody; + +import com.fasterxml.jackson.databind.JsonNode; + +@Slf4j +public class AuthenticationUtils { + + /** + * 认证并获取Token + * + * @param baseExternalSystemParams 认证配置 + * @return 认证后的Token + * @throws Exception + */ + public static String authenticateAndGetToken(BaseExternalSystemParams baseExternalSystemParams) throws Exception { + BaseExternalSystemParams.AuthConfig authConfig = baseExternalSystemParams.getAuthConfig(); + if (authConfig == null) { + throw new RuntimeException("AuthConfig is not provided"); + } + + switch (authConfig.getAuthType()) { + case BASIC_AUTH: + // 基础认证 + String auth = authConfig.getBasicUsername() + ":" + authConfig.getBasicPassword(); + String encoding = java.util.Base64.getEncoder().encodeToString(auth.getBytes()); + return encoding; + case JWT: + // JWT认证 + return authConfig.getJwtToken(); + case OAUTH2: + // OAuth2认证 + return getOAuth2Token(baseExternalSystemParams); + default: + throw new RuntimeException("Unsupported auth type: " + authConfig.getAuthType()); + } + } + + /** + * 获取OAuth2 Token + * + * @param baseExternalSystemParams 认证配置 + * @return OAuth2 Token + * @throws Exception + */ + private static String getOAuth2Token(BaseExternalSystemParams baseExternalSystemParams) throws Exception { + BaseExternalSystemParams.AuthConfig authConfig = baseExternalSystemParams.getAuthConfig(); + try { + OkHttpRequestHeaders headers = new OkHttpRequestHeaders(); + headers.setHeaders(new HashMap<>()); + headers.setOkHttpRequestHeaderContentType(OkHttpRequestHeaderContentType.APPLICATION_FORM_URLENCODED); + + FormBody.Builder formBodyBuilder = new FormBody.Builder() + .add("client_id", authConfig.getOauth2ClientId()) + .add("client_secret", authConfig.getOauth2ClientSecret()) + .add("username", authConfig.getOauth2Username()) + .add("password", authConfig.getOauth2Password()) + .add("grant_type", authConfig.getOauth2GrantType()); + + // 添加 authMappings 中的参数 + if (authConfig.getAuthMappings() != null) { + for (BaseExternalSystemParams.AuthMapping authMapping : authConfig.getAuthMappings()) { + formBodyBuilder.add(authMapping.getKey(), authMapping.getValue()); + } + } + + RequestBody formBody = formBodyBuilder.build(); + + OkHttpResponse response = OkHttpUtils.postFormBody( + baseExternalSystemParams.getCompleteUrl(authConfig.getOauth2TokenUrl()), + headers, + null, + formBody, + 30000, 30000, 30000); + + if (response.getStatusCode() != 200) { + throw new TaskException("Authentication failed: " + response.getBody()); + } + + JsonNode authResult = JSONUtils.parseObject(response.getBody(), JsonNode.class); + if (authResult.has("access_token")) { + log.info("Authentication successful, token obtained"); + return authResult.get("access_token").asText(); + } else { + throw new TaskException("Failed to get access token from response"); + } + + } catch (Exception e) { + log.error("Authentication failed", e); + throw new TaskException("Authentication failed", e); + } + } + +} diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/BaseExternalSystemParams.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/BaseExternalSystemParams.java new file mode 100644 index 000000000000..ece3c3eb806e --- /dev/null +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/BaseExternalSystemParams.java @@ -0,0 +1,158 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.task.externalSystem; + +import java.util.List; + +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +@Data +@Slf4j +public class BaseExternalSystemParams { + + private Integer id; // System ID + + private String systemName; // System name + private String serviceAddress; // Service address + + private AuthConfig authConfig; // Authentication configuration + + private InterfaceConfig selectInterface; // Query interface configuration + private InterfaceConfig submitInterface; // Submit interface configuration + private PollingInterfaceConfig pollStatusInterface; // Polling status interface configuration + private InterfaceConfig stopInterface; // Stop interface configuration + + private int interfaceTimeout = 120000; // Interface timeout, default 120000 milliseconds (2 minutes) + + @Data + public static class AuthConfig { + + private AuthType authType; // Authentication type: BASIC, JWT, OAUTH2 + private String headerPrefix; // Authentication type: BASIC, JWT, OAUTH2 + + // === Basic Authentication === + private String basicUsername; + private String basicPassword; + + // === JWT Authentication === + private String jwtToken; // JWT token + + // === OAuth2 Authentication === + private String oauth2TokenUrl; + private String oauth2ClientId; + private String oauth2ClientSecret; + private String oauth2GrantType; // e.g., "client_credentials", "password" + private String oauth2Username; // Password mode only + private String oauth2Password; // Password mode only + + // === Dynamic mapping configuration (e.g. request header/parameter mapping) === + private AuthMapping[] authMappings; + } + + public enum AuthType { + BASIC_AUTH, JWT, OAUTH2 + } + + @Data + public static class InterfaceConfig { + + private String url; + private HttpMethod method; // Request method GET/POST + private String body; + private List parameters; // Parameter list + private List responseParameters; // Parameter list + + } + + @Data + public static class PollingInterfaceConfig extends InterfaceConfig { + + private PollingSuccessConfig pollingSuccessConfig; // Polling success configuration + private PollingFailureConfig pollingFailureConfig; // Polling failure configuration + } + + @Data + public static class RequestParameter { + + private String paramName; // Parameter name + private String paramValue; // Parameter value (can be a fixed value or placeholder) + private ParamLocation location; // Parameter location (header,param,body) + } + + @Data + public static class ResponseParameter { + + private String key; + private String jsonPath; + } + + @Data + public static class PollingSuccessConfig { + + private String successField; // Success judgment field name + private String successValue; // Value corresponding to success field + } + + @Data + public static class PollingFailureConfig { + + private String failureField; // Failure judgment field name + private String failureValue; // Value corresponding to failure field + } + + // Enum: Field types + public enum FieldType { + STRING, INTEGER, BOOLEAN, DATE, JSON_OBJECT, CUSTOM + } + + // Enum: HTTP Methods + public enum HttpMethod { + GET, POST, PUT + } + + // Enum: Parameter locations + public enum ParamLocation { + HEADER, PARAM + } + + @Data + public static class AuthMapping { + + private String key; + private String value; + } + public String getTokenPrefix(String headerPrefix) { + if (null == headerPrefix || headerPrefix.isEmpty()) { + return ""; + } else { + return headerPrefix.trim() + " "; + } + } + public String getCompleteUrl(String url) { + if (url == null || !url.startsWith("http")) { + if (serviceAddress == null) { + log.warn("Service address is not set."); + return url; // 或者抛出异常,根据业务需求决定 + } + return serviceAddress + url; + } + return url; + } + +} diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemParameters.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemParameters.java new file mode 100644 index 000000000000..e7a3686ba307 --- /dev/null +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemParameters.java @@ -0,0 +1,101 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.task.externalSystem; + +import org.apache.dolphinscheduler.common.utils.JSONUtils; +import org.apache.dolphinscheduler.plugin.task.api.enums.ResourceType; +import org.apache.dolphinscheduler.plugin.task.api.parameters.AbstractParameters; +import org.apache.dolphinscheduler.plugin.task.api.parameters.resource.DataSourceParameters; +import org.apache.dolphinscheduler.plugin.task.api.parameters.resource.ResourceParametersHelper; + +import org.jetbrains.annotations.NotNull; + +public class ExternalSystemParameters extends AbstractParameters { + + private int datasource; + + private int externalSystemId; + + private String authenticationToken; + + private String externalTaskId; + private String externalTaskName; + + public int getDatasource() { + return datasource; + } + + public void setDatasource(int datasource) { + this.datasource = datasource; + } + + public String getAuthenticationToken() { + return authenticationToken; + } + + public void setAuthenticationToken(String authenticationToken) { + this.authenticationToken = authenticationToken; + } + + public String getExternalTaskId() { + return externalTaskId; + } + + public void setExternalTaskId(String externalTaskId) { + this.externalTaskId = externalTaskId; + } + + public String getExternalTaskName() { + return externalTaskName; + } + + public void setExternalTaskName(String externalTaskName) { + this.externalTaskName = externalTaskName; + } + + public int getExternalSystemId() { + return externalSystemId; + } + + public void setExternalSystemId(int externalSystemId) { + this.externalSystemId = externalSystemId; + } + + @Override + public ResourceParametersHelper getResources() { + ResourceParametersHelper resources = super.getResources(); + resources.put(ResourceType.DATASOURCE, datasource); + return resources; + } + + public boolean checkParameters() { + // Add validation logic here + return true; + } + + public BaseExternalSystemParams generateExtendedContext(@NotNull ResourceParametersHelper parametersHelper) { + DataSourceParameters externalSystemResourceParameters = + (DataSourceParameters) parametersHelper.getResourceParameters(ResourceType.DATASOURCE, + externalSystemId); + BaseExternalSystemParams baseExternalSystemParams = + JSONUtils.parseObject(externalSystemResourceParameters.getConnectionParams(), + BaseExternalSystemParams.class); + return baseExternalSystemParams; + } + +} diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTask.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTask.java new file mode 100644 index 000000000000..d915364e2bc2 --- /dev/null +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTask.java @@ -0,0 +1,503 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.task.externalSystem; + +import org.apache.dolphinscheduler.common.model.OkHttpRequestHeaderContentType; +import org.apache.dolphinscheduler.common.model.OkHttpRequestHeaders; +import org.apache.dolphinscheduler.common.model.OkHttpResponse; +import org.apache.dolphinscheduler.common.utils.JSONUtils; +import org.apache.dolphinscheduler.common.utils.OkHttpUtils; +import org.apache.dolphinscheduler.plugin.task.api.AbstractTask; +import org.apache.dolphinscheduler.plugin.task.api.TaskCallBack; +import org.apache.dolphinscheduler.plugin.task.api.TaskConstants; +import org.apache.dolphinscheduler.plugin.task.api.TaskException; +import org.apache.dolphinscheduler.plugin.task.api.TaskExecutionContext; +import org.apache.dolphinscheduler.plugin.task.api.enums.TaskTimeoutStrategy; +import org.apache.dolphinscheduler.plugin.task.api.model.Property; +import org.apache.dolphinscheduler.plugin.task.api.parameters.AbstractParameters; +import org.apache.dolphinscheduler.plugin.task.api.utils.ParameterUtils; + +import org.apache.commons.lang3.StringUtils; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import lombok.extern.slf4j.Slf4j; +import okhttp3.FormBody; + +import com.jayway.jsonpath.JsonPath; + +@Slf4j +public class ExternalSystemTask extends AbstractTask { + + private Boolean traceEnabled = true; + + private ExternalSystemParameters externalSystemParameters; + private BaseExternalSystemParams baseExternalSystemParams; + private TaskExecutionContext taskExecutionContext; + private String accessToken; + private Map parameterMap = new HashMap<>(); + private Set successStatusCache = new HashSet<>(); + private Set failureStatusCache = new HashSet<>(); + + private boolean isTimeout = false; + private long taskStartTime; + + public ExternalSystemTask(TaskExecutionContext taskExecutionContext) { + super(taskExecutionContext); + this.taskExecutionContext = taskExecutionContext; + this.externalSystemParameters = + JSONUtils.parseObject(taskExecutionContext.getTaskParams(), ExternalSystemParameters.class); + baseExternalSystemParams = + externalSystemParameters.generateExtendedContext(taskExecutionContext.getResourceParametersHelper()); + accessToken = + baseExternalSystemParams.getTokenPrefix(baseExternalSystemParams.getAuthConfig().getHeaderPrefix()) + + baseExternalSystemParams.getAuthConfig().getJwtToken();// todo + // AuthenticationUtils.generateTokenForSystem + } + + @Override + public void init() { + externalSystemParameters = JSONUtils.parseObject( + taskExecutionContext.getTaskParams(), + ExternalSystemParameters.class); + log.info("Initialize external system task params {}", + JSONUtils.toPrettyJsonString(externalSystemParameters)); + + if (externalSystemParameters == null || !externalSystemParameters.checkParameters()) { + throw new RuntimeException("external system task params is not valid"); + } + + // Initialize parameter mapping + initParameterMap(); + initStatusCache(); + } + + @Override + public void handle(TaskCallBack taskCallBack) throws TaskException { + try { + taskStartTime = System.currentTimeMillis(); + submitExternalTask(); + TimeUnit.SECONDS.sleep(10); + trackExternalTaskStatus(); + } catch (Exception e) { + log.error("external system task error", e); + setExitStatusCode(TaskConstants.EXIT_CODE_FAILURE); + throw new TaskException("Execute external system task failed", e); + } + } + + @Override + public void cancel() throws TaskException { + try { + log.info("cancel external system task"); + cancelTaskInstance(); + } catch (Exception e) { + throw new TaskException("cancel external system task error", e); + } finally { + // Only set to failure status when timeout failure strategy is enabled and it actually timed out + log.info("External task timeout check isTimeoutFailureEnabled:{}", isTimeoutFailureEnabled()); + if (isTimeoutFailureEnabled()) { + long currentTime = System.currentTimeMillis(); + long usedTimeMillis = currentTime - taskStartTime; + long usedTime = (usedTimeMillis + 29999) / 60000; // Over 30 seconds takes 1 minute, less than 30 + // seconds takes 0 minutes + log.info( + "External task timeout check, used time: {}m, timeout: {}m, currentTime: {}, taskStartTime: {}", + usedTime, taskExecutionContext.getTaskTimeout() / 60, currentTime, taskStartTime); + if (usedTime >= taskExecutionContext.getTaskTimeout() / 60) { + isTimeout = true; + log.warn("External task timeout, used time: {}m, timeout: {}m", + usedTime, taskExecutionContext.getTaskTimeout() / 60); + } + } + if (isTimeout) { + setExitStatusCode(TaskConstants.EXIT_CODE_FAILURE); + log.info("External task cancelled due to timeout, set status to FAILED"); + } else { + setExitStatusCode(TaskConstants.EXIT_CODE_KILL); + log.info("External task cancelled manually or timeout not enabled, set status to KILLED"); + } + } + } + + private void submitExternalTask() throws TaskException { + try { + BaseExternalSystemParams.InterfaceConfig submitConfig = baseExternalSystemParams.getSubmitInterface(); + String url = replaceParameterPlaceholders(baseExternalSystemParams.getCompleteUrl(submitConfig.getUrl())); + Map headers = new HashMap<>(); + buildAuthHeader(accessToken, headers); + buildHeaders(submitConfig, headers); + Map requestBody = buildRequestBody(submitConfig); + Map requestParams = buildRequestParams(submitConfig); + + int interfaceTimeout = baseExternalSystemParams.getInterfaceTimeout(); + log.info("Using interface timeout value: {} milliseconds", interfaceTimeout); + OkHttpResponse response = + executeRequestWithoutRetry(submitConfig.getMethod(), url, headers, requestParams, requestBody, + interfaceTimeout, interfaceTimeout, interfaceTimeout); + log.info("Submit task response:{}", response); + if (response.getStatusCode() != 200) { + throw new TaskException("Submit task failed: " + response.getBody()); + } + + parseSubmitResponse(submitConfig.getResponseParameters(), response.getBody()); + } catch (Exception e) { + log.error("Submit task failed:{}", e); + throw new TaskException("Submit task failed", e); + } + } + + private void trackExternalTaskStatus() throws TaskException { + try { + String status; + do { + // Only check timeout when timeout failure strategy is enabled + if (isTimeoutFailureEnabled()) { + long currentTime = System.currentTimeMillis(); + long usedTimeMillis = currentTime - taskStartTime; + long usedTime = (usedTimeMillis + 29999) / 60000; // Over 30 seconds takes 1 minute, less than 30 + // seconds takes 0 minutes + if (usedTime >= taskExecutionContext.getTaskTimeout()) { + isTimeout = true; + log.error("External task timeout, used time: {}m, timeout: {}m", + usedTime, taskExecutionContext.getTaskTimeout() / 60); + setExitStatusCode(TaskConstants.EXIT_CODE_FAILURE); + cancelTaskInstance(); + return; + } + } + + status = pollTaskStatus(); + + if (successStatusCache.contains(status)) { + setExitStatusCode(TaskConstants.EXIT_CODE_SUCCESS); + log.info("External task completed successfully with status: {}", status); + return; + } else if (failureStatusCache.contains(status)) { + setExitStatusCode(TaskConstants.EXIT_CODE_FAILURE); + log.error("External task failed with status: {}", status); + return; + } + + TimeUnit.SECONDS.sleep(10); + } while (traceEnabled); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new TaskException("Task status tracking interrupted", e); + } catch (Exception e) { + log.error("Track task status failed", e); + setExitStatusCode(TaskConstants.EXIT_CODE_FAILURE); + throw new TaskException("Track task status failed", e); + } + } + + private String pollTaskStatus() throws TaskException { + try { + BaseExternalSystemParams.PollingInterfaceConfig pollConfig = + baseExternalSystemParams.getPollStatusInterface(); + String url = replaceParameterPlaceholders(baseExternalSystemParams.getCompleteUrl(pollConfig.getUrl())); + Map headers = new HashMap<>(); + buildAuthHeader(accessToken, headers); + buildHeaders(pollConfig, headers); + Map requestBody = buildRequestBody(pollConfig); + Map requestParams = buildRequestParams(pollConfig); + + int interfaceTimeout = baseExternalSystemParams.getInterfaceTimeout(); + OkHttpResponse response = + executeRequestWithRetry(pollConfig.getMethod(), url, headers, requestParams, requestBody, + interfaceTimeout, interfaceTimeout, interfaceTimeout, 3); + log.info("poll task status response:{}", response); + + if (response.getStatusCode() != 200) { + throw new TaskException("polling task failed: " + response.getBody()); + } + + String successField = pollConfig.getPollingSuccessConfig().getSuccessField(); + String failureField = pollConfig.getPollingFailureConfig().getFailureField(); + + Object successStatusObj = JsonPath.read(response.getBody(), successField); + Object failureStatusObj = JsonPath.read(response.getBody(), failureField); + + log.info("PollTaskStatus successfully, external task instance success status: {}, failure status: {}", + successStatusObj.toString(), failureStatusObj.toString()); + + if (successStatusObj != null) { + return successStatusObj.toString(); + } else if (failureStatusObj != null) { + return failureStatusObj.toString(); + } else { + log.warn("successStatusObj and failureStatusObj are null"); + return "UNKNOWN"; + } + } catch (Exception e) { + log.error("Poll task status failed", e); + throw new TaskException("Poll task status failed", e); + } + } + + private void cancelTaskInstance() throws TaskException { + try { + traceEnabled = false; + BaseExternalSystemParams.InterfaceConfig stopConfig = baseExternalSystemParams.getStopInterface(); + log.info("start cancel External System TaskInstance"); + String url = replaceParameterPlaceholders(baseExternalSystemParams.getCompleteUrl(stopConfig.getUrl())); + Map headers = new HashMap<>(); + buildAuthHeader(accessToken, headers); + buildHeaders(stopConfig, headers); + Map requestBody = buildRequestBody(stopConfig); + Map requestParams = buildRequestParams(stopConfig); + + int interfaceTimeout = baseExternalSystemParams.getInterfaceTimeout(); + OkHttpResponse response = + executeRequestWithRetry(stopConfig.getMethod(), url, headers, requestParams, requestBody, + interfaceTimeout, interfaceTimeout, interfaceTimeout, 3); + log.info("cancel task response:{}", response); + + if (response.getStatusCode() != 200) { + throw new TaskException("Cancel task failed: " + response.getBody()); + } + log.info("Cancel task result: {}", response.getBody()); + } catch (Exception e) { + log.error("Cancel task failed", e); + throw new TaskException("Cancel task failed", e); + } + } + + private OkHttpResponse executeRequestWithoutRetry(BaseExternalSystemParams.HttpMethod method, String url, + Map headers, Map requestParams, + Map requestBody, int connectTimeout, + int readTimeout, + int writeTimeout) throws TaskException { + return executeRequestWithRetry(method, url, headers, requestParams, requestBody, connectTimeout, readTimeout, + writeTimeout, 0); + } + + private OkHttpResponse executeRequestWithRetry(BaseExternalSystemParams.HttpMethod method, String url, + Map headers, Map requestParams, + Map requestBody, int connectTimeout, int readTimeout, + int writeTimeout, int maxRetries) throws TaskException { + int retryCount = 0; + while (retryCount <= maxRetries) { + OkHttpRequestHeaders okHttpRequestHeaders = new OkHttpRequestHeaders(); + okHttpRequestHeaders.setHeaders(headers); + OkHttpRequestHeaderContentType contentType = getContentType(headers); + okHttpRequestHeaders.setOkHttpRequestHeaderContentType(getContentType(headers)); + try { + switch (method) { + case POST: + if (contentType.equals(OkHttpRequestHeaderContentType.APPLICATION_JSON)) { + return OkHttpUtils.post(url, okHttpRequestHeaders, requestParams, requestBody, + connectTimeout, + readTimeout, writeTimeout); + } + if (contentType.equals(OkHttpRequestHeaderContentType.APPLICATION_FORM_URLENCODED)) { + FormBody.Builder formBodyBuilder = new FormBody.Builder(); + if (requestBody != null) { + for (Map.Entry entry : requestBody.entrySet()) { + formBodyBuilder.add(entry.getKey(), entry.getValue().toString()); + } + } + return OkHttpUtils.postFormBody(url, okHttpRequestHeaders, requestParams, + formBodyBuilder.build(), connectTimeout, + readTimeout, writeTimeout); + } + case PUT: + return OkHttpUtils.put(url, okHttpRequestHeaders, requestBody, connectTimeout, readTimeout, + writeTimeout); + case GET: + return OkHttpUtils.get(url, okHttpRequestHeaders, requestParams, connectTimeout, readTimeout, + writeTimeout); + default: + throw new TaskException("Unsupported HTTP method: " + method); + } + } catch (Exception e) { + retryCount++; + if (maxRetries > 0) { + log.warn("Request failed, retrying... (attempt {}/{})", retryCount, maxRetries, e); + if (retryCount > maxRetries) { + throw new TaskException("Request failed after " + maxRetries + " retries", e); + } + try { + TimeUnit.SECONDS.sleep(5); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + throw new TaskException("Request retry interrupted", ie); + } + } else { + throw new TaskException("Request failed without retry", e); + } + } + } + return null; // This line should never be reached + } + + private void buildAuthHeader(String accessToken, Map headers) { + headers.put("Authorization", accessToken); + } + + private Map buildHeaders(BaseExternalSystemParams.InterfaceConfig config, + Map requestParams) { + for (BaseExternalSystemParams.RequestParameter param : config.getParameters()) { + if (param.getLocation().equals(BaseExternalSystemParams.ParamLocation.HEADER)) { + requestParams.put(param.getParamName(), replaceParameterPlaceholders(param.getParamValue())); + } + } + return requestParams; + } + + private Map buildRequestBody(BaseExternalSystemParams.InterfaceConfig config) { + Map requestBody = new HashMap<>(); + if (config.getBody() != null) { + requestBody = JSONUtils.parseObject(replaceParameterPlaceholders(config.getBody()), Map.class); + } + return requestBody; + } + + private Map buildRequestParams(BaseExternalSystemParams.InterfaceConfig config) { + Map requestParams = new HashMap<>(); + for (BaseExternalSystemParams.RequestParameter param : config.getParameters()) { + if (param.getLocation().equals(BaseExternalSystemParams.ParamLocation.PARAM)) { + requestParams.put(param.getParamName(), replaceParameterPlaceholders(param.getParamValue())); + } + } + return requestParams; + } + + private String replaceParameterPlaceholders(String template) { + if (StringUtils.isEmpty(template)) { + return template; + } + + StringBuilder result = new StringBuilder(template); + for (Map.Entry entry : parameterMap.entrySet()) { + String placeholder = "${" + entry.getKey() + "}"; + int index; + while ((index = result.indexOf(placeholder)) != -1) { + result.replace(index, index + placeholder.length(), entry.getValue()); + } + } + String resultString = ParameterUtils.convertParameterPlaceholders(result.toString(), parameterMap); + log.info("after replaceParameterPlaceholders:{}", resultString); + return resultString; + } + + private void parseSubmitResponse(List responseParameters, + String responseBody) throws TaskException { + try { + for (BaseExternalSystemParams.ResponseParameter param : responseParameters) { + String jsonPath = param.getJsonPath(); + String key = param.getKey(); + Object value = JsonPath.read(responseBody, jsonPath); + + if (value == null) { + log.warn("Response parameter {} not found in response body", key); + continue; + } + + parameterMap.put(key, value.toString().replace("\"", "")); + log.info("Parsed parameter {}: {}", key, value.toString()); + + } + } catch (Exception e) { + log.error("Parse submit response failed", e); + throw new TaskException("Parse submit response failed", e); + } + } + + @Override + public AbstractParameters getParameters() { + return this.externalSystemParameters; + } + + /** + * Initialize parameter mapping + */ + private void initParameterMap() { + Map prepareParamsMap = taskExecutionContext.getPrepareParamsMap(); + if (prepareParamsMap != null) { + for (Map.Entry entry : prepareParamsMap.entrySet()) { + parameterMap.put(entry.getKey(), entry.getValue().getValue()); + } + } + if (externalSystemParameters.getExternalTaskId() != null) { + parameterMap.put(ExternalTaskConstants.EXTERNAL_TASK_ID, externalSystemParameters.getExternalTaskId()); + parameterMap.put(ExternalTaskConstants.EXTERNAL_TASK_NAME, externalSystemParameters.getExternalTaskName()); + } + } + + private void initStatusCache() { + BaseExternalSystemParams.PollingSuccessConfig successConfig = + baseExternalSystemParams.getPollStatusInterface().getPollingSuccessConfig(); + + if (successConfig != null && successConfig.getSuccessValue() != null) { + try { + String successValueString = successConfig.getSuccessValue(); + String[] successValues = successValueString.split(","); + for (String successValue : successValues) { + successStatusCache.add(successValue); + } + log.info("trackExternalTaskStatus successValues is :{}", successStatusCache); + + } catch (NullPointerException e) { + log.error("Error: successValue is null"); + } + } + BaseExternalSystemParams.PollingFailureConfig failureConfig = + baseExternalSystemParams.getPollStatusInterface().getPollingFailureConfig(); + if (failureConfig != null && failureConfig.getFailureField() != null) { + try { + String failureValueString = failureConfig.getFailureValue(); + String[] failureValues = failureValueString.split(","); + for (String failureValue : failureValues) { + failureStatusCache.add(failureValue); + } + log.info("trackExternalTaskStatus failureValues is :{}", failureStatusCache); + } catch (NullPointerException e) { + log.error("Error: failureStatus is null"); + } + } + } + + /** + * Check if timeout failure strategy is enabled + */ + private boolean isTimeoutFailureEnabled() { + return taskExecutionContext.getTaskTimeoutStrategy() != null + && taskExecutionContext.getTaskTimeout() > 0 + && taskExecutionContext.getTaskTimeout() < Integer.MAX_VALUE + && (taskExecutionContext.getTaskTimeoutStrategy() == TaskTimeoutStrategy.FAILED + || taskExecutionContext.getTaskTimeoutStrategy() == TaskTimeoutStrategy.WARNFAILED); + } + + private OkHttpRequestHeaderContentType getContentType(Map headers) { + if (headers == null || !headers.containsKey(ExternalTaskConstants.CONTENT_TYPE) + || !headers.containsKey(ExternalTaskConstants.CONTENT_TYPE_LOWERCASE)) { + return OkHttpRequestHeaderContentType.APPLICATION_JSON; + } + String contentType = headers.get(ExternalTaskConstants.CONTENT_TYPE); + return OkHttpRequestHeaderContentType.fromValue(contentType) != null + ? OkHttpRequestHeaderContentType.fromValue(contentType) + : OkHttpRequestHeaderContentType.APPLICATION_JSON; + } + +} diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTaskChannel.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTaskChannel.java new file mode 100644 index 000000000000..ac87a9ccca08 --- /dev/null +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTaskChannel.java @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.task.externalSystem; + +import org.apache.dolphinscheduler.common.utils.JSONUtils; +import org.apache.dolphinscheduler.plugin.task.api.AbstractTask; +import org.apache.dolphinscheduler.plugin.task.api.TaskChannel; +import org.apache.dolphinscheduler.plugin.task.api.TaskExecutionContext; +import org.apache.dolphinscheduler.plugin.task.api.parameters.AbstractParameters; + +public class ExternalSystemTaskChannel implements TaskChannel { + + @Override + public AbstractTask createTask(TaskExecutionContext taskRequest) { + return new ExternalSystemTask(taskRequest); + } + + @Override + public AbstractParameters parseParameters(String taskParams) { + return JSONUtils.parseObject(taskParams, ExternalSystemParameters.class); + } + +} diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTaskChannelFactory.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTaskChannelFactory.java new file mode 100644 index 000000000000..8a521403a365 --- /dev/null +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalSystemTaskChannelFactory.java @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.task.externalSystem; + +import org.apache.dolphinscheduler.plugin.task.api.TaskChannel; +import org.apache.dolphinscheduler.plugin.task.api.TaskChannelFactory; + +import com.google.auto.service.AutoService; + +@AutoService(TaskChannelFactory.class) +public class ExternalSystemTaskChannelFactory implements TaskChannelFactory { + + @Override + public String getName() { + return "EXTERNAL_SYSTEM"; + } + + @Override + public TaskChannel create() { + return new ExternalSystemTaskChannel(); + } +} diff --git a/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalTaskConstants.java b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalTaskConstants.java new file mode 100644 index 000000000000..d62df2207c6e --- /dev/null +++ b/dolphinscheduler-task-plugin/dolphinscheduler-task-external-system/src/main/java/org/apache/dolphinscheduler/plugin/task/externalSystem/ExternalTaskConstants.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.dolphinscheduler.plugin.task.externalSystem; + +import lombok.experimental.UtilityClass; + +@UtilityClass +public class ExternalTaskConstants { + + public static final String CONTENT_TYPE = "Content-Type"; + public static final String CONTENT_TYPE_LOWERCASE = "content-type"; + + public static final String EXTERNAL_TASK_ID = "id"; + public static final String EXTERNAL_TASK_NAME = "name"; + public static final int RESPONSE_CODE_SUCCESS = 200; +} diff --git a/dolphinscheduler-task-plugin/pom.xml b/dolphinscheduler-task-plugin/pom.xml index 41895289905f..718cd705d4cc 100644 --- a/dolphinscheduler-task-plugin/pom.xml +++ b/dolphinscheduler-task-plugin/pom.xml @@ -63,6 +63,7 @@ dolphinscheduler-task-datafactory dolphinscheduler-task-remoteshell dolphinscheduler-task-aliyunserverlessspark + dolphinscheduler-task-external-system diff --git a/dolphinscheduler-ui/public/images/task-icons/external_system.png b/dolphinscheduler-ui/public/images/task-icons/external_system.png new file mode 100644 index 0000000000000000000000000000000000000000..a564ebcc33a49d26a74f78971306879a33e6cc84 GIT binary patch literal 48646 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