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

ka94 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/incubator-devlake.git


The following commit(s) were added to refs/heads/main by this push:
     new 8762ae4f8 5080 Let test_connection report status from tool API (#5422)
8762ae4f8 is described below

commit 8762ae4f823131fd93944df9fcb27ae99e0cf322
Author: Camille Teruel <[email protected]>
AuthorDate: Fri Jun 9 21:00:32 2023 +0200

    5080 Let test_connection report status from tool API (#5422)
    
    * fix: AzureDevOps API returns 203 instead of 401
    
    When token is invalid, AzureDevOps API recently started returning 302s that 
redirect to a sign-in page
    with a status 203. Add a response hook to transform those 203 into 401.
    
    * feat: Let test_connection report HTTP status code
    
    Change test_connection to return a TestConnectionResult that contains a 
message and a status code
    that are used on go-side to build responses to GET 
'plugins/<plugin>/test-connection'.
    
    ---------
    
    Co-authored-by: Camille Teruel <[email protected]>
---
 backend/python/README.md                           | 16 +++++-------
 .../python/plugins/azuredevops/azuredevops/api.py  | 10 +++++++-
 .../python/plugins/azuredevops/azuredevops/main.py | 30 ++++++++++++----------
 backend/python/pydevlake/pydevlake/__init__.py     |  2 +-
 backend/python/pydevlake/pydevlake/ipc.py          |  4 +--
 backend/python/pydevlake/pydevlake/message.py      | 18 +++++++++++++
 backend/python/pydevlake/pydevlake/plugin.py       |  3 ++-
 backend/python/test/fakeplugin/fakeplugin/main.py  | 13 ++++++++--
 .../services/remote/plugin/connection_api.go       | 21 +++++++++++----
 9 files changed, 82 insertions(+), 35 deletions(-)

diff --git a/backend/python/README.md b/backend/python/README.md
index a93056b50..317d5dc73 100644
--- a/backend/python/README.md
+++ b/backend/python/README.md
@@ -57,7 +57,7 @@ class MyPlugin(dl.Plugin):
     def remote_scopes(self, connection, group_id: str) -> 
Iterable[MyPluginToolScope]:
         ...
 
-    def test_connection(self, connection: MyPluginConnection):
+    def test_connection(self, connection: MyPluginConnection) -> 
dl.TestConnectionResult:
         ...
 
 
@@ -191,19 +191,17 @@ class MyPlugin(dl.Plugin):
 
 The `test_connection` method is used to test if a given connection is valid.
 It should check that the connection credentials are valid.
-If the connection is not valid, it should raise an exception.
+It should make an authenticated request to the API and return a 
`TestConnectionResult`.
+There is a convenience static method `from_api_response` to create a 
`TestConnectionResult` object from an API response.
 
 ```python
 class MyPlugin(dl.Plugin):
     ...
 
-    def test_connection(self, connection: MyPluginConnection):
-        api = ...
-        response = ...
-        if response.status != 401:
-            raise Exception("Invalid credentials")
-        if response.status != 200:
-            raise Exception(f"Connection error {response}")
+    def test_connection(self, connection: MyPluginConnection) -> 
dl.TestConnectionResult:
+        api = ... # Create API client
+        response = ... # Make authenticated request to API
+        return dl.TestConnection.from_api_response(response)
 ```
 
 
diff --git a/backend/python/plugins/azuredevops/azuredevops/api.py 
b/backend/python/plugins/azuredevops/azuredevops/api.py
index f187f4125..0ceb0b5f4 100644
--- a/backend/python/plugins/azuredevops/azuredevops/api.py
+++ b/backend/python/plugins/azuredevops/azuredevops/api.py
@@ -16,7 +16,7 @@
 from typing import Optional
 import base64
 
-from pydevlake.api import API, request_hook, Paginator, Request
+from pydevlake.api import API, APIException, Paginator, Request, Response, 
request_hook, response_hook
 
 
 class AzurePaginator(Paginator):
@@ -43,6 +43,14 @@ class AzureDevOpsAPI(API):
     def set_api_version(self, request: Request):
         request.query_args['api-version'] = "7.0"
 
+    @response_hook
+    def change_203_to_401(self, response: Response):
+        # When the token is invalid, Azure DevOps returns a 302 that resolves 
to a sign-in page with status 203
+        # We want to change that to a 401 and raise an exception
+        if response.status == 203:
+            response.status = 401
+            raise APIException(response)
+
     def my_profile(self):
         req = 
Request('https://app.vssps.visualstudio.com/_apis/profile/profiles/me')
         return self.send(req)
diff --git a/backend/python/plugins/azuredevops/azuredevops/main.py 
b/backend/python/plugins/azuredevops/azuredevops/main.py
index cc5f28e2c..14172ac1c 100644
--- a/backend/python/plugins/azuredevops/azuredevops/main.py
+++ b/backend/python/plugins/azuredevops/azuredevops/main.py
@@ -22,7 +22,7 @@ from azuredevops.streams.jobs import Jobs
 from azuredevops.streams.pull_request_commits import GitPullRequestCommits
 from azuredevops.streams.pull_requests import GitPullRequests
 
-from pydevlake import Plugin, RemoteScopeGroup, DomainType, ScopeConfigPair
+from pydevlake import Plugin, RemoteScopeGroup, DomainType, ScopeConfigPair, 
TestConnectionResult
 from pydevlake.domain_layer.code import Repo
 from pydevlake.domain_layer.devops import CicdScope
 from pydevlake.pipeline_tasks import gitextractor, refdiff
@@ -106,20 +106,22 @@ class AzureDevOpsPlugin(Plugin):
                     defaultBranch=props.get('defaultBranch', 'main')
                 )
 
-    def test_connection(self, connection: AzureDevOpsConnection):
+    def test_connection(self, connection: AzureDevOpsConnection) -> 
TestConnectionResult:
         api = AzureDevOpsAPI(connection)
-        if connection.organization is None:
-            try:
-                api.my_profile()
-            except APIException as e:
-                if e.response.status == 401:
-                    raise Exception(f"Invalid token {e}. You may need to set 
organization name in connection or edit your token to set organization to 'All 
accessible organizations'")
-                raise
-        else:
-            try:
-                api.projects(connection.organization)
-            except APIException as e:
-                raise Exception(f"Invalid token: {e}")
+        message = None
+        hint = None
+        try:
+            if connection.organization is None:
+                hint = "You may need to edit your token to set organization to 
'All accessible organizations"
+                res = api.my_profile()
+            else:
+                hint = "Organization name may be incorrect or your token may 
not have access to the organization."
+                res = api.projects(connection.organization)
+        except APIException as e:
+            res = e.response
+            if res.status == 401:
+                message = f"Invalid token. {hint}"
+        return TestConnectionResult.from_api_response(res, message)
 
     def extra_tasks(self, scope: GitRepository, scope_config: 
GitRepositoryConfig, connection: AzureDevOpsConnection):
         if DomainType.CODE in scope_config.entity_types and not 
scope.is_external():
diff --git a/backend/python/pydevlake/pydevlake/__init__.py 
b/backend/python/pydevlake/pydevlake/__init__.py
index aff946fce..5adc1ba9e 100644
--- a/backend/python/pydevlake/pydevlake/__init__.py
+++ b/backend/python/pydevlake/pydevlake/__init__.py
@@ -35,7 +35,7 @@ def Field(*args, primary_key: bool=False, source: 
Optional[str]=None, **kwargs):
 
 from .model import ToolModel, ToolScope, DomainScope, Connection, ScopeConfig, 
DomainType, domain_id
 from .logger import logger
-from .message import RemoteScopeGroup
+from .message import RemoteScopeGroup, TestConnectionResult
 from .plugin import Plugin, ScopeConfigPair
 from .stream import Stream, Substream
 from .context import Context
diff --git a/backend/python/pydevlake/pydevlake/ipc.py 
b/backend/python/pydevlake/pydevlake/ipc.py
index 7a9fd237f..1db11d88f 100644
--- a/backend/python/pydevlake/pydevlake/ipc.py
+++ b/backend/python/pydevlake/pydevlake/ipc.py
@@ -26,7 +26,7 @@ from sqlalchemy.engine import Engine
 
 from pydevlake.context import Context
 from pydevlake.message import Message
-from pydevlake.model import DomainType, SubtaskRun
+from pydevlake.model import SubtaskRun
 
 
 def plugin_method(func):
@@ -85,7 +85,7 @@ class PluginCommands:
         if "name" not in connection:
             connection["name"] = "Test connection"
         connection = self._plugin.connection_type(**connection)
-        self._plugin.test_connection(connection)
+        return self._plugin.test_connection(connection)
 
     @plugin_method
     def make_pipeline(self, scope_config_pairs: list[tuple[dict, dict]], 
connection: dict):
diff --git a/backend/python/pydevlake/pydevlake/message.py 
b/backend/python/pydevlake/pydevlake/message.py
index 54e078235..20d320439 100644
--- a/backend/python/pydevlake/pydevlake/message.py
+++ b/backend/python/pydevlake/pydevlake/message.py
@@ -21,6 +21,7 @@ import jsonref
 
 from pydevlake.model import ToolScope
 from pydevlake.migration import MigrationScript
+from pydevlake.api import Response
 
 
 class Message(BaseModel):
@@ -111,3 +112,20 @@ class RemoteScope(RemoteScopeTreeNode):
 
 class RemoteScopes(Message):
     __root__: list[RemoteScopeTreeNode]
+
+
+class TestConnectionResult(Message):
+    success: bool
+    message: str
+    status: int
+
+    @staticmethod
+    def from_api_response(response: Response, message: str = None):
+        success = response.status == 200
+        if not message:
+            message = "Connection successful" if success else "Connection 
failed"
+        return TestConnectionResult(
+            success=success,
+            message=message,
+            status=response.status
+        )
diff --git a/backend/python/pydevlake/pydevlake/plugin.py 
b/backend/python/pydevlake/pydevlake/plugin.py
index 3bcfd2dda..253abacd3 100644
--- a/backend/python/pydevlake/pydevlake/plugin.py
+++ b/backend/python/pydevlake/pydevlake/plugin.py
@@ -22,6 +22,7 @@ import sys
 import fire
 
 import pydevlake.message as msg
+from pydevlake.api import APIException
 from pydevlake.subtasks import Subtask
 from pydevlake.logger import logger
 from pydevlake.ipc import PluginCommands
@@ -68,7 +69,7 @@ class Plugin(ABC):
         return None
 
     @abstractmethod
-    def test_connection(self, connection: Connection):
+    def test_connection(self, connection: Connection) -> 
msg.TestConnectionResult:
         """
         Test if the the connection with the datasource can be established with 
the given connection.
         Must raise an exception if the connection can't be established.
diff --git a/backend/python/test/fakeplugin/fakeplugin/main.py 
b/backend/python/test/fakeplugin/fakeplugin/main.py
index 0c2461f71..ad3e625a2 100644
--- a/backend/python/test/fakeplugin/fakeplugin/main.py
+++ b/backend/python/test/fakeplugin/fakeplugin/main.py
@@ -20,7 +20,7 @@ import json
 
 from pydantic import SecretStr
 
-from pydevlake import Plugin, Connection, Stream, ToolModel, ToolScope, 
ScopeConfig, RemoteScopeGroup, DomainType, Field
+from pydevlake import Plugin, Connection, Stream, ToolModel, ToolScope, 
ScopeConfig, RemoteScopeGroup, DomainType, Field, TestConnectionResult
 from pydevlake.domain_layer.devops import CicdScope, CICDPipeline, CICDStatus, 
CICDResult, CICDType
 
 VALID_TOKEN = "this_is_a_valid_token"
@@ -147,7 +147,16 @@ class FakePlugin(Plugin):
 
     def test_connection(self, connection: FakeConnection):
         if connection.token.get_secret_value() != VALID_TOKEN:
-            raise Exception("Invalid token")
+            return TestConnectionResult(
+                success=False,
+                message="Invalid token",
+                status=401
+            )
+        return TestConnectionResult(
+            success=True,
+            message="Connection successful",
+            status=200
+        )
 
     @property
     def streams(self):
diff --git a/backend/server/services/remote/plugin/connection_api.go 
b/backend/server/services/remote/plugin/connection_api.go
index 81b49f5c7..93cdc8751 100644
--- a/backend/server/services/remote/plugin/connection_api.go
+++ b/backend/server/services/remote/plugin/connection_api.go
@@ -18,6 +18,7 @@ limitations under the License.
 package plugin
 
 import (
+       "fmt"
        "net/http"
 
        "github.com/apache/incubator-devlake/core/errors"
@@ -26,17 +27,27 @@ import (
        "github.com/apache/incubator-devlake/server/services/remote/bridge"
 )
 
+type TestConnectionResult struct {
+       Success bool   `json:"success"`
+       Message string `json:"message"`
+       Status  int    `json:"status"`
+}
+
 func (pa *pluginAPI) TestConnection(input *plugin.ApiResourceInput) 
(*plugin.ApiResourceOutput, errors.Error) {
-       err := pa.invoker.Call("test-connection", bridge.DefaultContext, 
input.Body).Err
+       var result TestConnectionResult
+       err := pa.invoker.Call("test-connection", bridge.DefaultContext, 
input.Body).Get(&result)
        if err != nil {
                body := shared.ApiBody{
                        Success: false,
-                       Message: err.Error(),
+                       Message: fmt.Sprintf("Error while testing connection: 
%s", err.Error()),
                }
-               return &plugin.ApiResourceOutput{Body: body, Status: 400}, nil
+               return &plugin.ApiResourceOutput{Body: body, Status: 500}, nil
        } else {
-               body := shared.ApiBody{Success: true}
-               return &plugin.ApiResourceOutput{Body: body, Status: 200}, nil
+               body := shared.ApiBody{
+                       Success: result.Success,
+                       Message: result.Message,
+               }
+               return &plugin.ApiResourceOutput{Body: body, Status: 
result.Status}, nil
        }
 }
 

Reply via email to