Repository: PrefectHQ/prefect

Issue:
Module import fails when deploying using Deployment.build_from_flow and remote storage and agent on a remove server
### First check

- [X] I added a descriptive title to this issue.
- [X] I used the GitHub search to find a similar issue and didn't find it.
- [X] I searched the Prefect documentation for this issue.
- [X] I checked that this issue is related to Prefect and not one of its dependencies.

### Bug summary

I'm using the new pythonic way to deploy in v2.1.0 with a RemoteFileSystem storage.
The deployment works fine, but when running the flow, an exception is raised, the script is not found.

FileNotFoundError: [Errno 2] No such file or directory: '/home/me/prefect/tasks/amsru.py'
(...detailed tracback here...)
prefect.exceptions.ScriptError: Script at '/home/me/prefect/tasks/amsru.py' encountered an exception

I have a deploy.py file in /home/me/prefect which "import tasks.amsru" because the flow is in '/home/me/prefect/tasks/amsru.py'. This is very similar to @anna-geller example.
It seems the error is because the agent on the server tries to import tasks.amsru from its local filesystem instead of the remote storage. I've checked that the tasks/ directory is uploaded on the remote storage and is up to date. 

I suspect the problem is L479 in deployments.py:
        deployment.entrypoint = f"{Path(flow_file).absolute()}:{flow.fn.__name__}"

the entrypoint is defined in a different why when using the CLI  L483 in cli/deployment.py

This bug might be related to #6463

### Reproduction

```python
in the main.py file

from flows.healthcheck import healthcheck
from prefect.deployments import Deployment
from prefect.filesystems import S3
from prefect.filesystems import RemoteFileSystem

deployment = Deployment.build_from_flow(
    flow=healthcheck,
    name="pythonics3",
    description="hi!",
    version="1",
    work_queue_name="default",
    tags=["myproject"],
    storage=RemoteFileSystem.load("code"),
    infra_overrides=dict(env={"PREFECT_LOGGING_LEVEL": "DEBUG"}),
    output="pythonics3.yaml",
)

if __name__ == "__main__":
    deployment.apply()


------
And in the flows/healthcheck.py file:


from prefect import flow

@flow
def healthcheck():
    return 'ok'
```


### Error

```

Traceback (most recent call last):
  File "<frozen importlib._bootstrap_external>", line 846, in exec_module
  File "<frozen importlib._bootstrap_external>", line 982, in get_code
  File "<frozen importlib._bootstrap_external>", line 1039, in get_data
FileNotFoundError: [Errno 2] No such file or directory: '/home/me/prefect/tasks/amsru.py'

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/me/miniconda3/envs/prefect/lib/python3.9/site-packages/prefect/engine.py", line 246, in retrieve_flow_then_begin_flow_run
    flow = await load_flow_from_flow_run(flow_run, client=client)
  File "/home/me/miniconda3/envs/prefect/lib/python3.9/site-packages/prefect/client.py", line 105, in with_injected_client
    return await fn(*args, **kwargs)
  File "/home/me/miniconda3/envs/prefect/lib/python3.9/site-packages/prefect/deployments.py", line 73, in load_flow_from_flow_run
    flow = await run_sync_in_worker_thread(import_object, str(import_path))
  File "/home/me/miniconda3/envs/prefect/lib/python3.9/site-packages/prefect/utilities/asyncutils.py", line 56, in run_sync_in_worker_thread
    return await anyio.to_thread.run_sync(call, cancellable=True)
  File "/home/me/miniconda3/envs/prefect/lib/python3.9/site-packages/anyio/to_thread.py", line 31, in run_sync
    return await get_asynclib().run_sync_in_worker_thread(
  File "/home/me/miniconda3/envs/prefect/lib/python3.9/site-packages/anyio/_backends/_asyncio.py", line 937, in run_sync_in_worker_thread
    return await future
  File "/home/me/miniconda3/envs/prefect/lib/python3.9/site-packages/anyio/_backends/_asyncio.py", line 867, in run
    result = context.run(func, *args)
  File "/home/me/miniconda3/envs/prefect/lib/python3.9/site-packages/prefect/utilities/importtools.py", line 193, in import_object
    module = load_script_as_module(script_path)
  File "/home/me/miniconda3/envs/prefect/lib/python3.9/site-packages/prefect/utilities/importtools.py", line 156, in load_script_as_module
    raise ScriptError(user_exc=exc, path=path) from exc
prefect.exceptions.ScriptError: Script at '/home/me/prefect/tasks/amsru.py' encountered an exception

```


### Versions

```
Version:             2.1.0
API version:         0.8.0
Python version:      3.9.13
Git commit:          30ca715c
Built:               Wed, Aug 17, 2022 5:13 PM
OS/Arch:             linux/x86_64
Profile:             default
Server type:         hosted
```


### Additional context

none


Discussion:
what remote system did you use? could you share the configuration (without sensitive info)? I'd like to try reproduce on S3, Azure or GCS
Also, could you try the same with CLI? This way, we can identify whether this has anything to do with the Python interface or is some general issue in your setup like path configuration

- I confirm that it works with the CLI. The problem is only when using python.
- I'm using RemoteFileSystem with a s3 compatible system (at my university).
#6482 and #6463 also raised the same issue. A user mentioned that doing this work for him:

