Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

File I/O Abstraction (Part I) #15568

Merged
merged 7 commits into from
Nov 14, 2024
Merged

File I/O Abstraction (Part I) #15568

merged 7 commits into from
Nov 14, 2024

Conversation

shenglol
Copy link
Contributor

@shenglol shenglol commented Nov 11, 2024

This is the first PR in a series of changes to introduce a file I/O abstraction layer to the Bicep project. Currently, Bicep.Core heavily relies on the large IFileSystem interface, System.Uri. This tight coupling introduces several challenges, such as making it difficult for the Bicep resource provider to supply a mock implementation of IFileSystem. Additionally, it prevents support for VS Code virtual workspaces because System.Uri is surprisingly not fully RFC-compliant (see: #11467).

To address these issues, a higher-level abstraction for file I/O is required. Given the broad scope of this refactoring, this PR focuses on laying the groundwork by introducing the new abstraction layer and using it to load configuration. Subsequent PRs will replace the usage of IFileSystem and System.Uri throughout Bicep.Core and tidy up the IFileResolver interface with the new APIs.

Key Changes in this PR

The core of this PR is the introduction of a new Bicep.IO project. This project defines the new file I/O abstraction layer and includes:

  • Bicep.IO.Abstraction types:
    • IFileExplorer: Encapsulates directory and file discovery.
    • IFileHandle: A pointer to a file, abstracting operations like reading or writing.
    • IDirectoryHandle: A pointer to a directory for managing directory traversal.
    • ResourceIdentifier: An RFC3986 compliant URI (without Query and Fragment) which is going to replace System.Uri in Bicep.Core.
  • Bicep.IO.FileSystem types:
    • Concrete implementations of the above interfaces based on IFileSystem.

The ultimate objective is to phase out direct usage of Bicep.IO.FileSystem within Bicep.Core and restrict it to referencing only Bicep.IO.Abstraction via BannedSymbols.txt. This design ensures cleaner separation of concerns and enables better testing, security, and support for virtual environments.

Below is an architecture diagram illustrating the new design and how the layers interact:

Untitled
Microsoft Reviewers: Open in CodeFlow

Copy link
Contributor

github-actions bot commented Nov 12, 2024

Test this change out locally with the following install scripts (Action run 11845655422)

VSCode
  • Mac/Linux
    bash <(curl -Ls https://aka.ms/bicep/nightly-vsix.sh) --run-id 11845655422
  • Windows
    iex "& { $(irm https://aka.ms/bicep/nightly-vsix.ps1) } -RunId 11845655422"
Azure CLI
  • Mac/Linux
    bash <(curl -Ls https://aka.ms/bicep/nightly-cli.sh) --run-id 11845655422
  • Windows
    iex "& { $(irm https://aka.ms/bicep/nightly-cli.ps1) } -RunId 11845655422"

Copy link
Contributor

github-actions bot commented Nov 12, 2024

Dotnet Test Results

    78 files   -     30      78 suites   - 30   29m 31s ⏱️ - 10m 29s
11 410 tests +     2  11 410 ✅ +     2  0 💤 ±0  0 ❌ ±0 
26 551 runs   - 13 190  26 551 ✅  - 13 190  0 💤 ±0  0 ❌ ±0 

Results for commit 0d25ac3. ± Comparison against base commit 4a382ed.

This pull request removes 1841 and adds 659 tests. Note that renamed tests count towards both.

		nestedProp1: 1
		nestedProp2: 2
		prop1: true
		prop2: false
	1
	2
	\$'")
	prop1: true
	prop2: false
…
Bicep.Core.IntegrationTests.AzTypesViaRegistryTests ‑ Bicep_compiler_handles_corrupted_extension_package_gracefully (\u001f�\u0008\u0000\u0000\u0000\u0000\u0000\u0000
�ӽ
� \u0010\u0007p�>E��xzj2d��W�6�\u000fbJLh���k�B\u0007C�4\u0016�o�8<O�4ߙq[�C�9
�\u00179%Kc�B\u000c�O@\u0002\u0001��\u0008�\u00125!ٸ�$\u0001��M�GY�\u001fċ�����@\u000b\u0010\u001aE�(�\u0012Q�\u001b_��� ���X
�bۛ�g�ک��k���$������O�g�\u0013@
(�B�>������L�y���\u0013$I�$1<\u0001�x\u000b�\u0000\u000c\u0000\u0000,"The path: index.json was not found in artifact contents")
Bicep.Core.IntegrationTests.AzTypesViaRegistryTests ‑ Bicep_compiler_handles_corrupted_extension_package_gracefully (\u001f�\u0008\u0000\u0000\u0000\u0000\u0000\u0000
�Խ
�0\u0010\u0007��>E�\u0003���
�.��\u000f\u0010�\u0013+�JS� ��� .-.�
�7�\u000erI���\u001bۮ��X;\u0006"IcF��=-e�z\u0007�!`�8�F)�M��'�qu���(���\u0007%)�MQ�\u0012�\u0000a�H\u0013�U
����eC5�Ӌm\u000eˢʱeGw���r�}�c\u0006\u0003^���+z��.$��\u0000H�h�4�Zi���J&�<�7\u001am+l/�5����a\u001d-�ޞ\u001cާ�-\u0008� ��\u0007U�R%\u0000\u000c\u0000\u0000,"Value cannot be null. (Parameter 'source')")
Bicep.Core.IntegrationTests.AzTypesViaRegistryTests ‑ Bicep_compiler_handles_corrupted_extension_package_gracefully (\u001f�\u0008\u0000\u0000\u0000\u0000\u0000\u0000
���\u000b�0\u0014\u0007��+������f���CFX\u0010t��B\u0006Z���?�<D\u0017��?��9n\u000f���}���z��8)J�<�\u0004��\u001a\u0012�u��@!��(�J\u0008S���;i�(+]�V�8�\u0007q\u001f�*͒�)�y
<�\u0013	ʗ
ff�ܵ�����K��qR�ky˛�|�kZ\u001d>�7�\u0013}�?��\u000c` �\u0004!\u0011�B�<�Q2����:at\n��is�E�\u0002�	<uO�eY��^�\u0011�\u0000\u000c\u0000\u0000,"'7' is an invalid end of a number. Expected a delimiter. Path: $.INVALID_JSON | LineNumber: 0 | BytePositionInLine: 20.")
Bicep.Core.IntegrationTests.AzTypesViaRegistryTests ‑ Bicep_compiler_handles_corrupted_extension_package_gracefully (\u001f�\u0008\u0000\u0000\u0000\u0000\u0000\u0000\u0003�ӽ
�0\u0010\u0007��}�<A�%\u0017�\u000e�\u001d�
�\u0015���hD���\u001b�B\u0007���B�\u001b�8�B��do�]a�E�\u0008-q�\u000867\u0019����\u0001\u0018`@/�I2��x?�%#���:��Į\u001f�2n��,r 
�PA*�d\u0019!mB�0�\u0003~������ΉKS�a��\u001aX��Ѩ���֎O��R1@\u00024*U0�\u001ft�#_$�����c�\u000b�(��5<\u0001́\U\u0000\u000c\u0000\u0000,"The path: index.json was not found in artifact contents")
Bicep.Core.IntegrationTests.AzTypesViaRegistryTests ‑ Bicep_compiler_handles_corrupted_extension_package_gracefully (\u001f�\u0008\u0000\u0000\u0000\u0000\u0000\u0000\u0003�Խ
�0\u0010\u0007��>E�\u0003��4\u001fV� 8X�*\u0008�\u0012l�
��V(�\uda58\udcf8���C0�19�\u000b�����z��8)J�Q�\u0004��\u001a����\u0006p�@�QD�\u0010\u0012!\�>I�GY��2F�\u001f��XWi�\u0004�<�\u0014g 	H�K\u00053sw�\u0003\u001f�uu	�<Njr-oyS�u�O�L�������-��qdv\u0000p�$\u0003��)�j<J&�<�O'���m�:m\u000e��Y`����ɲ,�\u001a�\u000bU���\u0000\u000c\u0000\u0000,"'7' is an invalid end of a number. Expected a delimiter. Path: $.INVALID_JSON | LineNumber: 0 | BytePositionInLine: 20.")
Bicep.Core.IntegrationTests.AzTypesViaRegistryTests ‑ Bicep_compiler_handles_corrupted_extension_package_gracefully (\u001f�\u0008\u0000\u0000\u0000\u0000\u0000\u0000\u0003��K
�0\u0010\u0006�=E�\u0001b�ͣ\u0015�w#��\u0000�\u001d�bki+\u0014Ļ�.�M��>\u0004�-3\u00033!�a�i�hR�j\u0016p\u0011�\u0019\u0019\u001b��\u0010��\u001d\u0010��~�k)\u0015!�\u001d}�\u001e��1�]e�Y?�\u000f�i�\u001cc�\u0001\u0004Z��\u0018�0�H�l-\u0019�ADKӜ�H�e��Vt��P���t\u0006|�o�\u0019���\u0003A�\u001f\u0000B��\u0007��%����%����\u0007�\u000e\u0005�%&
�;̏Xy\u001bz2�\u001a�K��8��L�\u0005� H(\u0000\u000c\u0000\u0000,"Value cannot be null. (Parameter 'source')")
Bicep.Core.IntegrationTests.AzTypesViaRegistryTests ‑ Repository_not_found_in_registry (ArtifactRegistryAddress { RegistryAddress = mcr.microsoft.com, RepositoryPath = unknown/path/az, ExtensionVersion = 0.0.0-placeholder },Azure.RequestFailedException: The artifact does not exist in the registry.
   at Bicep.Core.Registry.AzureContainerRegistryManager.DownloadManifestAndLayersAsync(IOciArtifactReference artifactReference, ContainerRegistryContentClient client) in /home/runner/work/bicep/bicep/src/Bicep.Core/Registry/AzureContainerRegistryManager.cs:line 138
   at Bicep.Core.Registry.AzureContainerRegistryManager.DownloadManifestAndLayersAsync(IOciArtifactReference artifactReference, ContainerRegistryContentClient client) in /home/runner/work/bicep/bicep/src/Bicep.Core/Registry/AzureContainerRegistryManager.cs:line 138,[(BCP192, Error, Unable to restore the artifact with reference "br:mcr.microsoft.com/unknown/path/az:0.0.0-placeholder": The artifact does not exist in the registry.)])
Bicep.Core.IntegrationTests.AzTypesViaRegistryTests ‑ Repository_not_found_in_registry (ArtifactRegistryAddress { RegistryAddress = mcr.microsoft.com, RepositoryPath = unknown/path/az, ExtensionVersion = 0.0.0-placeholder },Azure.RequestFailedException: The artifact does not exist in the registry.
   at Bicep.Core.Registry.AzureContainerRegistryManager.DownloadManifestAndLayersAsync(IOciArtifactReference artifactReference, ContainerRegistryContentClient client) in D:\a\bicep\bicep\src\Bicep.Core\Registry\AzureContainerRegistryManager.cs:line 138
   at Bicep.Core.Registry.AzureContainerRegistryManager.DownloadManifestAndLayersAsync(IOciArtifactReference artifactReference, ContainerRegistryContentClient client) in D:\a\bicep\bicep\src\Bicep.Core\Registry\AzureContainerRegistryManager.cs:line 138,[(BCP192, Error, Unable to restore the artifact with reference "br:mcr.microsoft.com/unknown/path/az:0.0.0-placeholder": The artifact does not exist in the registry.)])
Bicep.Core.IntegrationTests.AzTypesViaRegistryTests ‑ Repository_not_found_in_registry (ArtifactRegistryAddress { RegistryAddress = unknown.registry.azurecr.io, RepositoryPath = bicep/extensions/az, ExtensionVersion = 0.0.0-placeholder },System.AggregateException: Retry failed after 4 tries. Retry settings can be adjusted in ClientOptions.Retry or by configuring a custom retry policy in ClientOptions.RetryPolicy. (No such host is known. (unknown.registry.azurecr.io:443)) (No such host is known. (unknown.registry.azurecr.io:443)) (No such host is known. (unknown.registry.azurecr.io:443)) (No such host is known. (unknown.registry.azurecr.io:443))
   at Bicep.Core.Registry.AzureContainerRegistryManager.DownloadManifestAndLayersAsync(IOciArtifactReference artifactReference, ContainerRegistryContentClient client) in /home/runner/work/bicep/bicep/src/Bicep.Core/Registry/AzureContainerRegistryManager.cs:line 138
   at Bicep.Core.Registry.AzureContainerRegistryManager.<>c__DisplayClass4_0.<<PullArtifactAsync>g__DownloadManifestInternalAsync|0>d.MoveNext() in /home/runner/work/bicep/bicep/src/Bicep.Core/Registry/AzureContainerRegistryManager.cs:line 44
--- End of stack trace from previous location ---
   at Bicep.Core.Registry.AzureContainerRegistryManager.PullArtifactAsync(RootConfiguration configuration, IOciArtifactReference artifactReference) in /home/runner/work/bicep/bicep/src/Bicep.Core/Registry/AzureContainerRegistryManager.cs:line 51
   at Bicep.Core.Registry.AzureContainerRegistryManager.DownloadManifestAndLayersAsync(IOciArtifactReference artifactReference, ContainerRegistryContentClient client) in /home/runner/work/bicep/bicep/src/Bicep.Core/Registry/AzureContainerRegistryManager.cs:line 138
   at Bicep.Core.Registry.AzureContainerRegistryManager.<>c__DisplayClass4_0.<<PullArtifactAsync>g__DownloadManifestInternalAsync|0>d.MoveNext() in /home/runner/work/bicep/bicep/src/Bicep.Core/Registry/AzureContainerRegistryManager.cs:line 44
--- End of stack trace from previous location ---
   at Bicep.Core.Registry.AzureContainerRegistryManager.PullArtifactAsync(RootConfiguration configuration, IOciArtifactReference artifactReference) in /home/runner/work/bicep/bicep/src/Bicep.Core/Registry/AzureContainerRegistryManager.cs:line 63
   at Bicep.Core.Registry.OciArtifactRegistry.TryRestoreArtifactAsync(RootConfiguration configuration, OciArtifactReference reference) in /home/runner/work/bicep/bicep/src/Bicep.Core/Registry/OciArtifactRegistry.cs:line 499,[(BCP192, Error, Unable to restore the artifact with reference "br:unknown.registry.azurecr.io/bicep/extensions/az:0.0.0-placeholder": Retry failed after 4 tries. Retry settings can be adjusted in ClientOptions.Retry or by configuring a custom retry policy in ClientOptions.RetryPolicy. (No such host is known. (unknown.registry.azurecr.io:443)) (No such host is known. (unknown.registry.azurecr.io:443)) (No such host is known. (unknown.registry.azurecr.io:443)) (No such host is known. (unknown.registry.azurecr.io:443)))])
Bicep.Core.IntegrationTests.AzTypesViaRegistryTests ‑ Repository_not_found_in_registry (ArtifactRegistryAddress { RegistryAddress = unknown.registry.azurecr.io, RepositoryPath = bicep/extensions/az, ExtensionVersion = 0.0.0-placeholder },System.AggregateException: Retry failed after 4 tries. Retry settings can be adjusted in ClientOptions.Retry or by configuring a custom retry policy in ClientOptions.RetryPolicy. (No such host is known. (unknown.registry.azurecr.io:443)) (No such host is known. (unknown.registry.azurecr.io:443)) (No such host is known. (unknown.registry.azurecr.io:443)) (No such host is known. (unknown.registry.azurecr.io:443))
   at Bicep.Core.Registry.AzureContainerRegistryManager.DownloadManifestAndLayersAsync(IOciArtifactReference artifactReference, ContainerRegistryContentClient client) in D:\a\bicep\bicep\src\Bicep.Core\Registry\AzureContainerRegistryManager.cs:line 138
   at Bicep.Core.Registry.AzureContainerRegistryManager.<>c__DisplayClass4_0.<<PullArtifactAsync>g__DownloadManifestInternalAsync|0>d.MoveNext() in D:\a\bicep\bicep\src\Bicep.Core\Registry\AzureContainerRegistryManager.cs:line 44
--- End of stack trace from previous location ---
   at Bicep.Core.Registry.AzureContainerRegistryManager.PullArtifactAsync(RootConfiguration configuration, IOciArtifactReference artifactReference) in D:\a\bicep\bicep\src\Bicep.Core\Registry\AzureContainerRegistryManager.cs:line 51
   at Bicep.Core.Registry.AzureContainerRegistryManager.DownloadManifestAndLayersAsync(IOciArtifactReference artifactReference, ContainerRegistryContentClient client) in D:\a\bicep\bicep\src\Bicep.Core\Registry\AzureContainerRegistryManager.cs:line 138
   at Bicep.Core.Registry.AzureContainerRegistryManager.<>c__DisplayClass4_0.<<PullArtifactAsync>g__DownloadManifestInternalAsync|0>d.MoveNext() in D:\a\bicep\bicep\src\Bicep.Core\Registry\AzureContainerRegistryManager.cs:line 44
--- End of stack trace from previous location ---
   at Bicep.Core.Registry.AzureContainerRegistryManager.PullArtifactAsync(RootConfiguration configuration, IOciArtifactReference artifactReference) in D:\a\bicep\bicep\src\Bicep.Core\Registry\AzureContainerRegistryManager.cs:line 63
   at Bicep.Core.Registry.OciArtifactRegistry.TryRestoreArtifactAsync(RootConfiguration configuration, OciArtifactReference reference) in D:\a\bicep\bicep\src\Bicep.Core\Registry\OciArtifactRegistry.cs:line 499,[(BCP192, Error, Unable to restore the artifact with reference "br:unknown.registry.azurecr.io/bicep/extensions/az:0.0.0-placeholder": Retry failed after 4 tries. Retry settings can be adjusted in ClientOptions.Retry or by configuring a custom retry policy in ClientOptions.RetryPolicy. (No such host is known. (unknown.registry.azurecr.io:443)) (No such host is known. (unknown.registry.azurecr.io:443)) (No such host is known. (unknown.registry.azurecr.io:443)) (No such host is known. (unknown.registry.azurecr.io:443)))])
…

♻️ This comment has been updated with latest results.

</PropertyGroup>

<ItemGroup>
<PackageReference Include="System.IO.Abstractions" Version="21.0.29" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this to facilitate the refactor, or something we see sticking around long-term?

Copy link
Contributor Author

@shenglol shenglol Nov 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

System.IO.Abstraction will still be around and used for mocking the file system in tests. However, I plan to restrict references to the namespace within Bicep.Core.

{
public static class GlobalSettings
{
public static bool PathCaseSensitive { get; set; } = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the same as the .NET implementation? AFAIK there are exceptions to this - e.g. you can choose either case-sensitive or case-insensitive on OSX.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is same as Omnisharp's DocumentUri implementation. System.Uri doesn’t implement IEquatable, and that's why we have custom path comparers in Bicep.Core in a few places, which is not ideal. Speaking of exceptions, technically, case-insensitive and case-senstitive file systems can exist on all OS platforms, depending on what file system the user has chosen to use. However, there’s no reliable way to detect it. Given these limitations, I believe a heuristic based on the OS platform is the most practical solution.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can make portions of paths case-sensitive in NTFS, too, so getting this perfect is a mug's game.

When do we need to know if a path is case sensitive or not? Presumably this is handled by the OS when we try to open the file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to address path case sensitivity when resolving module paths during module instantiation to prevent duplicate models for the same source file.

public bool Equals(ResourceIdentifier other) =>
string.Equals(Scheme, other.Scheme, StringComparison.Ordinal) &&
string.Equals(Authority, other.Authority, StringComparison.Ordinal) &&
string.Equals(Path, other.Path, GlobalSettings.PathComparison);
Copy link
Member

@anthony-c-martin anthony-c-martin Nov 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the behavior between OSes be the same unless scheme is file?

Even this could be tricky - I wonder if it's worth making the OS-specific behavior ultra-clear and e.g. making a specific method for this, rather than baking it into the default equality comparer?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah good catch. Let me fix it.

Copy link
Contributor Author

@shenglol shenglol Nov 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it's worth making the OS-specific behavior ultra-clear and e.g. making a specific method for this, rather than baking it into the default equality comparer.

It's a long story...but that's what I implemented originally where I put the logic in Bicep.Core.FileSystem (this is essentially what rfc8089 section 2 suggests). While this design was conceptually better because it avoided leaking abstractions, it ironically ended up complicating the code by a lot. It also made it more difficult to replace System.Uri in Bicep.Core and eliminate PathHelper.PathComparer.

Additionally, I later discovered that MockFileSystem accounts for the OS platform, which would make the test code more complex if we followed this approach.

This current solution is definitely not perfect, but it's a compromise that simplifies the implementation. I'd be open to revisiting the design once the entire migration is complete. For example, there might be an opportunity to get it right if BicepSourceFile doesn’t hold a direct reference to ResourceIdentifier but instead uses a IFileHandle.

{
IDirectoryHandle GetParent();

Stream OpenRead();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@anthony-c-martin I looked into using BinaryData as the return type. While I appreciate its cleaner abstraction compared to Stream, I have some performance concerns since it always copies the data into a memory stream when created from a stream. I think it might be better to implement extension methods for reading and writing with BinaryData.

Copy link
Member

@majastrz majastrz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:shipit:

@shenglol shenglol merged commit 4b190e2 into main Nov 14, 2024
47 checks passed
@shenglol shenglol deleted the shenglol/file-io-abstraction branch November 14, 2024 21:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants