Repository: PrefectHQ/prefect

Issue:
`prefect deployment build` silently accepts wrong block types 
### 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

Deployment build silently completes but `flow-script-storage` is not an infrastructure block:

```sh
❯ prefect deployment build ./project/flow.py:my -n someflow -ib s3/flow-script-storage
Found flow 'my'
Deployment YAML created at '/Users/.../project/my-deployment.yaml'.
```

... so when the deployment's flow runs are submitted

we get an `AttributeError` in agent logs, 

`S3 object has no attribute env`

### Reproduction

```python
# create an s3 storage block `flow-script-storage`
# create a minimal flow `my`

prefect deployment build ./project/flow.py:my -n someflow -ib s3/flow-script-storage
```


### Error

not applicable, the issue is that there is no error

### Versions

```

Version:             2.1.1
API version:         0.8.0
Python version:      3.10.5
Git commit:          dc2ba222
Built:               Thu, Aug 18, 2022 10:18 AM
OS/Arch:             darwin/x86_64
Profile:             myplace
Server type:         hosted
```


### Additional context

_No response_


Discussion:
Thanks for the issue! We need to introduce some utilities for block capability validation and enforcement.

Resolving patch:
diff --git a/src/prefect/deployments.py b/src/prefect/deployments.py
--- a/src/prefect/deployments.py
+++ b/src/prefect/deployments.py
@@ -216,6 +216,10 @@ def _yaml_dict(self) -> dict:
             all_fields["storage"][
                 "_block_type_slug"
             ] = self.storage.get_block_type_slug()
+        if all_fields["infrastructure"]:
+            all_fields["infrastructure"][
+                "_block_type_slug"
+            ] = self.infrastructure.get_block_type_slug()
         return all_fields
 
     # top level metadata
@@ -266,12 +270,38 @@ def _yaml_dict(self) -> dict:
         description="The parameter schema of the flow, including defaults.",
     )
 
+    @validator("infrastructure", pre=True)
+    def infrastructure_must_have_capabilities(cls, value):
+        if isinstance(value, dict):
+            block_type = lookup_type(Block, value.pop("_block_type_slug"))
+            block = block_type(**value)
+        elif value is None:
+            return value
+        else:
+            block = value
+
+        if "run-infrastructure" not in block.get_block_capabilities():
+            raise ValueError(
+                "Infrastructure block must have 'run-infrastructure' capabilities."
+            )
+        return block
+
     @validator("storage", pre=True)
-    def cast_storage_to_block_type(cls, value):
+    def storage_must_have_capabilities(cls, value):
         if isinstance(value, dict):
-            block = lookup_type(Block, value.pop("_block_type_slug"))
-            return block(**value)
-        return value
+            block_type = lookup_type(Block, value.pop("_block_type_slug"))
+            block = block_type(**value)
+        elif value is None:
+            return value
+        else:
+            block = value
+
+        capabilities = block.get_block_capabilities()
+        if "get-directory" not in capabilities and "put-directory" not in capabilities:
+            raise ValueError(
+                "Remote Storage block must have both 'get-directory' and 'put-directory' capabilities."
+            )
+        return block
 
     @validator("parameter_openapi_schema", pre=True)
     def handle_openapi_schema(cls, value):
diff --git a/src/prefect/filesystems.py b/src/prefect/filesystems.py
--- a/src/prefect/filesystems.py
+++ b/src/prefect/filesystems.py
@@ -25,14 +25,44 @@ async def read_path(self, path: str) -> bytes:
 
 
 class WritableFileSystem(Block, abc.ABC):
-    _block_schema_capabilities = ["write-path"]
+    _block_schema_capabilities = ["read-path", "write-path"]
+
+    @abc.abstractmethod
+    async def read_path(self, path: str) -> bytes:
+        pass
 
     @abc.abstractmethod
     async def write_path(self, path: str, content: bytes) -> None:
         pass
 
 