```python
def fix_entrypoint(entrypoint: str) -> str:
    # Workaround until https://github.com/PrefectHQ/prefect/issues/6469 is resolved
    flow_path, flow = entrypoint.split(':')
    flow_path = Path(flow_path).relative_to(Path('.').absolute())
    return f"{flow_path}:{flow}"

d = Deployment.build_from_flow(...)
d.entrypoint = fix_entrypoint(d.entrypoint)
d.apply()
```

It seems to be something related to entrypoint
Definitely related to the entrypoint. It looks like the Deployment object uses the full path as mentioned above, while the CLI is based on the input by the user at build time.

Here are samples between the yaml generated via CLI vs the yaml generated via Python Object (some values were edited to remove keys & secrets)
[hello-deployment-cli.yaml.txt](https://github.com/PrefectHQ/prefect/files/9410819/hello-deployment-cli.yaml.txt)
vs
[hello-deployment-object.yaml.txt](https://github.com/PrefectHQ/prefect/files/9410820/hello-deployment-object.yaml.txt)

The storage block is fairly straight forward, just a simple remote file system to a minio bucket.
```
'''
This file creates the storage block to minio instance that hosts all our flows
'''

from prefect.filesystems import RemoteFileSystem

minio_block = RemoteFileSystem(
    basepath="s3://test-bucket",
    settings={
        "key": "<key>",
        "secret": "<secret>",
        "client_kwargs": {"endpoint_url": "https://<domain_to_minio>"},
    },
)
minio_block.save("test-bucket", overwrite=True)
```

Here is the deployment python script that generated the above yaml (without the to_yaml function)
```
from flows.f_hello import hello
from prefect.deployments import Deployment
from prefect.filesystems import RemoteFileSystem

storage = RemoteFileSystem.load("test-bucket")
deployment = Deployment.build_from_flow(
    flow=hello,
    name="hello",
    tags=[],
    storage=storage,
    path="flows",
    entrypoint="f_hello.py:hello",
)
deployment.apply()
```

It also looks like the entrypoint & path parameters are ignored when supplied.
I've also just hit the same issue when creating a deployment via the `Deployment.build_from_flow` method.

My expectation was that the relative path to the flow module would be used, but instead the `entrypoint` of the deployment is being set to the absolute path. This caused it to fail when executing remotely in a docker container.

Changing that line in `Deployment.build_from_flow` to the following fixed the issue for me:
```python
deployment.entrypoint = f"{Path(flow_file).relative_to(os.getcwd())}:{flow.fn.__name__}"
```
which is pretty much the work around @khuyentran1401 mentioned.

Resolving patch:
diff --git a/src/prefect/deployments.py b/src/prefect/deployments.py
--- a/src/prefect/deployments.py
+++ b/src/prefect/deployments.py
@@ -506,7 +506,8 @@ async def build_from_flow(
         await deployment.load()
 
         # set a few attributes for this flow object
-        deployment.entrypoint = f"{Path(flow_file).absolute()}:{flow.fn.__name__}"
+        entry_path = Path(flow_file).absolute().relative_to(Path(".").absolute())
+        deployment.entrypoint = f"{entry_path}:{flow.fn.__name__}"
         deployment.parameter_openapi_schema = parameter_schema(flow)
         if not deployment.version:
             deployment.version = flow.version


Test patch:
diff --git a/tests/cli/test_deployment_cli.py b/tests/cli/test_deployment_cli.py
--- a/tests/cli/test_deployment_cli.py
+++ b/tests/cli/test_deployment_cli.py
@@ -60,6 +60,24 @@ def test_work_queue_name_is_populated_as_default(self, patch_import, tmp_path):
         deployment = Deployment.load_from_yaml(tmp_path / "test.yaml")
         assert deployment.work_queue_name == "default"
 
+    def test_entrypoint_is_saved_as_relative_path(self, patch_import, tmp_path):
+        invoke_and_assert(
+            [
+                "deployment",
+                "build",
+                "fake-path.py:fn",
+                "-n",
+                "TEST",
+                "-o",
+                str(tmp_path / "test.yaml"),
+            ],
+            expected_code=0,
+            temp_dir=tmp_path,
+        )
+
+        deployment = Deployment.load_from_yaml(tmp_path / "test.yaml")
+        assert deployment.entrypoint == "fake-path.py:fn"
+
     def test_server_side_settings_are_used_if_present(self, patch_import, tmp_path):
         d = Deployment(
             name="TEST",
diff --git a/tests/test_deployments.py b/tests/test_deployments.py
--- a/tests/test_deployments.py
+++ b/tests/test_deployments.py
@@ -211,6 +211,15 @@ async def test_build_from_flow_sets_description_and_version_if_not_set(
         assert d.description == flow_function.description
         assert d.version == flow_function.version
 
+    async def test_build_from_flow_sets_correct_entrypoint(self, flow_function):
+        """
+        Entrypoints are *always* relative to {storage.basepath / path}
+        """
+        d = await Deployment.build_from_flow(
+            flow=flow_function, name="foo", description="a", version="b"
+        )
+        d.entrypoint == "tests/fixtures/client.py:client_test_flow"
+
     async def test_build_from_flow_sets_provided_attrs(self, flow_function):
         d = await Deployment.build_from_flow(
             flow_function,