-class LocalFileSystem(ReadableFileSystem, WritableFileSystem):
+class ReadableDeploymentStorage(Block, abc.ABC):
+    _block_schema_capabilities = ["get-directory"]
+
+    @abc.abstractmethod
+    async def get_directory(
+        self, from_path: str = None, local_path: str = None
+    ) -> None:
+        pass
+
+
+class WritableDeploymentStorage(Block, abc.ABC):
+    _block_schema_capabilities = ["get-directory", "put-directory"]
+
+    @abc.abstractmethod
+    async def get_directory(
+        self, from_path: str = None, local_path: str = None
+    ) -> None:
+        pass
+
+    @abc.abstractmethod
+    async def put_directory(
+        self, local_path: str = None, to_path: str = None, ignore_file: str = None
+    ) -> None:
+        pass
+
+
+class LocalFileSystem(WritableFileSystem, WritableDeploymentStorage):
     """
     Store data as a file on a local file system.
 
@@ -171,7 +201,7 @@ async def write_path(self, path: str, content: bytes) -> str:
             await f.write(content)
 
 
-class RemoteFileSystem(ReadableFileSystem, WritableFileSystem):
+class RemoteFileSystem(WritableFileSystem, WritableDeploymentStorage):
     """
     Store data as a file on a remote file system.
 
@@ -342,7 +372,7 @@ def filesystem(self) -> fsspec.AbstractFileSystem:
         return self._filesystem
 
 
-class S3(ReadableFileSystem, WritableFileSystem):
+class S3(WritableFileSystem, WritableDeploymentStorage):
     """
     Store data as a file on AWS S3.
 
@@ -426,7 +456,7 @@ async def write_path(self, path: str, content: bytes) -> str:
         return await self.filesystem.write_path(path=path, content=content)
 
 
-class GCS(ReadableFileSystem, WritableFileSystem):
+class GCS(WritableFileSystem, WritableDeploymentStorage):
     """
     Store data as a file on Google Cloud Storage.
 
@@ -507,7 +537,7 @@ async def write_path(self, path: str, content: bytes) -> str:
         return await self.filesystem.write_path(path=path, content=content)
 
 
-class Azure(ReadableFileSystem, WritableFileSystem):
+class Azure(WritableFileSystem, WritableDeploymentStorage):
     """
     Store data as a file on Azure Datalake and Azure Blob Storage.
 
@@ -601,7 +631,7 @@ async def write_path(self, path: str, content: bytes) -> str:
         return await self.filesystem.write_path(path=path, content=content)
 
 
-class SMB(ReadableFileSystem, WritableFileSystem):
+class SMB(WritableFileSystem, WritableDeploymentStorage):
     """
     Store data as a file on a SMB share.
 


Test patch:
diff --git a/tests/test_deployments.py b/tests/test_deployments.py
--- a/tests/test_deployments.py
+++ b/tests/test_deployments.py
@@ -3,6 +3,7 @@
 
 from prefect.blocks.core import Block
 from prefect.deployments import Deployment
+from prefect.filesystems import LocalFileSystem
 from prefect.infrastructure import Process
 
 
@@ -11,6 +12,18 @@ async def test_that_name_is_required(self):
         with pytest.raises(ValidationError, match="field required"):
             Deployment()
 
+    async def test_that_infra_block_capabilities_are_validated(self):
+        bad_infra = LocalFileSystem(basepath=".")
+
+        with pytest.raises(ValueError, match="'run-infrastructure' capabilities"):
+            Deployment(name="foo", infrastructure=bad_infra)
+
+    async def test_that_storage_block_capabilities_are_validated(self):
+        bad_storage = Process()
+
+        with pytest.raises(ValueError, match="capabilities"):
+            Deployment(name="foo", storage=bad_storage)
+
     async def test_that_infrastructure_defaults_to_process(self):
         d = Deployment(name="foo")
         assert isinstance(d.infrastructure, Process)
@@ -177,6 +190,27 @@ async def test_build_from_flow_sets_provided_attrs(self, flow_function):
 
 
 class TestYAML:
+    def test_deployment_yaml_roundtrip(self, tmp_path):
+        storage = LocalFileSystem(basepath=".")
+        infrastructure = Process()
+
+        d = Deployment(
+            name="yaml",
+            flow_name="test",
+            storage=storage,
+            infrastructure=infrastructure,
+            tags=["A", "B"],
+        )
+        yaml_path = str(tmp_path / "dep.yaml")
+        d.to_yaml(yaml_path)
+
+        new_d = Deployment.load_from_yaml(yaml_path)
+        assert new_d.name == "yaml"
+        assert new_d.tags == ["A", "B"]
+        assert new_d.flow_name == "test"
+        assert new_d.storage == storage
+        assert new_d.infrastructure == infrastructure
+
     def test_yaml_comment_for_work_queue(self, tmp_path):
         d = Deployment(name="yaml", flow_name="test")
         yaml_path = str(tmp_path / "dep.yaml")