├── BicepNet.Core ├── Azure │ ├── RoleDefinitionType.cs │ ├── ChildResourceType.cs │ ├── ManagementGroupHelper.cs │ ├── SubscriptionHelper.cs │ ├── AzureHelpers.cs │ ├── RoleHelper.cs │ ├── AzureResourceProvider.cs │ └── PolicyHelper.cs ├── Configuration │ ├── BicepConfigMode.cs │ ├── BicepNetExtensions.cs │ ├── bicepconfig.json │ ├── BicepNetConfigurationManager.Custom.cs │ └── BicepNetConfigurationManager.Clone.cs ├── Models │ ├── BicepConfigInfo.cs │ ├── BicepRepository.cs │ ├── BicepRepositoryModuleTag.cs │ └── BicepRepositoryModule.cs ├── Properties │ ├── launchSettings.json │ └── PublishProfiles │ │ └── FolderProfile.pubxml ├── Authentication │ ├── BicepAccessToken.cs │ ├── ExternalTokenCredential.cs │ └── BicepNetTokenCredentialFactory.cs ├── BicepHelper.cs ├── BicepWrapper.Format.cs ├── BicepWrapper.Decompile.cs ├── BicepNet.Core.csproj ├── BicepWrapper.ExportChildResources.cs ├── BicepWrapper.ConvertResourceToBicep.cs ├── BicepWrapper.Restore.cs ├── BicepWrapper.ExportResource.cs ├── BicepWrapper.Build.cs ├── BicepWrapper.Publish.cs ├── BicepWrapper.cs └── BicepWrapper.FindModule.cs ├── .gitattributes ├── .github ├── workflows │ ├── dependabot_hack.yml │ ├── build.yml │ └── codeql.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── actions │ ├── test-linux │ │ └── action.yml │ ├── build │ │ └── action.yml │ └── code-coverage │ │ └── action.yml └── PULL_REQUEST_TEMPLATE.md ├── BicepNet.PS ├── Commands │ ├── GetBicepNetVersionCommand.cs │ ├── ClearBicepNetCredentialCommand.cs │ ├── GetBicepNetAccessTokenCommand.cs │ ├── RestoreBicepNetFileCommand.cs │ ├── BicepNetBaseCommand.PSCmdlet.cs │ ├── ConvertToBicepNetFileCommand.cs │ ├── ConvertResourceToBicepCommand.cs │ ├── BuildBicepNetFileCommand.cs │ ├── ExportBicepNetChildResourcesCommand.cs │ ├── BuildBicepNetParamFileCommand.cs │ ├── PublishBicepNetFileCommand.cs │ ├── GetBicepNetCachePathCommand.cs │ ├── ExportBicepNetResourceCommand.cs │ ├── SetBicepNetCredentialCommand.cs │ ├── FindBicepNetModuleCommand.cs │ ├── GetBicepNetConfigCommand.cs │ ├── FormatBicepNetFile.cs │ └── BicepNetBaseCommand.ILogger.cs ├── BicepNet.PS.csproj ├── tests │ └── Module.tests.ps1 ├── LoadContext │ ├── ModuleInitializer.cs │ └── DependencyAssemblyLoadContext.cs └── BicepNet.PS.psd1 ├── CHANGELOG.md ├── BicepNet.CLI ├── BicepNet.CLI.csproj └── Program.cs ├── GitVersion.yml ├── RequiredModules.psd1 ├── codecov.yml ├── .build └── tasks │ ├── BicepNet.generateTestCases.build.ps1 │ └── BicepNet.build.ps1 ├── LICENSE ├── .vscode ├── launch.json └── tasks.json ├── SECURITY.md ├── README.md ├── .devcontainer └── devcontainer.json ├── Resolve-Dependency.psd1 ├── BicepNet.sln ├── CONTRIBUTING.md ├── .gitignore └── Resolve-Dependency.ps1 /BicepNet.Core/Azure/RoleDefinitionType.cs: -------------------------------------------------------------------------------- 1 | namespace BicepNet.Core.Azure; 2 | 3 | public enum RoleDefinitionType 4 | { 5 | BuiltInRole, 6 | CustomRole, 7 | } -------------------------------------------------------------------------------- /BicepNet.Core/Configuration/BicepConfigMode.cs: -------------------------------------------------------------------------------- 1 | namespace BicepNet.Core.Configuration; 2 | 3 | public enum BicepConfigScope 4 | { 5 | Merged, 6 | Default, 7 | Local 8 | } 9 | -------------------------------------------------------------------------------- /BicepNet.Core/Azure/ChildResourceType.cs: -------------------------------------------------------------------------------- 1 | namespace BicepNet.Core.Azure; 2 | 3 | public enum ChildResourceType 4 | { 5 | PolicyDefinitions, 6 | PolicyInitiatives, 7 | PolicyAssignments, 8 | RoleDefinitions, 9 | RoleAssignments, 10 | Subscriptions, 11 | ResourceGroups, 12 | } -------------------------------------------------------------------------------- /BicepNet.Core/Models/BicepConfigInfo.cs: -------------------------------------------------------------------------------- 1 | namespace BicepNet.Core.Models; 2 | 3 | public class BicepConfigInfo(string path, string config) 4 | { 5 | public string Path { get; } = path; 6 | public string Config { get; } = config; 7 | 8 | public override string ToString() => Path; 9 | } 10 | -------------------------------------------------------------------------------- /BicepNet.Core/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "BicepNet.Core": { 4 | "commandName": "Project", 5 | "commandLineArgs": "exportresource /providers/Microsoft.Management/managementGroups/EsLZ/providers/Microsoft.Authorization/policyDefinitions/Append-AppService-httpsonly" 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Needed for publishing of examples, build worker defaults to core.autocrlf=input. 2 | * text eol=autocrlf 3 | 4 | *.mof text eol=crlf 5 | *.sh text eol=lf 6 | *.svg eol=lf 7 | 8 | # Ensure any exe files are treated as binary 9 | *.exe binary 10 | *.jpg binary 11 | *.xl* binary 12 | *.pfx binary 13 | *.png binary 14 | *.dll binary 15 | *.so binary 16 | -------------------------------------------------------------------------------- /.github/workflows/dependabot_hack.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Dependabot hack 3 | on: # yamllint disable-line rule:truthy 4 | push: 5 | branches: 6 | - never-trigger-this-dependabot-hack-workflow 7 | 8 | jobs: 9 | 10 | dependabot_hack: 11 | name: Ensure dependabot version checks 12 | runs-on: ubuntu-20.04 13 | steps: 14 | # Dockerfile: 15 | - uses: azure/bicep@v0.10.61 -------------------------------------------------------------------------------- /BicepNet.Core/Authentication/BicepAccessToken.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BicepNet.Core.Authentication; 4 | 5 | public class BicepAccessToken(string token, DateTimeOffset expiresOn) 6 | { 7 | public string Token { get; set; } = token; 8 | public DateTimeOffset ExpiresOn { get; set; } = expiresOn; 9 | 10 | public override string ToString() 11 | { 12 | return Token; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /BicepNet.Core/Models/BicepRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace BicepNet.Core.Models; 4 | 5 | public class BicepRepository(string name, string endpoint) 6 | { 7 | public string Name { get; } = name; 8 | public string Endpoint { get; } = endpoint; 9 | public IList ModuleVersions { get; set; } = []; 10 | 11 | public override string ToString() => Name; 12 | } 13 | -------------------------------------------------------------------------------- /BicepNet.PS/Commands/GetBicepNetVersionCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace BicepNet.PS.Commands; 4 | 5 | [Cmdlet(VerbsCommon.Get, "BicepNetVersion")] 6 | [CmdletBinding] 7 | public class GetBicepNetVersionCommand : BicepNetBaseCommand 8 | { 9 | protected override void EndProcessing() 10 | { 11 | var result = bicepWrapper.BicepVersion; 12 | WriteObject(result); 13 | } 14 | } -------------------------------------------------------------------------------- /BicepNet.PS/Commands/ClearBicepNetCredentialCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace BicepNet.PS.Commands; 4 | 5 | [Cmdlet(VerbsCommon.Clear, "BicepNetCredential")] 6 | [CmdletBinding()] 7 | public class ClearBicepNetCredentialCommand : BicepNetBaseCommand 8 | { 9 | protected override void BeginProcessing() 10 | { 11 | base.BeginProcessing(); 12 | bicepWrapper.ClearAuthentication(); 13 | } 14 | } -------------------------------------------------------------------------------- /BicepNet.PS/Commands/GetBicepNetAccessTokenCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace BicepNet.PS.Commands; 4 | 5 | [Cmdlet(VerbsCommon.Get, "BicepNetAccessToken")] 6 | [CmdletBinding()] 7 | public class GetBicepNetAccessTokenCommand : BicepNetBaseCommand 8 | { 9 | protected override void BeginProcessing() 10 | { 11 | base.BeginProcessing(); 12 | WriteObject(bicepWrapper.GetAccessToken()); 13 | } 14 | } -------------------------------------------------------------------------------- /BicepNet.PS/Commands/RestoreBicepNetFileCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace BicepNet.PS.Commands; 4 | 5 | [Cmdlet(VerbsData.Restore, "BicepNetFile")] 6 | public class RestoreBicepNetFileCommand : BicepNetBaseCommand 7 | { 8 | [Parameter(Mandatory = true, ValueFromPipeline = true)] 9 | [ValidateNotNullOrEmpty] 10 | public string Path { get; set; } 11 | 12 | protected override void ProcessRecord() 13 | { 14 | bicepWrapper.Restore(Path); 15 | } 16 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog for BicepNet.PS 2 | 3 | The format is based on and uses the types of changes according to [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 4 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 5 | 6 | ## [Unreleased] 7 | 8 | ### Added 9 | 10 | - Support for Bicep v0.27.1. 11 | 12 | ### Security 13 | 14 | - Added `global.json` to match Bicep project build config. This allows the project to build without .NET 8 vulnerability scanning. 15 | -------------------------------------------------------------------------------- /BicepNet.Core/Properties/PublishProfiles/FolderProfile.pubxml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Release 8 | Any CPU 9 | bin\Release\net5.0\publish\ 10 | FileSystem 11 | 12 | -------------------------------------------------------------------------------- /BicepNet.PS/Commands/BicepNetBaseCommand.PSCmdlet.cs: -------------------------------------------------------------------------------- 1 | using BicepNet.Core; 2 | using System.Management.Automation; 3 | 4 | namespace BicepNet.PS.Commands; 5 | 6 | public partial class BicepNetBaseCommand : PSCmdlet 7 | { 8 | protected string name; 9 | protected BicepWrapper bicepWrapper; 10 | 11 | protected override void BeginProcessing() 12 | { 13 | base.BeginProcessing(); 14 | bicepWrapper = new BicepWrapper(this); 15 | name = MyInvocation.InvocationName; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /BicepNet.PS/BicepNet.PS.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /BicepNet.PS/Commands/ConvertToBicepNetFileCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace BicepNet.PS.Commands; 4 | 5 | [Cmdlet(VerbsData.ConvertTo, "BicepNetFile")] 6 | public class ConvertToBicepNetFile : BicepNetBaseCommand 7 | { 8 | [Parameter(Mandatory = true, ValueFromPipeline = true)] 9 | [ValidateNotNullOrEmpty] 10 | public string Path { get; set; } 11 | 12 | protected override void ProcessRecord() 13 | { 14 | var result = bicepWrapper.Decompile(Path); 15 | WriteObject(result); 16 | } 17 | } -------------------------------------------------------------------------------- /BicepNet.CLI/BicepNet.CLI.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /BicepNet.Core/Models/BicepRepositoryModuleTag.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace BicepNet.Core.Models; 4 | 5 | public class BicepRepositoryModuleTag(string name, string digest, string target, DateTimeOffset createdOn, DateTimeOffset updatedOn) 6 | { 7 | public string Name { get; } = name; 8 | public string Digest { get; } = digest; 9 | public string Target { get; } = target; 10 | 11 | public DateTimeOffset CreatedOn { get; } = createdOn; 12 | public DateTimeOffset UpdatedOn { get; } = updatedOn; 13 | 14 | public override string ToString() => Name; 15 | } 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Additional context** 24 | Add any other context about the problem here. 25 | -------------------------------------------------------------------------------- /BicepNet.PS/Commands/ConvertResourceToBicepCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace BicepNet.PS.Commands; 4 | 5 | [Cmdlet(VerbsData.Convert, "BicepNetResourceToBicep")] 6 | public class ConvertBicepNetResourceToBicep : BicepNetBaseCommand 7 | { 8 | [Parameter(Mandatory = true)] 9 | [ValidateNotNullOrEmpty] 10 | public string ResourceId { get; set; } 11 | 12 | [Parameter(Mandatory = true)] 13 | [ValidateNotNullOrEmpty] 14 | public string ResourceBody { get; set; } 15 | 16 | protected override void ProcessRecord() 17 | { 18 | var result = bicepWrapper.ConvertResourceToBicep(ResourceId, ResourceBody); 19 | WriteObject(result); 20 | } 21 | } -------------------------------------------------------------------------------- /BicepNet.PS/Commands/BuildBicepNetFileCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace BicepNet.PS.Commands; 4 | 5 | [Cmdlet(VerbsLifecycle.Build, "BicepNetFile")] 6 | public class BuildBicepNetFileCommand : BicepNetBaseCommand 7 | { 8 | [Parameter(Mandatory = true, ValueFromPipeline = true)] 9 | [ValidateNotNullOrEmpty] 10 | public string Path { get; set; } 11 | 12 | [Parameter()] 13 | public SwitchParameter NoRestore { get; set; } 14 | 15 | protected override void ProcessRecord() 16 | { 17 | var result = bicepWrapper.Build(Path, noRestore: NoRestore.IsPresent); 18 | foreach (var item in result) 19 | { 20 | WriteObject(item); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /BicepNet.PS/Commands/ExportBicepNetChildResourcesCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace BicepNet.PS.Commands; 4 | 5 | [Cmdlet(VerbsData.Export, "BicepNetChildResource")] 6 | public class ExportBicepNetChildResourceCommand : BicepNetBaseCommand 7 | { 8 | [Parameter(Mandatory = true, ValueFromPipeline = true)] 9 | [ValidateNotNullOrEmpty] 10 | public string ParentResourceId { get; set; } 11 | 12 | [Parameter(Mandatory = false)] 13 | public SwitchParameter IncludeTargetScope { get; set; } 14 | 15 | protected override void ProcessRecord() 16 | { 17 | var result = bicepWrapper.ExportChildResoures(ParentResourceId, includeTargetScope: IncludeTargetScope.IsPresent); 18 | WriteObject(result); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /GitVersion.yml: -------------------------------------------------------------------------------- 1 | mode: Mainline 2 | next-version: 0.0.1 3 | major-version-bump-message: '(breaking\schange|breaking|major)\b' 4 | minor-version-bump-message: '(adds?|features?|minor)\b' 5 | patch-version-bump-message: '\s?(fix|patch)' 6 | no-bump-message: '\+semver:\s?(none|skip)' 7 | assembly-informational-format: '{NuGetVersionV2}+Sha.{Sha}.Date.{CommitDate}' 8 | branches: 9 | main: 10 | tag: preview 11 | regex: ^main$ 12 | pull-request: 13 | tag: PR 14 | feature: 15 | tag: useBranchName 16 | increment: Minor 17 | regex: f(eature(s)?)?[\/-] 18 | source-branches: ['main'] 19 | hotfix: 20 | tag: fix 21 | increment: Patch 22 | regex: (hot)?fix(es)?[\/-] 23 | source-branches: ['main'] 24 | 25 | ignore: 26 | sha: [] 27 | merge-message-formats: {} 28 | 29 | tag-prefix: '[vV]' 30 | -------------------------------------------------------------------------------- /BicepNet.Core/Authentication/ExternalTokenCredential.cs: -------------------------------------------------------------------------------- 1 | using Azure.Core; 2 | using System; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace BicepNet.Core.Authentication; 7 | 8 | public class ExternalTokenCredential(string token, DateTimeOffset expiresOn) : TokenCredential 9 | { 10 | private readonly string token = token; 11 | private readonly DateTimeOffset expiresOn = expiresOn; 12 | 13 | public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken) 14 | { 15 | return new AccessToken(token, expiresOn); 16 | } 17 | 18 | public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken) 19 | { 20 | return new ValueTask(new AccessToken(token, expiresOn)); 21 | } 22 | } -------------------------------------------------------------------------------- /BicepNet.Core/Models/BicepRepositoryModule.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace BicepNet.Core.Models; 6 | 7 | public class BicepRepositoryModule(string digest, string repository, List tags, DateTimeOffset createdOn, DateTimeOffset updatedOn) 8 | { 9 | public string Digest { get; } = digest; 10 | public string Repository { get; } = repository; 11 | public List Tags { get; } = tags; 12 | public DateTimeOffset CreatedOn { get; } = createdOn; 13 | public DateTimeOffset UpdatedOn { get; } = updatedOn; 14 | 15 | // Return a string of comma-separated tags or 'null' 16 | public override string ToString() 17 | => Tags.Count != 0 ? string.Join(", ", Tags.OrderByDescending(t => t.UpdatedOn).Select(t => t.ToString())) : "null"; 18 | } 19 | -------------------------------------------------------------------------------- /BicepNet.PS/Commands/BuildBicepNetParamFileCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace BicepNet.PS.Commands; 4 | 5 | [Cmdlet(VerbsLifecycle.Build, "BicepNetParamFile")] 6 | public class BuildBicepNetParamFileCommand : BicepNetBaseCommand 7 | { 8 | [Parameter(Mandatory = true, ValueFromPipeline = true)] 9 | [ValidateNotNullOrEmpty] 10 | public string Path { get; set; } 11 | 12 | [Parameter(Mandatory = false)] 13 | [ValidateNotNullOrEmpty] 14 | public string TemplatePath { get; set; } = ""; 15 | 16 | [Parameter()] 17 | public SwitchParameter NoRestore { get; set; } 18 | 19 | protected override void ProcessRecord() 20 | { 21 | var result = bicepWrapper.Build(Path, TemplatePath, NoRestore.IsPresent); 22 | foreach (var item in result) 23 | { 24 | WriteObject(item); 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /BicepNet.PS/Commands/PublishBicepNetFileCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace BicepNet.PS.Commands; 4 | 5 | [Cmdlet(VerbsData.Publish, "BicepNetFile")] 6 | public class PublishBicepNetFileCommand : BicepNetBaseCommand 7 | { 8 | [Parameter(Mandatory = true, ValueFromPipeline = true)] 9 | [ValidateNotNullOrEmpty] 10 | public string Path { get; set; } 11 | 12 | [Parameter(Mandatory = true)] 13 | [ValidateNotNullOrEmpty] 14 | public string Target { get; set; } 15 | 16 | [Parameter(Mandatory = false)] 17 | [ValidateNotNullOrEmpty] 18 | public string DocumentationUri { get; set; } 19 | 20 | [Parameter(Mandatory = false)] 21 | public SwitchParameter Force { get; set; } 22 | protected override void ProcessRecord() 23 | { 24 | bicepWrapper.Publish(Path, Target, DocumentationUri, Force.IsPresent); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /RequiredModules.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | PSDependOptions = @{ 3 | AddToPath = $true 4 | Target = 'output\RequiredModules' 5 | Parameters = @{ 6 | Repository = 'PSGallery' 7 | } 8 | } 9 | 10 | InvokeBuild = 'latest' 11 | PSScriptAnalyzer = 'latest' 12 | Pester = 'latest' 13 | ModuleBuilder = 'latest' 14 | ChangelogManagement = 'latest' 15 | Sampler = 'latest' 16 | 'Sampler.GitHubTasks' = 'latest' 17 | MarkdownLinkCheck = 'latest' 18 | 'DscResource.Common' = 'latest' 19 | 'DscResource.Test' = 'latest' 20 | 'DscResource.AnalyzerRules' = 'latest' 21 | xDscResourceDesigner = 'latest' 22 | 'DscResource.DocGenerator' = 'latest' 23 | 'Azure/Bicep' = 'v0.27.1' 24 | } 25 | -------------------------------------------------------------------------------- /.github/actions/test-linux/action.yml: -------------------------------------------------------------------------------- 1 | name: "Test Linux" 2 | 3 | on: 4 | workflow_call: 5 | 6 | runs: 7 | using: 'composite' 8 | steps: 9 | - uses: actions/checkout@v4 10 | with: 11 | ref: ${{ github.head_ref }} 12 | fetch-depth: 0 13 | 14 | - name: Update PowerShell 15 | uses: bjompen/UpdatePWSHAction@v1.0.0 16 | with: 17 | ReleaseVersion: 'stable' 18 | 19 | - name: Download Build Artifact 20 | uses: actions/download-artifact@v4 21 | with: 22 | name: ${{ env.buildArtifactName }} 23 | path: ${{ env.buildFolderName }} 24 | 25 | - name: Run Tests 26 | shell: pwsh 27 | run: ./build.ps1 -tasks test 28 | 29 | - name: Publish Test Artifact 30 | uses: actions/upload-artifact@v4 31 | with: 32 | path: ${{ env.buildFolderName }}/${{ env.testResultFolderName }}/ 33 | name: CodeCoverageLinux 34 | if: success() || failure() 35 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | require_ci_to_pass: no 3 | # master should be the baseline for reporting 4 | branch: main 5 | 6 | comment: 7 | layout: "reach, diff, flags, files" 8 | behavior: default 9 | 10 | coverage: 11 | range: 50..80 12 | round: down 13 | precision: 0 14 | 15 | status: 16 | project: 17 | default: 18 | # Set the overall project code coverage requirement to 70% 19 | target: 70 20 | patch: 21 | default: 22 | # Set the pull request requirement to not regress overall coverage by more than 5% 23 | # and let codecov.io set the goal for the code changed in the patch. 24 | target: auto 25 | threshold: 5 26 | 27 | # Use this if there are paths that should not be part of the code coverage, for 28 | # example a deprecated function where tests has been removed. 29 | #ignore: 30 | # - 'source/Public/Get-Deprecated.ps1' 31 | 32 | -------------------------------------------------------------------------------- /BicepNet.PS/Commands/GetBicepNetCachePathCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace BicepNet.PS.Commands; 4 | 5 | [Cmdlet(VerbsCommon.Get, "BicepNetCachePath", DefaultParameterSetName = "br")] 6 | [CmdletBinding] 7 | public class GetBicepNetCachePathCommand : BicepNetBaseCommand 8 | { 9 | [Parameter(ParameterSetName="br")] 10 | public SwitchParameter Oci { get; set; } 11 | 12 | [Parameter(Mandatory = true, ParameterSetName = "ts")] 13 | public SwitchParameter TemplateSpecs { get; set; } 14 | 15 | protected override void EndProcessing() 16 | { 17 | string result = ""; 18 | if (Oci.IsPresent || ParameterSetName == "br") 19 | { 20 | result = bicepWrapper.OciCachePath; 21 | } 22 | else if (TemplateSpecs.IsPresent) 23 | { 24 | result = bicepWrapper.TemplateSpecsCachePath; 25 | } 26 | WriteObject(result); 27 | } 28 | } -------------------------------------------------------------------------------- /.build/tasks/BicepNet.generateTestCases.build.ps1: -------------------------------------------------------------------------------- 1 | task GenerateTestCases { 2 | $ModuleVersion = Split-ModuleVersion -ModuleVersion (Get-BuiltModuleVersion -OutputDirectory 'output' -ModuleName 'BicepNet.PS' -VersionedOutputDirectory) 3 | $ModuleOutputPath = "output/BicepNet.PS/$($ModuleVersion.Version)/BicepNet.PS" 4 | Import-Module (Convert-Path $ModuleOutputPath) 5 | $ModuleCommands = Get-Command -Module 'BicepNet.PS' 6 | $CommandList = foreach ($Command in $ModuleCommands) { 7 | [PSCustomObject]@{ 8 | CommandName = $Command.Name 9 | Parameters = $Command.parameters.Keys | ForEach-Object { 10 | [PSCustomObject]@{ 11 | ParameterName = $_ 12 | ParameterType = $Command.parameters[$_].ParameterType.FullName 13 | } 14 | } 15 | } 16 | } 17 | $CommandList | ConvertTo-Json -Depth 3 | Out-File 'BicepNet.PS/tests/BicepNet.PS.ParameterTests.json' 18 | } 19 | -------------------------------------------------------------------------------- /BicepNet.PS/Commands/ExportBicepNetResourceCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | 3 | namespace BicepNet.PS.Commands; 4 | 5 | [Cmdlet(VerbsData.Export, "BicepNetResource")] 6 | public class ExportBicepNetResourceCommand : BicepNetBaseCommand 7 | { 8 | [Parameter(Mandatory = true, ValueFromPipeline = true)] 9 | [ValidateNotNullOrEmpty] 10 | public string[] ResourceId { get; set; } 11 | 12 | [Parameter(Mandatory = false, ValueFromPipeline = false)] 13 | public SwitchParameter IncludeTargetScope { get; set; } 14 | 15 | protected override void ProcessRecord() 16 | { 17 | try 18 | { 19 | var result = bicepWrapper.ExportResources(ResourceId, MyInvocation.PSCommandPath, IncludeTargetScope.IsPresent); 20 | WriteObject(result); 21 | } 22 | catch (System.Exception exception) 23 | { 24 | WriteError(new ErrorRecord(exception, this.name, ErrorCategory.WriteError, null)); 25 | throw; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) PSBicep 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "PowerShell Interactive Session", 9 | "type": "PowerShell", 10 | "request": "launch", 11 | "cwd": "${cwd}" 12 | }, 13 | { 14 | "name": "BicepNet", 15 | "type": "coreclr", 16 | "request": "launch", 17 | "preLaunchTask": "build", 18 | "program": "pwsh", 19 | "args": [ 20 | "-NoExit", 21 | "-NoProfile", 22 | "-Command", 23 | "Import-Module ${workspaceFolder}/out/BicepNet.PS", 24 | ], 25 | "cwd": "${workspaceFolder}", 26 | "stopAtEntry": false, 27 | "console": "integratedTerminal", 28 | "justMyCode": false, 29 | "requireExactSource": false 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /BicepNet.Core/BicepHelper.cs: -------------------------------------------------------------------------------- 1 | using Azure.Deployments.Core.Comparers; 2 | using Bicep.Core.Resources; 3 | using Bicep.Core.TypeSystem.Providers.Az; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Linq; 7 | 8 | namespace BicepNet.Core; 9 | 10 | internal static class BicepHelper 11 | { 12 | internal static ResourceTypeReference ResolveBicepTypeDefinition(string fullyQualifiedType, AzResourceTypeLoader azResourceTypeLoader, ILogger? logger = null) 13 | { 14 | var matchedType = azResourceTypeLoader.GetAvailableTypes() 15 | .Where(x => StringComparer.OrdinalIgnoreCase.Equals(fullyQualifiedType, x.FormatType())) 16 | .OrderByDescending(x => x.ApiVersion, ApiVersionComparer.Instance) 17 | .FirstOrDefault(); 18 | if (matchedType is null || matchedType.ApiVersion is null) 19 | { 20 | var message = $"Failed to find a Bicep type definition for resource of type \"{fullyQualifiedType}\"."; 21 | logger?.LogCritical("{message}", message); 22 | throw new InvalidOperationException(message); 23 | } 24 | 25 | return matchedType; 26 | } 27 | } -------------------------------------------------------------------------------- /BicepNet.PS/Commands/SetBicepNetCredentialCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Management.Automation; 3 | 4 | namespace BicepNet.PS.Commands; 5 | 6 | [Cmdlet(VerbsCommon.Set, "BicepNetCredential")] 7 | [CmdletBinding(DefaultParameterSetName = "Interactive")] 8 | public class SetBicepNetCredentialCommand : BicepNetBaseCommand 9 | { 10 | [Parameter(Mandatory = true, ParameterSetName = "Token")] 11 | [ValidateNotNullOrEmpty] 12 | public string AccessToken { get; set; } 13 | 14 | [Parameter(ParameterSetName = "Interactive")] 15 | [ValidateNotNullOrEmpty] 16 | public string TenantId { get; set; } 17 | 18 | protected override void BeginProcessing() 19 | { 20 | base.BeginProcessing(); 21 | switch (ParameterSetName) 22 | { 23 | case "Token": 24 | bicepWrapper.SetAuthentication(AccessToken); 25 | break; 26 | case "Interactive": 27 | bicepWrapper.SetAuthentication(null, TenantId); 28 | break; 29 | default: 30 | throw new InvalidOperationException("Not a valid parameter set!"); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /BicepNet.Core/BicepWrapper.Format.cs: -------------------------------------------------------------------------------- 1 | using Bicep.Core.Parsing; 2 | using Bicep.Core.PrettyPrint; 3 | using Bicep.Core.PrettyPrint.Options; 4 | using Bicep.Core.Workspaces; 5 | using System; 6 | 7 | namespace BicepNet.Core; 8 | 9 | public partial class BicepWrapper 10 | { 11 | public static string Format(string content, string kind, string newline, string indentKind, int indentSize = 2, bool insertFinalNewline = false) 12 | { 13 | var fileKind = (BicepSourceFileKind)Enum.Parse(typeof(BicepSourceFileKind), kind, true); 14 | var newlineOption = (NewlineOption)Enum.Parse(typeof(NewlineOption), newline, true); 15 | var indentKindOption = (IndentKindOption)Enum.Parse(typeof(IndentKindOption), indentKind, true); 16 | 17 | BaseParser parser = fileKind == BicepSourceFileKind.BicepFile ? new Parser(content) : new ParamsParser(content); 18 | 19 | var options = new PrettyPrintOptions(newlineOption, indentKindOption, indentSize, insertFinalNewline); 20 | var output = PrettyPrinter.PrintProgram(parser.Program(), options, parser.LexingErrorLookup, parser.ParsingErrorLookup); 21 | 22 | return output; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /BicepNet.PS/tests/Module.tests.ps1: -------------------------------------------------------------------------------- 1 | param( 2 | $TestCaseData = "$PSScriptRoot/BicepNet.PS.ParameterTests.json" 3 | ) 4 | 5 | BeforeDiscovery { 6 | $TestCases = Get-Content $TestCaseData | ConvertFrom-Json 7 | } 8 | 9 | BeforeAll { 10 | $ModulePath = Convert-Path "$PSScriptRoot/../../output/BicepNet.PS" 11 | Import-Module $ModulePath -Force 12 | $ModuleCommands = Get-Command -Module 'BicepNet.PS' 13 | } 14 | 15 | Describe 'No breaking parameter changes' { 16 | Context 'Checking parameter definitions on command <_.CommandName>' -Foreach $TestCases { 17 | 18 | BeforeAll { 19 | $TestCommand = $_ 20 | $ModuleCommand = $ModuleCommands | Where-Object {$_.Name -eq $TestCommand.CommandName} 21 | } 22 | 23 | It 'Parameter <_.Parametername> is of type <_.ParameterType>' -Foreach $_.Parameters { 24 | $ModuleCommand.Parameters.($_.Parametername).ParameterType.FullName | Should -Be $_.ParameterType 25 | } 26 | 27 | It 'All parameters are tested' { 28 | $ModuleCommand.Parameters.Keys | ForEach-Object { 29 | $TestCommand.Parameters.ParameterName | Should -Contain $_ 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /BicepNet.Core/BicepWrapper.Decompile.cs: -------------------------------------------------------------------------------- 1 | using Bicep.Core.FileSystem; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | 6 | namespace BicepNet.Core; 7 | 8 | public partial class BicepWrapper 9 | { 10 | public IDictionary Decompile(string templatePath) => 11 | joinableTaskFactory.Run(() => DecompileAsync(templatePath)); 12 | 13 | public async Task> DecompileAsync(string templatePath) 14 | { 15 | var inputPath = PathHelper.ResolvePath(templatePath); 16 | var inputUri = PathHelper.FilePathToFileUrl(inputPath); 17 | 18 | if (!fileResolver.TryRead(inputUri).IsSuccess(out var jsonContent)) 19 | { 20 | throw new InvalidOperationException($"Failed to read {inputUri}"); 21 | } 22 | 23 | var template = new Dictionary(); 24 | var decompilation = await decompiler.Decompile(PathHelper.ChangeToBicepExtension(inputUri), jsonContent); 25 | 26 | foreach (var (fileUri, bicepOutput) in decompilation.FilesToSave) 27 | { 28 | template.Add(fileUri.LocalPath, bicepOutput); 29 | } 30 | 31 | return template; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /BicepNet.PS/Commands/FindBicepNetModuleCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Management.Automation; 3 | 4 | namespace BicepNet.PS.Commands; 5 | 6 | [Cmdlet(VerbsCommon.Find, "BicepNetModule")] 7 | public class FindBicepNetModuleCommand : BicepNetBaseCommand 8 | { 9 | [Parameter(ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Path")] 10 | [ValidateNotNullOrEmpty] 11 | public string Path { get; set; } 12 | 13 | [Parameter(ValueFromPipeline = true, ValueFromPipelineByPropertyName = true, ParameterSetName = "Registry")] 14 | [ValidateNotNullOrEmpty] 15 | public string Registry { get; set; } 16 | 17 | [Parameter(ParameterSetName = "Cache")] 18 | public SwitchParameter Cache { get; set; } 19 | 20 | protected override void ProcessRecord() 21 | { 22 | var result = ParameterSetName switch { 23 | "Path" => bicepWrapper.FindModules(Path, false), 24 | "Registry" => bicepWrapper.FindModules(Registry, true), 25 | "Cache" => bicepWrapper.FindModules(), 26 | _ => throw new InvalidOperationException("Invalid parameter set"), 27 | }; 28 | 29 | foreach (var item in result) 30 | { 31 | WriteObject(item); 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /BicepNet.Core/BicepNet.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /.github/actions/build/action.yml: -------------------------------------------------------------------------------- 1 | name: "Build Module" 2 | 3 | on: 4 | workflow_call: 5 | 6 | runs: 7 | using: 'composite' 8 | steps: 9 | - name: Update PowerShell 10 | uses: bjompen/UpdatePWSHAction@v1.0.0 11 | with: 12 | ReleaseVersion: 'stable' 13 | 14 | - name: Setup.NET Core 15 | uses: actions/setup-dotnet@v4 16 | with: 17 | dotnet-version: '8.0' 18 | 19 | - name: Install GitVersion 20 | uses: gittools/actions/gitversion/setup@v1.1.1 21 | with: 22 | versionSpec: '5.x' 23 | 24 | - name: Determine Version 25 | id: gitversion 26 | uses: gittools/actions/gitversion/execute@v1.1.1 27 | 28 | - name: Setup assets cache 29 | id: assetscache 30 | uses: actions/cache@v4 31 | with: 32 | path: output/RequiredModules 33 | key: ${{ hashFiles('RequiredModules.psd1') }} 34 | 35 | - name: Download required dependencies 36 | if: steps.assetscache.outputs.cache-hit != 'true' 37 | shell: pwsh 38 | run: ./build.ps1 -ResolveDependency 39 | 40 | - name: Build module 41 | shell: pwsh 42 | run: ./build.ps1 -tasks package -Verbose 43 | env: 44 | ModuleVersion: ${{ env.gitVersion.NuGetVersionV2 }} 45 | 46 | - name: Publish build artifacts 47 | uses: actions/upload-artifact@v4 48 | with: 49 | name: ${{ env.buildArtifactName }} 50 | path: ${{ env.buildFolderName }}/ 51 | -------------------------------------------------------------------------------- /.github/actions/code-coverage/action.yml: -------------------------------------------------------------------------------- 1 | name: "Publish test results" 2 | 3 | on: 4 | workflow_call: 5 | 6 | runs: 7 | using: 'composite' 8 | steps: 9 | - uses: actions/checkout@v4 10 | with: 11 | ref: ${{ github.head_ref }} 12 | fetch-depth: 0 13 | 14 | - name: Download Test Artifact Linux 15 | uses: actions/download-artifact@v4 16 | with: 17 | name: CodeCoverageLinux 18 | path: ${{ env.buildFolderName }}/${{ env.testResultFolderName }}/CodeCoverageLinux/ 19 | 20 | # - name: Download Test Artifact Windows 21 | # uses: actions/download-artifact@v4 22 | # with: 23 | # name: CodeCoverageWin 24 | # path: ${{ env.buildFolderName }}/${{ env.testResultFolderName }}/CodeCoverageWin/ 25 | 26 | - name: Publish Linux Test Results 27 | id: linux-test-results 28 | uses: EnricoMi/publish-unit-test-result-action@v2 29 | if: always() 30 | with: 31 | nunit_files: ${{ env.buildFolderName }}/${{ env.testResultFolderName }}/CodeCoverageLinux/NUnit*.xml 32 | check_name: Linux Test Results 33 | 34 | # - name: Publish Win Test Results 35 | # id: win-test-results 36 | # uses: EnricoMi/publish-unit-test-result-action@v2 37 | # if: always() 38 | # with: 39 | # nunit_files: ${{ env.buildFolderName }}/${{ env.testResultFolderName }}/CodeCoverageWin/NUnit*.xml 40 | # check_name: Win Test Results 41 | -------------------------------------------------------------------------------- /BicepNet.PS/Commands/GetBicepNetConfigCommand.cs: -------------------------------------------------------------------------------- 1 | using BicepNet.Core.Configuration; 2 | using System; 3 | using System.Management.Automation; 4 | 5 | namespace BicepNet.PS.Commands; 6 | 7 | [Cmdlet(VerbsCommon.Get, "BicepNetConfig")] 8 | [CmdletBinding()] 9 | public class GetBicepNetConfigCommand : BicepNetBaseCommand 10 | { 11 | [Parameter(ParameterSetName = "Scope")] 12 | [ValidateSet(["Default", "Merged", "Local"])] 13 | public string Scope { get; set; } 14 | 15 | [Parameter(ValueFromPipeline = true)] 16 | [ValidateNotNullOrEmpty] 17 | public string Path { get; set; } 18 | 19 | protected override void BeginProcessing() 20 | { 21 | base.BeginProcessing(); 22 | 23 | // If Scope is not set 24 | // Set Scope to Default if no Path, otherwise Merged 25 | Scope ??= Path is null ? "Default" : "Merged"; 26 | 27 | if (Path is not null && Scope == "Default") 28 | { 29 | WriteWarning("The Path parameter is specified but the Scope parameter is set to Default, the Path will not be used!"); 30 | } 31 | } 32 | 33 | protected override void ProcessRecord() 34 | { 35 | // Parse Scope to enum and pass it together to BicepWrapper with Path, which can be null here if not provided 36 | WriteObject(bicepWrapper.GetBicepConfigInfo((BicepConfigScope)Enum.Parse(typeof(BicepConfigScope), Scope), Path)); 37 | } 38 | } -------------------------------------------------------------------------------- /BicepNet.PS/LoadContext/ModuleInitializer.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Management.Automation; 3 | using System.Reflection; 4 | using System.Runtime.Loader; 5 | 6 | namespace BicepNet.PS.LoadContext; 7 | 8 | public class BicepNetModuleInitializer : IModuleAssemblyInitializer 9 | { 10 | private static readonly string s_binBasePath = Path.GetFullPath( 11 | Path.Combine( 12 | Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), 13 | "..")); 14 | 15 | private static readonly string s_binCommonPath = Path.Combine(s_binBasePath, "BicepNet.Core"); 16 | 17 | public void OnImport() 18 | { 19 | AssemblyLoadContext.Default.Resolving += ResolveAssembly_NetCore; 20 | } 21 | 22 | private static Assembly ResolveAssembly_NetCore( 23 | AssemblyLoadContext assemblyLoadContext, 24 | AssemblyName assemblyName) 25 | { 26 | // In .NET Core, PowerShell deals with assembly probing so our logic is much simpler 27 | // We only care about our Engine assembly 28 | if (!assemblyName.Name.Equals("BicepNet.Core")) 29 | { 30 | return null; 31 | } 32 | 33 | // Now load the Engine assembly through the dependency ALC, and let it resolve further dependencies automatically 34 | return DependencyAssemblyLoadContext.GetForDirectory(s_binCommonPath).LoadFromAssemblyName(assemblyName); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | ## Security 2 | 3 | We take the security of our modules seriously, which includes all source code repositories managed through our GitHub organization. 4 | 5 | If you believe you have found a security vulnerability in any of our repository, please report it to us as described below. 6 | 7 | ## Reporting Security Issues 8 | 9 | **Please do not report security vulnerabilities through public GitHub issues.** 10 | 11 | Instead, please report them to one or several maintainers of the repository. 12 | The easiest way to do so is to send us a direct message via twitter or find us 13 | on some other social platform. 14 | 15 | You should receive a response within 48 hours. If for some reason you do not, please follow up by other means or to other contributors. 16 | 17 | Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue: 18 | 19 | * Type of issue 20 | * Full paths of source file(s) related to the manifestation of the issue 21 | * The location of the affected source code (tag/branch/commit or direct URL) 22 | * Any special configuration required to reproduce the issue 23 | * Step-by-step instructions to reproduce the issue 24 | * Proof-of-concept or exploit code (if possible) 25 | * Impact of the issue, including how an attacker might exploit the issue 26 | 27 | This information will help us triage your report more quickly. 28 | 29 | ## Preferred Languages 30 | 31 | We prefer all communications to be in English. 32 | -------------------------------------------------------------------------------- /BicepNet.PS/Commands/FormatBicepNetFile.cs: -------------------------------------------------------------------------------- 1 | using System.Management.Automation; 2 | using BicepNet.Core; 3 | 4 | namespace BicepNet.PS.Commands; 5 | 6 | [Cmdlet(VerbsCommon.Format, "BicepNet")] 7 | public class FormatBicepNet : BicepNetBaseCommand 8 | { 9 | [Parameter(Mandatory = true, ValueFromPipeline = true)] 10 | [ValidateNotNullOrEmpty] 11 | public string Content { get; set; } 12 | 13 | [Parameter(Mandatory = false, ValueFromPipeline = false)] 14 | [ValidateNotNullOrEmpty] 15 | [ValidateSet("BicepFile", "ParamsFile")] 16 | public string FileKind { get; set; } = "BicepFile"; 17 | 18 | [Parameter(Mandatory = false, ValueFromPipeline = false)] 19 | [ValidateNotNullOrEmpty] 20 | [ValidateSet("Auto", "LF", "CRLF")] 21 | public string NewlineOption { get; set; } = "Auto"; 22 | 23 | [Parameter(Mandatory = false, ValueFromPipeline = false)] 24 | [ValidateNotNullOrEmpty] 25 | [ValidateSet("Space", "Tab")] 26 | public string IndentKindOption { get; set; } = "Space"; 27 | 28 | [Parameter(Mandatory = false, ValueFromPipeline = false)] 29 | [ValidateNotNullOrEmpty] 30 | public int IndentSize { get; set; } = 2; 31 | 32 | [Parameter(Mandatory = false, ValueFromPipeline = false)] 33 | [ValidateNotNullOrEmpty] 34 | public bool InsertFinalNewline { get; set; } = false; 35 | 36 | protected override void ProcessRecord() 37 | { 38 | var result = BicepWrapper.Format(Content, FileKind, NewlineOption, IndentKindOption, IndentSize, InsertFinalNewline); 39 | WriteObject(result); 40 | } 41 | } -------------------------------------------------------------------------------- /BicepNet.Core/BicepWrapper.ExportChildResources.cs: -------------------------------------------------------------------------------- 1 | using BicepNet.Core.Azure; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace BicepNet.Core; 8 | 9 | public partial class BicepWrapper 10 | { 11 | public IDictionary ExportChildResoures(string scopeId, string? configurationPath = null, bool includeTargetScope = false) => 12 | joinableTaskFactory.Run(() => ExportChildResouresAsync(scopeId, configurationPath, includeTargetScope)); 13 | 14 | public async Task> ExportChildResouresAsync(string scopeId, string? configurationPath = null, bool includeTargetScope = false) 15 | { 16 | Dictionary result = []; 17 | var scopeResourceId = AzureHelpers.ValidateResourceId(scopeId); 18 | var cancellationToken = new CancellationToken(); 19 | var config = configurationManager.GetConfiguration(new Uri(configurationPath ?? "inmemory://main.bicep")); 20 | var resourceDefinitions = azResourceProvider.GetChildResourcesAsync(config, scopeResourceId, cancellationToken); 21 | 22 | await foreach (var (id, resource) in resourceDefinitions) 23 | { 24 | var name = AzureHelpers.GetResourceFriendlyName(id); 25 | var resourceId = AzureHelpers.ValidateResourceId(id); 26 | var matchedType = BicepHelper.ResolveBicepTypeDefinition(resourceId.FullyQualifiedType, azResourceTypeLoader, logger); 27 | result.Add(name, AzureResourceProvider.GenerateBicepTemplate(resourceId, matchedType, resource, includeTargetScope: includeTargetScope)); 28 | } 29 | 30 | return result; 31 | } 32 | } -------------------------------------------------------------------------------- /BicepNet.PS/Commands/BicepNetBaseCommand.ILogger.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Management.Automation; 5 | 6 | namespace BicepNet.PS.Commands; 7 | 8 | public partial class BicepNetBaseCommand : ILogger 9 | { 10 | private readonly List logLevels = [ 11 | LogLevel.Trace, 12 | LogLevel.Debug, 13 | LogLevel.Information, 14 | LogLevel.Warning, 15 | LogLevel.Error 16 | ]; 17 | 18 | public IDisposable BeginScope(TState state) => default!; 19 | 20 | public bool IsEnabled(LogLevel logLevel) => logLevels.Contains(logLevel); 21 | 22 | public void Log( 23 | LogLevel logLevel, 24 | EventId eventId, 25 | TState state, 26 | Exception exception, 27 | Func formatter) 28 | { 29 | if (!IsEnabled(logLevel)) 30 | { 31 | return; 32 | } 33 | 34 | switch (logLevel) 35 | { 36 | case LogLevel.Trace: 37 | WriteVerbose(formatter(state, exception)); 38 | break; 39 | case LogLevel.Debug: 40 | WriteDebug(formatter(state, exception)); 41 | break; 42 | case LogLevel.Information: 43 | WriteVerbose(formatter(state, exception)); 44 | break; 45 | case LogLevel.Warning: 46 | WriteWarning(formatter(state, exception)); 47 | break; 48 | case LogLevel.Error: 49 | WriteError(new ErrorRecord(exception ?? new Exception(formatter(state, exception)), name, ErrorCategory.WriteError, null)); 50 | break; 51 | default: 52 | break; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /BicepNet.Core/Azure/ManagementGroupHelper.cs: -------------------------------------------------------------------------------- 1 | using Azure.Core; 2 | using Azure.ResourceManager; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Runtime.CompilerServices; 6 | using System.Text.Json; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace BicepNet.Core.Azure; 11 | 12 | internal static class ManagementGroupHelper 13 | { 14 | public static async Task GetManagementGroupAsync(ResourceIdentifier resourceIdentifier, ArmClient armClient, CancellationToken cancellationToken) 15 | { 16 | var mg = armClient.GetManagementGroupResource(resourceIdentifier); 17 | var mgResponse = await mg.GetAsync(cancellationToken: cancellationToken); 18 | if (mgResponse is null || mgResponse.GetRawResponse().ContentStream is not { } mgContentStream) 19 | { 20 | throw new InvalidOperationException($"Failed to fetch resource from Id '{resourceIdentifier}'"); 21 | } 22 | mgContentStream.Position = 0; 23 | return await JsonSerializer.DeserializeAsync(mgContentStream, cancellationToken: cancellationToken); 24 | } 25 | 26 | public static async IAsyncEnumerable GetManagementGroupDescendantsAsync(ResourceIdentifier resourceIdentifier, ArmClient armClient, [EnumeratorCancellation] CancellationToken cancellationToken) 27 | { 28 | var mg = armClient.GetManagementGroupResource(resourceIdentifier); 29 | var list = mg.GetDescendantsAsync(cancellationToken: cancellationToken); 30 | 31 | await foreach (var item in list) 32 | { 33 | if (item.ParentId != resourceIdentifier) { continue; } 34 | if (item.ResourceType == "Microsoft.Management/managementGroups/subscriptions") 35 | { 36 | var subId = $"{mg.Id}/subscriptions/{item.Name}"; 37 | yield return subId; 38 | } else 39 | { 40 | yield return item.Id.ToString(); 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BicepNet 2 | 3 | > **BicepNet is no longer a standalone project from PSBicep! The code lives on as its own part of the [Bicep PowerShell module](https://github.com/PSBicep/PSBicep), a hybrid module written in both C# (the part that was previously this project) and PowerShell.** 4 | 5 | --- 6 | 7 | This is the repository for **BicepNet**, a thin wrapper around [Bicep](https://github.com/Azure/bicep) that will load all Bicep assemblies in a separate context to avoid conflicts with other modules. **BicepNet** is developed for the [Bicep PowerShell](https://github.com/PSBicep/PSBicep) module but could be used for any other project where you want to leverage Bicep functionality natively in PowerShell or .NET. 8 | 9 | Using BicepNet is generally much faster than calling the CLI since the overhead of loading all assemblies is only performed once. Since BicepNet depends on internal code from the Bicep project, support for new versions of Bicep is incorporated with a bit of delay. The table below shows wich version of Bicep is used in each release of BicepNet. 10 | 11 | ## Bicep assembly versions 12 | 13 | | BicepNet version | Bicep assembly version | 14 | | --- | --- | 15 | | `2.3.1` | `0.24.24` | 16 | | `2.3.0` | `0.22.6` | 17 | | `2.2.0` | `0.21.1` | 18 | | `2.1.0` | `0.18.4` | 19 | | `2.0.10` | `0.11.1` | 20 | | `2.0.9` | `0.10.61` | 21 | | `2.0.8` | `0.9.1` | 22 | | `2.0.7` | `0.8.9` | 23 | | `2.0.6` | `0.7.4` | 24 | | `2.0.5` | `0.6.18` | 25 | | `2.0.4` | `0.6.18` | 26 | | `2.0.3` | `0.5.6` | 27 | | `2.0.2` | `0.4.1318` | 28 | | `2.0.1` | `0.4.1272` | 29 | | `2.0.0` | `0.4.1124` | 30 | | `1.0.7` | `0.4.1008` | 31 | | `1.0.6` | `0.4.1008` | 32 | | `1.0.5` | `0.4.613` | 33 | | `1.0.4` | `0.4.451` | 34 | | `1.0.3` | `0.4.412` | 35 | | `1.0.2` | `0.4.412` | 36 | | `1.0.1` | `0.4.63` | 37 | | `1.0.0` | `0.4.63` | 38 | 39 | ## Contributing 40 | 41 | If you like the Bicep PowerShell module and want to contribute you are very much welcome to do so. Please see the [Bicep PowerShell module](https://github.com/PSBicep/PSBicep) for how to get started. 42 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/dotnet 3 | { 4 | "name": "BicepNet development environment", 5 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 6 | "image": "mcr.microsoft.com/devcontainers/base:jammy", 7 | "features": { 8 | "ghcr.io/devcontainers/features/dotnet:1": { 9 | "version": "8.0" 10 | }, 11 | "ghcr.io/devcontainers/features/powershell:1": { 12 | "version": "7.4.2" 13 | }, 14 | "ghcr.io/devcontainers/features/github-cli:1": {} 15 | }, 16 | // Configure tool-specific properties. 17 | "customizations": { 18 | "vscode": { 19 | "settings": { 20 | "powershell.powerShellAdditionalExePaths": { 21 | "pwsh": "/opt/microsoft/powershell/7/pwsh" 22 | }, 23 | "terminal.integrated.defaultProfile.linux": "pwsh" 24 | }, 25 | "extensions": [ 26 | "eamodio.gitlens", 27 | "github.vscode-pull-request-github", 28 | "ms-azuretools.vscode-bicep", 29 | "ms-dotnettools.csdevkit", 30 | "ms-vscode.powershell", 31 | "ms-vsliveshare.vsliveshare", 32 | "vscode-icons-team.vscode-icons" 33 | ] 34 | } 35 | }, 36 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 37 | // "forwardPorts": [5000, 5001], 38 | // "portsAttributes": { 39 | // "5001": { 40 | // "protocol": "https" 41 | // } 42 | // } 43 | // Use 'postCreateCommand' to run commands after the container is created. 44 | "postCreateCommand": "pwsh -c './build.ps1 -ResolveDependency compile'", 45 | // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. 46 | "remoteUser": "vscode" 47 | } 48 | -------------------------------------------------------------------------------- /BicepNet.Core/Configuration/BicepNetExtensions.cs: -------------------------------------------------------------------------------- 1 | using Azure.Bicep.Types; 2 | using Azure.Bicep.Types.Az; 3 | using Bicep.Cli; 4 | using Bicep.Cli.Helpers; 5 | using Bicep.Cli.Logging; 6 | using Bicep.Core.Registry; 7 | using Bicep.Core.Registry.Auth; 8 | using Bicep.Core.TypeSystem.Providers; 9 | using Bicep.Core.TypeSystem.Providers.Az; 10 | using Bicep.Core.Workspaces; 11 | using Bicep.LanguageServer.Providers; 12 | using BicepNet.Core.Authentication; 13 | using BicepNet.Core.Azure; 14 | using BicepNet.Core.Configuration; 15 | using Microsoft.Extensions.DependencyInjection; 16 | using Microsoft.Extensions.DependencyInjection.Extensions; 17 | using Microsoft.Extensions.Logging; 18 | using System; 19 | 20 | namespace BicepNet.Core; 21 | 22 | public static class BicepNetExtensions 23 | { 24 | public static ServiceCollection AddBicepNet(this ServiceCollection services, ILogger bicepLogger) 25 | { 26 | services 27 | .AddSingleton() 28 | .AddBicepCore() 29 | .AddBicepDecompiler() 30 | .AddSingleton() 31 | .AddSingleton(s => s.GetRequiredService()) 32 | .AddSingleton() 33 | .AddSingleton(s => s.GetRequiredService()) 34 | .AddSingleton() 35 | .AddSingleton(s => s.GetRequiredService()) 36 | .AddSingleton() 37 | .AddSingleton(s => s.GetRequiredService()) 38 | .AddSingleton() 39 | 40 | .AddSingleton(bicepLogger) 41 | .AddSingleton(new IOContext(Console.Out, Console.Error)) 42 | .AddSingleton() 43 | 44 | .AddSingleton() 45 | .Replace(ServiceDescriptor.Singleton(s => s.GetRequiredService())); 46 | 47 | return services; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Resolve-Dependency.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | #PSDependTarget = './output/modules' 3 | #Proxy = '' 4 | #ProxyCredential = '$MyCredentialVariable' #TODO: find a way to support credentials in build (resolve variable) 5 | 6 | Gallery = 'PSGallery' 7 | 8 | # To use a private nuget repository change the following to your own feed. The locations must be a Nuget v2 feed due 9 | # to limitation in PowerShellGet v2.x. Example below is for a Azure DevOps Server project-scoped feed. While resolving 10 | # dependencies it will be registered as a trusted repository with the name specified in the property 'Gallery' above, 11 | # unless property 'Name' is provided in the hashtable below, if so it will override the property 'Gallery' above. The 12 | # registered repository will be removed when dependencies has been resolved, unless it was already registered to begin 13 | # with. If repository is registered already but with different URL:s the repository will be re-registered and reverted 14 | # after dependencies has been resolved. Currently only Windows integrated security works with private Nuget v2 feeds 15 | # (or if it is a public feed with no security), it is not possible yet to securely provide other credentials for the feed. 16 | #RegisterGallery = @{ 17 | # #Name = 'MyPrivateFeedName' 18 | # GallerySourceLocation = 'https://azdoserver.company.local///_packaging//nuget/v2' 19 | # GalleryPublishLocation = 'https://azdoserver.company.local///_packaging//nuget/v2' 20 | # GalleryScriptSourceLocation = 'https://azdoserver.company.local///_packaging//nuget/v2' 21 | # GalleryScriptPublishLocation = 'https://azdoserver.company.local///_packaging//nuget/v2' 22 | # #InstallationPolicy = 'Trusted' 23 | #} 24 | 25 | #AllowOldPowerShellGetModule = $true 26 | #MinimumPSDependVersion = '0.3.0' 27 | AllowPrerelease = $false 28 | WithYAML = $true # Will also bootstrap PowerShell-Yaml to read other config files 29 | } 30 | 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | # Pull Request 2 | 3 | 15 | 16 | ## Pull Request (PR) description 17 | 18 | 39 | 40 | ## Task list 41 | 42 | 50 | 51 | - [ ] The PR represents a single logical change. i.e. Cosmetic updates should go in different PRs. 52 | - [ ] Added an entry under the Unreleased section of in the CHANGELOG.md as per [format](https://keepachangelog.com/en/1.0.0/). 53 | - [ ] Local clean build passes without issue or fail tests (`build.ps1 -ResolveDependency`). 54 | - [ ] Markdown help added/updated. 55 | - [ ] Unit tests added/updated. 56 | -------------------------------------------------------------------------------- /.build/tasks/BicepNet.build.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | [string[]] 3 | $Path = @('BicepNet.Core', 'BicepNet.PS'), 4 | 5 | [ValidateSet('Debug', 'Release')] 6 | [string] 7 | $Configuration = 'Release', 8 | 9 | [Switch] 10 | $Full, 11 | 12 | [switch] 13 | $ClearNugetCache 14 | ) 15 | 16 | task dotnetBuild { 17 | $CommonFiles = [System.Collections.Generic.HashSet[string]]::new() 18 | if($ClearNugetCache) { 19 | dotnet nuget locals all --clear 20 | } 21 | if ($Full) { 22 | dotnet build-server shutdown 23 | } 24 | 25 | foreach ($projPath in $Path) { 26 | $outPathFolder = Split-Path -Path (Resolve-Path -Path $projPath) -Leaf 27 | Write-Host $projPath 28 | Write-Host $outPathFolder 29 | $outPath = "bin/$outPathFolder" 30 | if (-not (Test-Path -Path $projPath)) { 31 | throw "Path '$projPath' does not exist." 32 | } 33 | 34 | Push-Location -Path $projPath 35 | 36 | # Remove output folder if exists 37 | if (Test-Path -Path $outPath) { 38 | Remove-Item -Path $outPath -Recurse -Force 39 | } 40 | 41 | Write-Host "Restoring '$projPath'" -ForegroundColor 'Magenta' 42 | dotnet restore --force-evaluate 43 | Write-Host "Building '$projPath' to '$outPath'" -ForegroundColor 'Magenta' 44 | dotnet publish -c $Configuration -o $outPath 45 | 46 | # Remove everything we don't need from the build 47 | Get-ChildItem -Path $outPath | 48 | Foreach-Object { 49 | if ($_.Extension -notin '.dll', '.pdb' -or $CommonFiles.Contains($_.Name)) { 50 | # Only keep DLLs and PDBs, and only keep one copy of each file. 51 | Remove-Item $_.FullName -Recurse -Force 52 | } 53 | else { 54 | [void]$CommonFiles.Add($_.Name) 55 | } 56 | } 57 | 58 | Pop-Location 59 | } 60 | 61 | # Hack to get the logging abstractions DLL into the PS module instead of the ALC 62 | Move-Item "BicepNet.Core/bin/BicepNet.Core/Microsoft.Extensions.Logging.Abstractions.dll" "BicepNet.PS/bin/BicepNet.PS" -ErrorAction 'Ignore' 63 | } 64 | -------------------------------------------------------------------------------- /BicepNet.CLI/Program.cs: -------------------------------------------------------------------------------- 1 | 2 | using BicepNet.Core; 3 | using BicepNet.Core.Configuration; 4 | using Microsoft.Extensions.Logging; 5 | 6 | using var loggerFactory = LoggerFactory.Create(builder => 7 | { 8 | builder 9 | .AddConsole(); 10 | }); 11 | ILogger logger = loggerFactory.CreateLogger(); 12 | BicepWrapper bicepWrapper = new(logger); 13 | 14 | Console.WriteLine(string.Join(',',args)); 15 | 16 | if (args.Length > 0) 17 | { 18 | switch (args[0].ToLower()) 19 | { 20 | case "build": 21 | if (string.IsNullOrEmpty(args[1])) 22 | throw new ArgumentException("Missing template path"); 23 | 24 | var buildResult = bicepWrapper.Build(args[1]); 25 | foreach (var item in buildResult) 26 | { 27 | Console.WriteLine(item); 28 | } 29 | break; 30 | case "exportresource": 31 | if (string.IsNullOrEmpty(args[1])) 32 | throw new ArgumentException("Missing resource id"); 33 | 34 | var exportResult = bicepWrapper.ExportResources(new[] { args[1] }); 35 | Console.WriteLine(exportResult); 36 | break; 37 | case "converttobicep": 38 | if (string.IsNullOrEmpty(args[1])) 39 | throw new ArgumentException("Missing resource id"); 40 | if (string.IsNullOrEmpty(args[2])) 41 | throw new ArgumentException("Missing resource body"); 42 | 43 | var body = System.IO.File.ReadAllText(args[2]); 44 | var convertResult = bicepWrapper.ConvertResourceToBicep(args[1], body); 45 | Console.WriteLine(convertResult); 46 | break; 47 | case "config": 48 | if (string.IsNullOrEmpty(args[1])) 49 | throw new ArgumentException("Missing scope"); 50 | if (!Enum.TryParse(args[1], out BicepConfigScope scope)) { 51 | throw new ArgumentException($"Invalid scope: ${args[1]}"); 52 | } 53 | var path = args.Length < 3 || string.IsNullOrEmpty(args[2]) ? "" : args[2]; 54 | Console.WriteLine(bicepWrapper.GetBicepConfigInfo(scope, path).Config); 55 | break; 56 | default: 57 | break; 58 | } 59 | } -------------------------------------------------------------------------------- /BicepNet.PS/LoadContext/DependencyAssemblyLoadContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.IO; 4 | using System.Reflection; 5 | using System.Runtime.Loader; 6 | 7 | namespace BicepNet.PS.LoadContext; 8 | 9 | public class DependencyAssemblyLoadContext(string dependencyDirPath) : AssemblyLoadContext(nameof(DependencyAssemblyLoadContext)) 10 | { 11 | private static readonly string s_psHome = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location); 12 | 13 | private static readonly ConcurrentDictionary s_dependencyLoadContexts = new(); 14 | 15 | internal static DependencyAssemblyLoadContext GetForDirectory(string directoryPath) 16 | { 17 | return s_dependencyLoadContexts.GetOrAdd(directoryPath, (path) => new DependencyAssemblyLoadContext(path)); 18 | } 19 | 20 | private readonly string _dependencyDirPath = dependencyDirPath; 21 | 22 | protected override Assembly Load(AssemblyName assemblyName) 23 | { 24 | string assemblyFileName = $"{assemblyName.Name}.dll"; 25 | 26 | // Make sure we allow other common PowerShell dependencies to be loaded by PowerShell 27 | // But specifically exclude certain assemblies like Newtonsoft.Json and System.Text.Json since we want to use different versions here for Bicep 28 | if (!assemblyName.Name.Equals("Newtonsoft.Json", StringComparison.OrdinalIgnoreCase) && 29 | !assemblyName.Name.Equals("System.Text.Json", StringComparison.OrdinalIgnoreCase) && 30 | !assemblyName.Name.Equals("System.Text.Encodings.Web", StringComparison.OrdinalIgnoreCase)) 31 | { 32 | string psHomeAsmPath = Path.Join(s_psHome, assemblyFileName); 33 | if (File.Exists(psHomeAsmPath)) 34 | { 35 | // With this API, returning null means nothing is loaded 36 | return null; 37 | } 38 | } 39 | 40 | // Now try to load the assembly from the dependency directory 41 | string dependencyAsmPath = Path.Join(_dependencyDirPath, assemblyFileName); 42 | if (File.Exists(dependencyAsmPath)) 43 | { 44 | return LoadFromAssemblyPath(dependencyAsmPath); 45 | } 46 | 47 | return null; 48 | } 49 | } -------------------------------------------------------------------------------- /BicepNet.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32929.385 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BicepNet.Core", "BicepNet.Core\BicepNet.Core.csproj", "{2FE677B0-9877-479E-A602-B63E9C5F3F1E}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BicepNet.PS", "BicepNet.PS\BicepNet.PS.csproj", "{B1EDAC69-3368-470B-BC23-815922CA4205}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8A866D8B-8046-403D-89BD-BC580240DA8C}" 11 | ProjectSection(SolutionItems) = preProject 12 | scripts\build.ps1 = scripts\build.ps1 13 | EndProjectSection 14 | EndProject 15 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BicepNet.CLI", "BicepNet.CLI\BicepNet.CLI.csproj", "{09B0AABB-A6EE-41C3-88BF-8719B86C90EE}" 16 | EndProject 17 | Global 18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 19 | Debug|Any CPU = Debug|Any CPU 20 | Release|Any CPU = Release|Any CPU 21 | EndGlobalSection 22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 23 | {2FE677B0-9877-479E-A602-B63E9C5F3F1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 24 | {2FE677B0-9877-479E-A602-B63E9C5F3F1E}.Debug|Any CPU.Build.0 = Debug|Any CPU 25 | {2FE677B0-9877-479E-A602-B63E9C5F3F1E}.Release|Any CPU.ActiveCfg = Release|Any CPU 26 | {2FE677B0-9877-479E-A602-B63E9C5F3F1E}.Release|Any CPU.Build.0 = Release|Any CPU 27 | {B1EDAC69-3368-470B-BC23-815922CA4205}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {B1EDAC69-3368-470B-BC23-815922CA4205}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {B1EDAC69-3368-470B-BC23-815922CA4205}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {B1EDAC69-3368-470B-BC23-815922CA4205}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {09B0AABB-A6EE-41C3-88BF-8719B86C90EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {09B0AABB-A6EE-41C3-88BF-8719B86C90EE}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {09B0AABB-A6EE-41C3-88BF-8719B86C90EE}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {09B0AABB-A6EE-41C3-88BF-8719B86C90EE}.Release|Any CPU.Build.0 = Release|Any CPU 35 | EndGlobalSection 36 | GlobalSection(SolutionProperties) = preSolution 37 | HideSolutionNode = FALSE 38 | EndGlobalSection 39 | GlobalSection(ExtensibilityGlobals) = postSolution 40 | SolutionGuid = {A088BA9D-74EA-4A19-A418-9AEE6B3F809D} 41 | EndGlobalSection 42 | EndGlobal 43 | -------------------------------------------------------------------------------- /BicepNet.Core/BicepWrapper.ConvertResourceToBicep.cs: -------------------------------------------------------------------------------- 1 | using Bicep.Core.PrettyPrint; 2 | using Bicep.Core.PrettyPrint.Options; 3 | using Bicep.Core.Rewriters; 4 | using Bicep.Core.Semantics; 5 | using Bicep.Core.Workspaces; 6 | using BicepNet.Core.Azure; 7 | using System; 8 | using System.Collections.Immutable; 9 | using System.Text.Json; 10 | 11 | namespace BicepNet.Core; 12 | 13 | public partial class BicepWrapper 14 | { 15 | public string ConvertResourceToBicep(string resourceId, string resourceBody) 16 | { 17 | var id = AzureHelpers.ValidateResourceId(resourceId); 18 | var matchedType = BicepHelper.ResolveBicepTypeDefinition(id.FullyQualifiedType, azResourceTypeLoader, logger); 19 | JsonElement resource = JsonSerializer.Deserialize(resourceBody); 20 | 21 | var template = AzureResourceProvider.GenerateBicepTemplate(id, matchedType, resource, includeTargetScope: true); 22 | return RewriteBicepTemplate(template); 23 | } 24 | 25 | public string RewriteBicepTemplate(string template) 26 | { 27 | BicepFile virtualBicepFile = SourceFileFactory.CreateBicepFile(new Uri($"inmemory:///generated.bicep"), template); 28 | 29 | var sourceFileGrouping = SourceFileGroupingBuilder.Build( 30 | fileResolver, 31 | moduleDispatcher, 32 | configurationManager, 33 | workspace, 34 | virtualBicepFile.FileUri, 35 | featureProviderFactory, 36 | false); 37 | 38 | var compilation = new Compilation( 39 | featureProviderFactory, 40 | environment, 41 | namespaceProvider, 42 | sourceFileGrouping, 43 | configurationManager, 44 | bicepAnalyzer, 45 | moduleDispatcher, 46 | new AuxiliaryFileCache(fileResolver), 47 | ImmutableDictionary.Empty); 48 | 49 | var bicepFile = RewriterHelper.RewriteMultiple( 50 | compiler, 51 | compilation, 52 | virtualBicepFile, 53 | rewritePasses: 1, 54 | model => new TypeCasingFixerRewriter(model), 55 | model => new ReadOnlyPropertyRemovalRewriter(model)); 56 | 57 | var printOptions = new PrettyPrintOptions(NewlineOption.LF, IndentKindOption.Space, 2, false); 58 | template = PrettyPrinter.PrintValidProgram(bicepFile.ProgramSyntax, printOptions); 59 | 60 | return template; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /BicepNet.Core/Configuration/bicepconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This is the base configuration which provides the defaults for all values (end users don't see this file). 3 | // Intellisense for bicepconfig.json is controlled by src/vscode-bicep/schemas/bicepconfig.schema.json 4 | 5 | "cloud": { 6 | "currentProfile": "AzureCloud", 7 | "profiles": { 8 | "AzureCloud": { 9 | "resourceManagerEndpoint": "https://management.azure.com", 10 | "activeDirectoryAuthority": "https://login.microsoftonline.com" 11 | }, 12 | "AzureChinaCloud": { 13 | "resourceManagerEndpoint": "https://management.chinacloudapi.cn", 14 | "activeDirectoryAuthority": "https://login.chinacloudapi.cn" 15 | }, 16 | "AzureUSGovernment": { 17 | "resourceManagerEndpoint": "https://management.usgovcloudapi.net", 18 | "activeDirectoryAuthority": "https://login.microsoftonline.us" 19 | } 20 | }, 21 | "credentialPrecedence": [ 22 | "AzurePowerShell", 23 | "AzureCLI" 24 | ] 25 | }, 26 | "moduleAliases": { 27 | "ts": {}, 28 | "br": { 29 | "public": { 30 | "registry": "mcr.microsoft.com", 31 | "modulePath": "bicep" 32 | } 33 | } 34 | }, 35 | "providerAliases": { 36 | "br": { 37 | "public": { 38 | "registry": "mcr.microsoft.com", 39 | "providerPath": "bicep/providers" 40 | } 41 | } 42 | }, 43 | "providers": { 44 | "az": "builtin:", 45 | "kubernetes": "builtin:", 46 | "microsoftGraph": "builtin:" 47 | }, 48 | "implicitProviders": ["az"], 49 | "analyzers": { 50 | "core": { 51 | "verbose": false, 52 | "enabled": true, 53 | "rules": { 54 | "no-hardcoded-env-urls": { 55 | "level": "warning", 56 | "disallowedhosts": [ 57 | "api.loganalytics.io", 58 | "azuredatalakeanalytics.net", 59 | "azuredatalakestore.net", 60 | "batch.core.windows.net", 61 | "core.windows.net", 62 | "database.windows.net", 63 | "datalake.azure.net", 64 | "gallery.azure.com", 65 | "graph.windows.net", 66 | "login.microsoftonline.com", 67 | "management.azure.com", 68 | "management.core.windows.net", 69 | "region.asazure.windows.net", 70 | "trafficmanager.net", 71 | "vault.azure.net" 72 | ], 73 | "excludedhosts": [ 74 | "schema.management.azure.com" 75 | ] 76 | } 77 | } 78 | } 79 | }, 80 | "experimentalFeaturesEnabled": {}, 81 | "formatting": {} 82 | } 83 | -------------------------------------------------------------------------------- /BicepNet.Core/BicepWrapper.Restore.cs: -------------------------------------------------------------------------------- 1 | using Bicep.Core; 2 | using Bicep.Core.Diagnostics; 3 | using Bicep.Core.FileSystem; 4 | using Bicep.Core.Navigation; 5 | using Bicep.Core.Registry; 6 | using Bicep.Core.Syntax; 7 | using Bicep.Core.Workspaces; 8 | using Microsoft.Extensions.Logging; 9 | using System.Collections.Generic; 10 | using System.Collections.Immutable; 11 | using System.Linq; 12 | using System.Threading.Tasks; 13 | 14 | namespace BicepNet.Core; 15 | 16 | public partial class BicepWrapper 17 | { 18 | public void Restore(string inputFilePath, bool forceModulesRestore = false) => joinableTaskFactory.Run(() => RestoreAsync(inputFilePath, forceModulesRestore)); 19 | 20 | public async Task RestoreAsync(string inputFilePath, bool forceModulesRestore = false) 21 | { 22 | logger?.LogInformation("Restoring external modules to local cache for file {inputFilePath}", inputFilePath); 23 | var inputPath = PathHelper.ResolvePath(inputFilePath); 24 | var inputUri = PathHelper.FilePathToFileUrl(inputPath); 25 | 26 | var bicepCompiler = new BicepCompiler(featureProviderFactory, environment, namespaceProvider, configurationManager, bicepAnalyzer, fileResolver, moduleDispatcher); 27 | var compilation = await bicepCompiler.CreateCompilation(inputUri, workspace, true, forceModulesRestore); 28 | 29 | var originalModulesToRestore = compilation.SourceFileGrouping.GetArtifactsToRestore().ToImmutableHashSet(); 30 | 31 | // RestoreModules() does a distinct but we'll do it also to prevent duplicates in processing and logging 32 | var modulesToRestoreReferences = ArtifactHelper.GetValidArtifactReferences(originalModulesToRestore) 33 | .Distinct() 34 | .OrderBy(key => key.FullyQualifiedReference); 35 | 36 | // restore is supposed to only restore the module references that are syntactically valid 37 | await moduleDispatcher.RestoreArtifacts(modulesToRestoreReferences, forceModulesRestore); 38 | 39 | // update the errors based on restore status 40 | var sourceFileGrouping = SourceFileGroupingBuilder.Rebuild( 41 | fileResolver, 42 | featureProviderFactory, 43 | moduleDispatcher, 44 | configurationManager, 45 | workspace, 46 | compilation.SourceFileGrouping); 47 | 48 | LogDiagnostics(compilation); 49 | 50 | if (modulesToRestoreReferences.Any()) 51 | { 52 | logger?.LogInformation("Successfully restored modules in {inputFilePath}", inputFilePath); 53 | } 54 | else 55 | { 56 | logger?.LogInformation("No new modules to restore in {inputFilePath}", inputFilePath); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /BicepNet.Core/BicepWrapper.ExportResource.cs: -------------------------------------------------------------------------------- 1 | using BicepNet.Core.Azure; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Text.Json; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace BicepNet.Core; 9 | 10 | public partial class BicepWrapper 11 | { 12 | public IDictionary ExportResources(string[] ids, string? configurationPath = null, bool includeTargetScope = false) => 13 | joinableTaskFactory.Run(() => ExportResourcesAsync(ids, configurationPath, includeTargetScope)); 14 | 15 | public async Task> ExportResourcesAsync(string[] ids, string? configurationPath = null, bool includeTargetScope = false) 16 | { 17 | Dictionary result = []; 18 | var taskList = new List>(); 19 | foreach (string id in ids) 20 | { 21 | taskList.Add(ExportResourceAsync(id, configurationPath, includeTargetScope)); 22 | } 23 | foreach ((string name, string template) in await Task.WhenAll(taskList)) 24 | { 25 | if(string.IsNullOrEmpty(name)) { continue; } 26 | result.Add(name, template); 27 | } 28 | 29 | return result; 30 | } 31 | 32 | private async Task<(string resourceName, string template)> ExportResourceAsync(string id, string? configurationPath = null, bool includeTargetScope = false) 33 | { 34 | var resourceId = AzureHelpers.ValidateResourceId(id); 35 | resourceId.Deconstruct( 36 | out _, 37 | out string fullyQualifiedType, 38 | out _, 39 | out _, 40 | out _ 41 | ); 42 | var matchedType = BicepHelper.ResolveBicepTypeDefinition(fullyQualifiedType, azResourceTypeLoader, logger); 43 | 44 | JsonElement resource; 45 | try 46 | { 47 | var cancellationToken = new CancellationToken(); 48 | var config = configurationManager.GetConfiguration(new Uri(configurationPath ?? "inmemory://main.bicep")); 49 | resource = await azResourceProvider.GetGenericResource(config, resourceId, matchedType.ApiVersion, cancellationToken); 50 | } 51 | catch (Exception exception) 52 | { 53 | var message = $"Failed to fetch resource '{resourceId}' with API version {matchedType.ApiVersion}: {exception}"; 54 | throw new InvalidOperationException(message); 55 | } 56 | 57 | if(resource.ValueKind == JsonValueKind.Null) 58 | { 59 | return ("", ""); 60 | } 61 | 62 | string template = AzureResourceProvider.GenerateBicepTemplate(resourceId, matchedType, resource, includeTargetScope: includeTargetScope); 63 | template = RewriteBicepTemplate(template); 64 | var name = AzureHelpers.GetResourceFriendlyName(id); 65 | 66 | return (name, template); 67 | } 68 | } -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build, Test and Release 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | types: 8 | - opened 9 | - synchronize 10 | paths-ignore: 11 | - CHANGELOG.md 12 | 13 | push: 14 | branches: 15 | - main 16 | paths-ignore: 17 | - CHANGELOG.md 18 | - .github/** 19 | tags: [v*] 20 | 21 | env: 22 | buildFolderName: output 23 | buildArtifactName: output 24 | testResultFolderName: testResults 25 | 26 | jobs: 27 | build: 28 | name: Build 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout Code 32 | uses: actions/checkout@v4 33 | with: 34 | ref: ${{ github.head_ref }} 35 | fetch-depth: 0 36 | 37 | - name: 'Build and Package Module' 38 | uses: ./.github/actions/build 39 | 40 | test-linux: 41 | name: Test on Linux 42 | runs-on: ubuntu-latest 43 | needs: 44 | - build 45 | steps: 46 | - name: Checkout Code 47 | uses: actions/checkout@v4 48 | with: 49 | ref: ${{ github.head_ref }} 50 | fetch-depth: 0 51 | 52 | - name: 'Test on Linux' 53 | uses: ./.github/actions/test-linux 54 | 55 | code-coverage: 56 | name: Publish Code Coverage 57 | if: success() || failure() 58 | runs-on: ubuntu-latest 59 | needs: 60 | - build 61 | - test-linux 62 | # - test-win 63 | permissions: 64 | checks: write 65 | pull-requests: write 66 | steps: 67 | - name: Checkout Code 68 | uses: actions/checkout@v4 69 | with: 70 | ref: ${{ github.head_ref }} 71 | fetch-depth: 0 72 | - name: 'Publish Code Coverage' 73 | uses: ./.github/actions/code-coverage 74 | 75 | release: 76 | if: success() && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') 77 | runs-on: ubuntu-latest 78 | needs: 79 | - build 80 | - test-linux 81 | - code-coverage 82 | steps: 83 | - name: Checkout Code 84 | uses: actions/checkout@v4 85 | with: 86 | ref: ${{ github.head_ref }} 87 | fetch-depth: 0 88 | 89 | - name: Update PowerShell 90 | uses: bjompen/UpdatePWSHAction@v1.0.0 91 | with: 92 | ReleaseVersion: 'stable' 93 | 94 | - name: Download Build Artifact 95 | uses: actions/download-artifact@v4 96 | with: 97 | name: ${{ env.buildArtifactName }} 98 | path: ${{ env.buildFolderName }} 99 | 100 | - name: Publish Release 101 | shell: pwsh 102 | run: Import-Module ./output/RequiredModules/PowerShellForGitHub; ./build.ps1 -tasks publish 103 | env: 104 | GitHubToken: ${{ secrets.GITHUB_TOKEN }} 105 | GalleryApiToken: ${{ secrets.BICEPNET_PSGALLERY_KEY }} 106 | 107 | - name: Send Changelog PR 108 | shell: pwsh 109 | run: Get-Module -Name PowerShellForGitHub -ListAvailable ;./build.ps1 -tasks Create_ChangeLog_GitHub_PR 110 | env: 111 | GitHubToken: ${{ secrets.GITHUB_TOKEN }} 112 | -------------------------------------------------------------------------------- /BicepNet.Core/Configuration/BicepNetConfigurationManager.Custom.cs: -------------------------------------------------------------------------------- 1 | using Bicep.Core.Configuration; 2 | using Bicep.Core.Json; 3 | using BicepNet.Core.Models; 4 | using System; 5 | using System.IO; 6 | using System.Reflection; 7 | using System.Text.Json; 8 | 9 | namespace BicepNet.Core.Configuration; 10 | 11 | // Customizations and additions to out clone of Bicep.Core.Configuration.ConfigurationManager 12 | // Our code is kept in separate file to simplify maintenance 13 | public partial class BicepNetConfigurationManager 14 | { 15 | private const string BuiltInConfigurationResourceName = "BicepNet.Core.Configuration.bicepconfig.json"; 16 | 17 | public static BicepConfigInfo GetConfigurationInfo() 18 | { 19 | string configString = GetDefaultConfiguration().ToUtf8Json(); 20 | return new BicepConfigInfo("Default", configString); 21 | } 22 | public BicepConfigInfo GetConfigurationInfo(BicepConfigScope mode, Uri sourceFileUri) 23 | { 24 | RootConfiguration config; 25 | switch (mode) 26 | { 27 | case BicepConfigScope.Default: 28 | config = GetDefaultConfiguration(); 29 | return new BicepConfigInfo("Default", config.ToUtf8Json()); 30 | case BicepConfigScope.Merged: 31 | config = GetConfiguration(sourceFileUri); 32 | return new BicepConfigInfo(config.ConfigFileUri?.LocalPath ?? "Default", config.ToUtf8Json()); 33 | case BicepConfigScope.Local: 34 | config = GetConfiguration(sourceFileUri); 35 | if (config.ConfigFileUri is not null) 36 | { 37 | using var fileStream = fileSystem.FileStream.New(config.ConfigFileUri.LocalPath, FileMode.Open, FileAccess.Read); 38 | var configString = JsonElementFactory.CreateElementFromStream(fileStream).ToString(); 39 | return new BicepConfigInfo(config.ConfigFileUri.LocalPath, configString); 40 | } 41 | throw new FileNotFoundException("Local configuration file not found for path {path}!", sourceFileUri.LocalPath); 42 | default: 43 | throw new ArgumentException("BicepConfigMode not valid!"); 44 | } 45 | } 46 | 47 | // From Bicep.Core, implement GetBuiltInConfiguration to replace IConfigurationManager.GetBuiltInConfiguration() 48 | private static RootConfiguration GetDefaultConfiguration() => BuiltInConfigurationLazy.Value; 49 | 50 | private static readonly Lazy BuiltInConfigurationLazy = new(() => RootConfiguration.Bind(BuiltInConfigurationElement)); 51 | 52 | protected static readonly JsonElement BuiltInConfigurationElement = GetBuiltInConfigurationElement(); 53 | 54 | private static JsonElement GetBuiltInConfigurationElement() 55 | { 56 | using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(BuiltInConfigurationResourceName) ?? 57 | throw new InvalidOperationException("Could not get manifest resource stream for built-in configuration."); 58 | return JsonElementFactory.CreateElementFromStream(stream); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | schedule: 9 | - cron: '42 12 * * 0' 10 | 11 | env: 12 | buildFolderName: output 13 | buildArtifactName: output 14 | 15 | jobs: 16 | analyze: 17 | name: Analyze 18 | # Runner size impacts CodeQL analysis time. To learn more, please see: 19 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 20 | # - https://gh.io/supported-runners-and-hardware-resources 21 | # - https://gh.io/using-larger-runners 22 | # Consider using larger runners for possible analysis time improvements. 23 | runs-on: 'ubuntu-latest' 24 | timeout-minutes: 360 25 | permissions: 26 | actions: read 27 | contents: read 28 | security-events: write 29 | 30 | strategy: 31 | fail-fast: false 32 | matrix: 33 | language: [ 'csharp' ] 34 | # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] 35 | # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both 36 | # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | with: 43 | fetch-depth: 0 44 | 45 | - name: Update PowerShell 46 | uses: bjompen/UpdatePWSHAction@v1.0.0 47 | with: 48 | ReleaseVersion: 'stable' 49 | 50 | - name: Setup.NET Core 51 | uses: actions/setup-dotnet@v4 52 | with: 53 | dotnet-version: '8.0' 54 | 55 | - name: Install GitVersion 56 | uses: gittools/actions/gitversion/setup@v1.1.1 57 | with: 58 | versionSpec: '5.x' 59 | 60 | - name: Determine Version 61 | id: gitversion 62 | uses: gittools/actions/gitversion/execute@v1.1.1 63 | 64 | - name: Setup assets cache 65 | id: assetscache 66 | uses: actions/cache@v4 67 | with: 68 | path: output/RequiredModules 69 | key: ${{ hashFiles('RequiredModules.psd1') }} 70 | 71 | - name: Download required dependencies 72 | if: steps.assetscache.outputs.cache-hit != 'true' 73 | shell: pwsh 74 | run: ./build.ps1 -ResolveDependency 75 | 76 | # Initializes the CodeQL tools for scanning. 77 | - name: Initialize CodeQL 78 | uses: github/codeql-action/init@v3 79 | with: 80 | languages: ${{ matrix.language }} 81 | # If you wish to specify custom queries, you can do so here or in a config file. 82 | # By default, queries listed here will override any specified in a config file. 83 | # Prefix the list here with "+" to use these queries and those in the config file. 84 | 85 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 86 | # queries: security-extended,security-and-quality 87 | 88 | - name: Build module 89 | shell: pwsh 90 | run: ./build.ps1 -tasks package -Verbose 91 | env: 92 | ModuleVersion: ${{ env.gitVersion.NuGetVersionV2 }} 93 | 94 | - name: Perform CodeQL Analysis 95 | uses: github/codeql-action/analyze@v3 96 | with: 97 | category: "/language:${{matrix.language}}" 98 | -------------------------------------------------------------------------------- /BicepNet.Core/Azure/SubscriptionHelper.cs: -------------------------------------------------------------------------------- 1 | using Azure.Core; 2 | using Azure.ResourceManager; 3 | using Azure.ResourceManager.Subscription; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Net.Http; 7 | using System.Net.Http.Headers; 8 | using System.Runtime.CompilerServices; 9 | using System.Text.Json; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | namespace BicepNet.Core.Azure; 14 | 15 | internal static class SubscriptionHelper 16 | { 17 | public static async Task GetManagementGroupSubscriptionAsync(ResourceIdentifier resourceIdentifier, AccessToken accessToken, CancellationToken cancellationToken) 18 | { 19 | var Uri = $"https://management.azure.com/{resourceIdentifier}?api-version=2020-05-01"; 20 | using HttpClient client = new(); 21 | client.DefaultRequestHeaders.Accept.Clear(); 22 | client.DefaultRequestHeaders.Accept.Add( 23 | new MediaTypeWithQualityHeaderValue("application/json")); 24 | client.DefaultRequestHeaders.Add("User-Agent", ".NET Foundation Repository Reporter"); 25 | client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", accessToken.Token);//$"Bearer {accessToken.Token}"; 26 | try 27 | { 28 | var json = await client.GetStringAsync(Uri,cancellationToken); 29 | return JsonDocument.Parse(json).RootElement; 30 | } 31 | catch (Exception e) 32 | { 33 | if (e.Message.EndsWith("404 (Not Found).")) 34 | { 35 | return default; 36 | } 37 | throw; 38 | } 39 | } 40 | 41 | public static async IAsyncEnumerable ListManagementGroupSubscriptionsAsync(ResourceIdentifier resourceIdentifier, AccessToken accessToken, [EnumeratorCancellation] CancellationToken cancellationToken) 42 | { 43 | if (resourceIdentifier.ResourceType != "Microsoft.Management/managementGroups") 44 | { 45 | throw new ArgumentException("Invalid resource type, must be \"Microsoft.Management/managementGroups\""); 46 | } 47 | 48 | var Uri = $"https://management.azure.com/{resourceIdentifier}/subscriptions?api-version=2020-05-01"; 49 | using HttpClient client = new(); 50 | client.DefaultRequestHeaders.Accept.Clear(); 51 | client.DefaultRequestHeaders.Accept.Add( 52 | new MediaTypeWithQualityHeaderValue("application/json")); 53 | client.DefaultRequestHeaders.Add("User-Agent", ".NET Foundation Repository Reporter"); 54 | client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", accessToken.Token); 55 | 56 | var json = await client.GetStringAsync(Uri, cancellationToken); 57 | using var result = JsonDocument.Parse(json); 58 | foreach (JsonElement element in result.RootElement.EnumerateArray()) 59 | { 60 | yield return element; 61 | } 62 | 63 | } 64 | 65 | public static async Task GetSubscriptionAsync(ResourceIdentifier resourceIdentifier, ArmClient armClient, CancellationToken cancellationToken) 66 | { 67 | var subId = SubscriptionAliasResource.CreateResourceIdentifier(resourceIdentifier.SubscriptionId); 68 | var sub = armClient.GetSubscriptionAliasResource(subId); 69 | 70 | var subResponse = await sub.GetAsync(cancellationToken: cancellationToken); 71 | if (subResponse is null || subResponse.GetRawResponse().ContentStream is not { } subContentStream) 72 | { 73 | throw new InvalidOperationException($"Failed to fetch resource from Id '{resourceIdentifier}'"); 74 | } 75 | subContentStream.Position = 0; 76 | return await JsonSerializer.DeserializeAsync(subContentStream, cancellationToken: cancellationToken); 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /BicepNet.Core/BicepWrapper.Build.cs: -------------------------------------------------------------------------------- 1 | using Bicep.Core.Emit; 2 | using Bicep.Core.FileSystem; 3 | using Bicep.Core.Semantics; 4 | using Bicep.Core.Workspaces; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.IO; 8 | using System.Threading.Tasks; 9 | 10 | namespace BicepNet.Core; 11 | 12 | public partial class BicepWrapper 13 | { 14 | public IList Build(string bicepPath, string usingPath = "", bool noRestore = false) => joinableTaskFactory.Run(() => BuildAsync(bicepPath, usingPath, noRestore)); 15 | 16 | public async Task> BuildAsync(string bicepPath, string usingPath = "", bool noRestore = false) 17 | { 18 | var inputPath = PathHelper.ResolvePath(bicepPath); 19 | var inputUri = PathHelper.FilePathToFileUrl(inputPath); 20 | 21 | if (!IsBicepFile(inputUri) && !IsBicepparamsFile(inputUri)) 22 | { 23 | throw new InvalidOperationException($"Input file '{inputPath}' must have a .bicep or .bicepparam extension."); 24 | } 25 | 26 | var compilation = await compiler.CreateCompilation(inputUri, skipRestore: noRestore); 27 | 28 | var summary = LogDiagnostics(compilation); 29 | 30 | if (diagnosticLogger is not null && summary.HasErrors) 31 | { 32 | throw new InvalidOperationException($"Failed to compile file: {inputPath}"); 33 | } 34 | 35 | var fileKind = compilation.SourceFileGrouping.EntryPoint.FileKind; 36 | 37 | var stream = new MemoryStream(); 38 | EmitResult emitresult = fileKind switch 39 | { 40 | BicepSourceFileKind.BicepFile => new TemplateEmitter(compilation.GetEntrypointSemanticModel()).Emit(stream), 41 | BicepSourceFileKind.ParamsFile => EmitParamsFile(compilation, usingPath, stream), 42 | _ => throw new NotImplementedException($"Unexpected file kind '{fileKind}'"), 43 | }; 44 | 45 | if (emitresult.Status != EmitStatus.Succeeded) 46 | { 47 | throw new InvalidOperationException($"Failed to emit bicep with error: ${emitresult.Status}"); 48 | } 49 | 50 | stream.Position = 0; 51 | using var reader = new StreamReader(stream); 52 | var result = await reader.ReadToEndAsync(); 53 | 54 | var template = new List 55 | { 56 | result 57 | }; 58 | return template; 59 | } 60 | 61 | private static bool IsBicepFile(Uri inputUri) => PathHelper.HasBicepExtension(inputUri); 62 | private static bool IsBicepparamsFile(Uri inputUri) => PathHelper.HasBicepparamsExtension(inputUri); 63 | 64 | private static EmitResult EmitParamsFile(Compilation compilation, string usingPath, Stream stream) 65 | { 66 | var bicepPath = PathHelper.ResolvePath(usingPath); 67 | var paramsSemanticModel = compilation.GetEntrypointSemanticModel(); 68 | if (usingPath != "" && paramsSemanticModel.Root.TryGetBicepFileSemanticModelViaUsing().IsSuccess(out var usingModel)) 69 | { 70 | if (usingModel is not SemanticModel bicepSemanticModel) 71 | { 72 | throw new InvalidOperationException($"Bicep file {bicepPath} provided can only be used if the Bicep parameters \"using\" declaration refers to a Bicep file on disk."); 73 | } 74 | 75 | var bicepFileUsingPathUri = bicepSemanticModel.Root.FileUri; 76 | 77 | if (bicepPath is not null && !bicepFileUsingPathUri.Equals(PathHelper.FilePathToFileUrl(bicepPath))) 78 | { 79 | throw new InvalidOperationException($"Bicep file {bicepPath} provided with templatePath option doesn't match the Bicep file {bicepSemanticModel?.Root.Name} referenced by the using declaration in the parameters file"); 80 | } 81 | 82 | } 83 | var emitter = new ParametersEmitter(paramsSemanticModel); 84 | return emitter.Emit(stream); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to BicepNet 2 | 3 | BicepNet is a thin wrapper around bicep that solves the problem with conflicting assemblies. 4 | The techique is sourced from this blog post: [Resolving PowerShell module assembly dependency conflicts](https://docs.microsoft.com/en-us/powershell/scripting/dev-cross-plat/resolving-dependency-conflicts?view=powershell-7.1) 5 | 6 | BicepNet has two parts, BicepNet.Core and BicepNet.PS. 7 | 8 | ## BicepNet.PS 9 | 10 | BicepNet.PS is a PowerShell module written i C# that creates an assembly load context and loads any dependencies of BicepNet.Core into that context. 11 | This will resolve any conflicts with other PowerShell modules that depends on different versions of the same DLL-files as Bicep does. 12 | 13 | The goal of BicepNet.PS is also to translate any functionality exposed by BicepNet.Core as PowerShell cmdlets. 14 | 15 | Each cmdlet is defined in a separate file in the Commands folder. The more generic load context code is in the LoadContext folder. 16 | 17 | ## BicepNet.Core 18 | 19 | BicepNet.Core will have all it's dependencies loaded into the assembly load context created by BicepNet.PS. That means that PowerShell will not be able to access anything defined directly in bicep or any of it's dependencies. To solve this, BicepNet.Core has to implement and expose any functionallity we want to use from bicep. We have chosen to expose these as static methods in the class BicepNet.Core.BicepWrapper. Each method is implemented in it's own file in the BicepNet.Core folder root. 20 | 21 | Any class needed for data returned to BicepNet.PS needs to be defined in the BicepNet.Core as well. These are defined as classes in the Modules folder of BicepNet.Core. 22 | 23 | ## Setting up a dev-environment - devcontainer 24 | 25 | The project ships with a dev container, it should contain everything needed. 26 | The container build process will clone and build Bicep which takes some time, please be patient. 27 | 28 | **Known Issues:** 29 | * There might be an error stating that it failed to restore projects. This is because the language servers tries to restore the project before the dependencies are downloaded. Just ignore the error and restart the container or reload the workspace and it should work as expected. 30 | 31 | ## Setting up a dev-environment - local development 32 | 33 | ### Prerequisites for local setup 34 | 35 | 1. git ([Download](https://git-scm.com/downloads)) 36 | 1. dotnet 7 SDK ([Download](https://dotnet.microsoft.com/download)) 37 | 1. Visual Studio Code ([Download](https://code.visualstudio.com/download)) 38 | 1. (optional) gitversion ([Download](https://gitversion.net/docs/usage/cli/installation)) 39 | 40 | ### Set up project 41 | 42 | 1. Make sure your execution policy allows execution of PowerShell scripts. 43 | `Set-ExecutionPolicy -ExecutionPolicy 'Unrestricted' -Scope 'CurrentUser' -Force` 44 | 1. Clone BicepNet repository to your local machine. 45 | `git clone https://github.com/PSBicep/BicepNet.git` 46 | 1. Run `./build.ps1 -ResolveDependency` to download any dependencies. This will download the Bicep source code and a few PowerShell modules used for building. 47 | To checkout a specific version of bicep, update the value for entry 'Azure/Bicep' in `RequiredModules.psd1` with desired git tag, branch or commit hash. 48 | 1. Build the project by running `./build.ps1`, this will build the module to output/BicepNet.PS and execute all tests. 49 | 50 | Now you are ready to start coding! 51 | 52 | ### Update parameters test baseline 53 | We are running an autogenerated test suite intended to pick up on breaking changes on parameters. The current parameter configuration is defined in 'BicepNet.PS/tests/BicepNet.PS.ParameterTests.json'. There is a build task that updates this file to current parameter configuration. If a parameter is changed, added or deleted, re-generate this file by running the command `./build.ps1 GenerateTestCases` 54 | 55 | ### Build 56 | To build BicepNet, run the script `./build.ps1 build`. 57 | This will create the folder 'output' containing a BicepNet.PS module that is ready to be loaded. 58 | To use the new version of BicepNet in PSBicep, copy the module to the PSBicep module folder. 59 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "_runner": "terminal", 4 | "windows": { 5 | "options": { 6 | "shell": { 7 | "executable": "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", 8 | "args": [ 9 | "-NoProfile", 10 | "-ExecutionPolicy", 11 | "Bypass", 12 | "-Command" 13 | ] 14 | } 15 | } 16 | }, 17 | "linux": { 18 | "options": { 19 | "shell": { 20 | "executable": "/usr/bin/pwsh", 21 | "args": [ 22 | "-NoProfile", 23 | "-Command" 24 | ] 25 | } 26 | } 27 | }, 28 | "osx": { 29 | "options": { 30 | "shell": { 31 | "executable": "/usr/local/bin/pwsh", 32 | "args": [ 33 | "-NoProfile", 34 | "-Command" 35 | ] 36 | } 37 | } 38 | }, 39 | "tasks": [ 40 | { 41 | "label": "build", 42 | "type": "shell", 43 | "command": "&${cwd}/build.ps1", 44 | "args": [], 45 | "presentation": { 46 | "echo": true, 47 | "reveal": "always", 48 | "focus": true, 49 | "panel": "new", 50 | "clear": false 51 | }, 52 | "runOptions": { 53 | "runOn": "default" 54 | }, 55 | "problemMatcher": [ 56 | { 57 | "owner": "powershell", 58 | "fileLocation": [ 59 | "absolute" 60 | ], 61 | "severity": "error", 62 | "pattern": [ 63 | { 64 | "regexp": "^\\s*(\\[-\\]\\s*.*?)(\\d+)ms\\s*$", 65 | "message": 1 66 | }, 67 | { 68 | "regexp": "(.*)", 69 | "code": 1 70 | }, 71 | { 72 | "regexp": "" 73 | }, 74 | { 75 | "regexp": "^.*,\\s*(.*):\\s*line\\s*(\\d+).*", 76 | "file": 1, 77 | "line": 2 78 | } 79 | ] 80 | } 81 | ] 82 | }, 83 | { 84 | "label": "test", 85 | "type": "shell", 86 | "command": "&${cwd}/build.ps1", 87 | "args": ["-AutoRestore","-Tasks","test"], 88 | "presentation": { 89 | "echo": true, 90 | "reveal": "always", 91 | "focus": true, 92 | "panel": "dedicated", 93 | "showReuseMessage": true, 94 | "clear": false 95 | }, 96 | "problemMatcher": [ 97 | { 98 | "owner": "powershell", 99 | "fileLocation": [ 100 | "absolute" 101 | ], 102 | "severity": "error", 103 | "pattern": [ 104 | { 105 | "regexp": "^\\s*(\\[-\\]\\s*.*?)(\\d+)ms\\s*$", 106 | "message": 1 107 | }, 108 | { 109 | "regexp": "(.*)", 110 | "code": 1 111 | }, 112 | { 113 | "regexp": "" 114 | }, 115 | { 116 | "regexp": "^.*,\\s*(.*):\\s*line\\s*(\\d+).*", 117 | "file": 1, 118 | "line": 2 119 | } 120 | ] 121 | } 122 | ] 123 | } 124 | ] 125 | } 126 | -------------------------------------------------------------------------------- /BicepNet.PS/BicepNet.PS.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | 3 | # Script or binary modules required by this module 4 | NestedModules = 'BicepNet.PS/BicepNet.PS.dll' 5 | 6 | # Version number of this module. 7 | ModuleVersion = '2.3.1' 8 | 9 | # Supported PSEditions 10 | CompatiblePSEditions = @('Core') 11 | 12 | # ID used to uniquely identify this module 13 | GUID = 'e36e6970-03f9-46ba-961f-86aae67933f8' 14 | 15 | # Author of this module 16 | Author = 'Simon Wåhlin' 17 | 18 | # Company or vendor of this module 19 | CompanyName = 'simonw.se' 20 | 21 | # Copyright statement for this module 22 | Copyright = '(c) 2023 Simon Wåhlin. All rights reserved.' 23 | 24 | # Description of the functionality provided by this module 25 | Description = 'A thin wrapper around bicep that will load all Bicep assemblies in a separate context to avoid conflicts with other modules. 26 | BicepNet is developed for the Bicep PowerShell module but could be used for any other project where you want to leverage Bicep functionality in PowerShell or .NET.' 27 | 28 | # Minimum version of the PowerShell engine required by this module 29 | PowerShellVersion = '7.3' 30 | 31 | # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 32 | # DotNetFrameworkVersion = '4.6.1' 33 | 34 | # Assemblies that must be loaded prior to importing this module 35 | RequiredAssemblies = @() 36 | 37 | # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. 38 | FunctionsToExport = @() 39 | 40 | # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. 41 | CmdletsToExport = @( 42 | 'Build-BicepNetFile' 43 | 'Build-BicepNetParamFile' 44 | 'Clear-BicepNetCredential' 45 | 'Convert-BicepNetResourceToBicep' 46 | 'ConvertTo-BicepNetFile' 47 | 'Export-BicepNetResource' 48 | 'Export-BicepNetChildResource' 49 | 'Find-BicepNetModule' 50 | 'Format-BicepNet' 51 | 'Get-BicepNetAccessToken' 52 | 'Get-BicepNetCachePath' 53 | 'Get-BicepNetConfig' 54 | 'Get-BicepNetVersion' 55 | 'Publish-BicepNetFile' 56 | 'Restore-BicepNetFile' 57 | 'Set-BicepNetCredential' 58 | ) 59 | 60 | # Variables to export from this module 61 | VariablesToExport = @() 62 | 63 | # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. 64 | AliasesToExport = @() 65 | 66 | # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. 67 | PrivateData = @{ 68 | PSData = @{ 69 | 70 | # Tags applied to this module. These help with module discovery in online galleries. 71 | Tags = @('azure', 'bicep', 'arm-json', 'arm-templates', 'windows', 'linux', 'bicepnet', 'psbicep') 72 | 73 | # A URL to the license for this module. 74 | LicenseUri = 'https://github.com/PSBicep/BicepNet/blob/main/LICENSE' 75 | 76 | #A URL to the main website for this project. 77 | ProjectUri = 'https://github.com/PSBicep/BicepNet' 78 | 79 | # A URL to an icon representing this module. 80 | IconUri = 'https://raw.githubusercontent.com/PSBicep/PSBicep/main/logo/BicePS.png' 81 | 82 | # ReleaseNotes of this module 83 | ReleaseNotes = 'https://github.com/PSBicep/BicepNet/releases' 84 | 85 | # Prerelease string of this module 86 | Prerelease = '' 87 | 88 | # Flag to indicate whether the module requires explicit user acceptance for install/update/save 89 | # RequireLicenseAcceptance = $false 90 | 91 | # External dependent modules of this module 92 | # ExternalModuleDependencies = @() 93 | } # End of PSData hashtable 94 | 95 | } # End of PrivateData hashtable 96 | 97 | } 98 | -------------------------------------------------------------------------------- /BicepNet.Core/BicepWrapper.Publish.cs: -------------------------------------------------------------------------------- 1 | using Bicep.Core; 2 | using Bicep.Core.Diagnostics; 3 | using Bicep.Core.Emit; 4 | using Bicep.Core.Exceptions; 5 | using Bicep.Core.FileSystem; 6 | using Bicep.Core.Registry; 7 | using Bicep.Core.SourceCode; 8 | using System; 9 | using System.IO; 10 | using System.Threading.Tasks; 11 | 12 | namespace BicepNet.Core; 13 | 14 | public partial class BicepWrapper 15 | { 16 | public void Publish(string inputFilePath, string targetModuleReference, string? documentationUri, bool overwriteIfExists = false) => 17 | joinableTaskFactory.Run(() => PublishAsync(inputFilePath, targetModuleReference, documentationUri, overwriteIfExists)); 18 | 19 | public async Task PublishAsync(string inputFilePath, string targetModuleReference, string? documentationUri, bool overwriteIfExists = false) 20 | { 21 | var inputPath = PathHelper.ResolvePath(inputFilePath); 22 | var inputUri = PathHelper.FilePathToFileUrl(inputPath); 23 | ArtifactReference? moduleReference = ValidateReference(targetModuleReference, inputUri); 24 | 25 | if (PathHelper.HasArmTemplateLikeExtension(inputUri)) 26 | { 27 | // Publishing an ARM template file. 28 | using var armTemplateStream = fileSystem.FileStream.New(inputPath, FileMode.Open, FileAccess.Read); 29 | await this.PublishModuleAsync(moduleReference, armTemplateStream, null, documentationUri, overwriteIfExists); 30 | return; 31 | } 32 | 33 | var bicepCompiler = new BicepCompiler(featureProviderFactory, environment, namespaceProvider, configurationManager, bicepAnalyzer, fileResolver, moduleDispatcher); 34 | var compilation = await bicepCompiler.CreateCompilation(inputUri, workspace); 35 | 36 | using var sourcesStream = SourceArchive.PackSourcesIntoStream(moduleDispatcher, compilation.SourceFileGrouping, null); 37 | 38 | if (!LogDiagnostics(compilation).HasErrors) 39 | { 40 | var stream = new MemoryStream(); 41 | new TemplateEmitter(compilation.GetEntrypointSemanticModel()).Emit(stream); 42 | 43 | stream.Position = 0; 44 | await PublishModuleAsync(moduleReference, stream, sourcesStream, documentationUri, overwriteIfExists); 45 | } 46 | } 47 | 48 | // copied from PublishCommand.cs 49 | private ArtifactReference ValidateReference(string targetModuleReference, Uri targetModuleUri) 50 | { 51 | if (!this.moduleDispatcher.TryGetModuleReference(targetModuleReference, targetModuleUri).IsSuccess(out var moduleReference, out var failureBuilder)) 52 | { 53 | // TODO: We should probably clean up the dispatcher contract so this sort of thing isn't necessary (unless we change how target module is set in this command) 54 | var message = failureBuilder(DiagnosticBuilder.ForDocumentStart()).Message; 55 | 56 | throw new BicepException(message); 57 | } 58 | 59 | if (!this.moduleDispatcher.GetRegistryCapabilities(moduleReference).HasFlag(RegistryCapabilities.Publish)) 60 | { 61 | throw new BicepException($"The specified module target \"{targetModuleReference}\" is not supported."); 62 | } 63 | 64 | return moduleReference; 65 | } 66 | 67 | // copied from PublishCommand.cs 68 | private async Task PublishModuleAsync(ArtifactReference target, Stream compiledArmTemplate, Stream? bicepSources, string? documentationUri, bool overwriteIfExists) 69 | { 70 | try 71 | { 72 | // If we don't want to overwrite, ensure module doesn't exist 73 | if (!overwriteIfExists && await this.moduleDispatcher.CheckModuleExists(target)) 74 | { 75 | throw new BicepException($"The module \"{target.FullyQualifiedReference}\" already exists in registry. Use -Force to overwrite the existing module."); 76 | } 77 | var binaryBicepSources = bicepSources == null ? null : BinaryData.FromStream(bicepSources); 78 | await this.moduleDispatcher.PublishModule(target, BinaryData.FromStream(compiledArmTemplate), binaryBicepSources, documentationUri); 79 | } 80 | catch (ExternalArtifactException exception) 81 | { 82 | throw new BicepException($"Unable to publish module \"{target.FullyQualifiedReference}\": {exception.Message}"); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | output/ 2 | out/ 3 | tmp/ 4 | TMP/ 5 | coverage.xml 6 | testResults.xml 7 | *.local.* 8 | 9 | 10 | # The following command works for downloading when using Git for Windows: 11 | # curl -LOf http://gist.githubusercontent.com/kmorcinek/2710267/raw/.gitignore 12 | # 13 | # Download this file using PowerShell v3 under Windows with the following comand: 14 | # Invoke-WebRequest https://gist.githubusercontent.com/kmorcinek/2710267/raw/ -OutFile .gitignore 15 | # 16 | # or wget: 17 | # wget --no-check-certificate http://gist.githubusercontent.com/kmorcinek/2710267/raw/.gitignore 18 | 19 | # User-specific files 20 | *.suo 21 | *.user 22 | *.sln.docstates 23 | 24 | # Build results 25 | [Dd]ebug/ 26 | [Rr]elease/ 27 | x64/ 28 | [Bb]in/ 29 | [Oo]bj/ 30 | # build folder is nowadays used for build scripts and should not be ignored 31 | #build/ 32 | 33 | # NuGet Packages 34 | *.nupkg 35 | # The packages folder can be ignored because of Package Restore 36 | **/packages/* 37 | # except build/, which is used as an MSBuild target. 38 | !**/packages/build/ 39 | # Uncomment if necessary however generally it will be regenerated when needed 40 | #!**/packages/repositories.config 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | *_i.c 47 | *_p.c 48 | *.ilk 49 | *.meta 50 | *.obj 51 | *.pch 52 | *.pdb 53 | *.pgc 54 | *.pgd 55 | *.rsp 56 | *.sbr 57 | *.tlb 58 | *.tli 59 | *.tlh 60 | *.tmp 61 | *.tmp_proj 62 | *.log 63 | *.vspscc 64 | *.vssscc 65 | .builds 66 | *.pidb 67 | *.log 68 | *.scc 69 | 70 | # OS generated files # 71 | .DS_Store* 72 | Icon? 73 | 74 | # Visual C++ cache files 75 | ipch/ 76 | *.aps 77 | *.ncb 78 | *.opensdf 79 | *.sdf 80 | *.cachefile 81 | 82 | # Visual Studio profiler 83 | *.psess 84 | *.vsp 85 | *.vspx 86 | 87 | # Guidance Automation Toolkit 88 | *.gpState 89 | 90 | # ReSharper is a .NET coding add-in 91 | _ReSharper*/ 92 | *.[Rr]e[Ss]harper 93 | 94 | # TeamCity is a build add-in 95 | _TeamCity* 96 | 97 | # DotCover is a Code Coverage Tool 98 | *.dotCover 99 | 100 | # NCrunch 101 | *.ncrunch* 102 | .*crunch*.local.xml 103 | 104 | # Installshield output folder 105 | [Ee]xpress/ 106 | 107 | # DocProject is a documentation generator add-in 108 | DocProject/buildhelp/ 109 | DocProject/Help/*.HxT 110 | DocProject/Help/*.HxC 111 | DocProject/Help/*.hhc 112 | DocProject/Help/*.hhk 113 | DocProject/Help/*.hhp 114 | DocProject/Help/Html2 115 | DocProject/Help/html 116 | 117 | # Click-Once directory 118 | publish/ 119 | 120 | # Publish Web Output 121 | *.Publish.xml 122 | 123 | # Windows Azure Build Output 124 | csx 125 | *.build.csdef 126 | 127 | # Windows Store app package directory 128 | AppPackages/ 129 | 130 | # Others 131 | *.Cache 132 | ClientBin/ 133 | [Ss]tyle[Cc]op.* 134 | ~$* 135 | *~ 136 | *.dbmdl 137 | *.[Pp]ublish.xml 138 | *.pfx 139 | *.publishsettings 140 | modulesbin/ 141 | tempbin/ 142 | 143 | # EPiServer Site file (VPP) 144 | AppData/ 145 | 146 | # RIA/Silverlight projects 147 | Generated_Code/ 148 | 149 | # Backup & report files from converting an old project file to a newer 150 | # Visual Studio version. Backup files are not needed, because we have git ;-) 151 | _UpgradeReport_Files/ 152 | Backup*/ 153 | UpgradeLog*.XML 154 | UpgradeLog*.htm 155 | 156 | # vim 157 | *.txt~ 158 | *.swp 159 | *.swo 160 | 161 | # Temp files when opening LibreOffice on ubuntu 162 | .~lock.* 163 | 164 | # svn 165 | .svn 166 | 167 | # CVS - Source Control 168 | **/CVS/ 169 | 170 | # Remainings from resolving conflicts in Source Control 171 | *.orig 172 | 173 | # SQL Server files 174 | **/App_Data/*.mdf 175 | **/App_Data/*.ldf 176 | **/App_Data/*.sdf 177 | 178 | 179 | #LightSwitch generated files 180 | GeneratedArtifacts/ 181 | _Pvt_Extensions/ 182 | ModelManifest.xml 183 | 184 | # ========================= 185 | # Windows detritus 186 | # ========================= 187 | 188 | # Windows image file caches 189 | Thumbs.db 190 | ehthumbs.db 191 | 192 | # Folder config file 193 | Desktop.ini 194 | 195 | # Recycle Bin used on file shares 196 | $RECYCLE.BIN/ 197 | 198 | # Mac desktop service store files 199 | .DS_Store 200 | 201 | # SASS Compiler cache 202 | .sass-cache 203 | 204 | # Visual Studio 2014 CTP 205 | **/*.sln.ide 206 | 207 | # Visual Studio temp something 208 | .vs/ 209 | 210 | # dotnet stuff 211 | project.lock.json 212 | 213 | # VS 2015+ 214 | *.vc.vc.opendb 215 | *.vc.db 216 | 217 | # Rider 218 | .idea/ 219 | 220 | # Visual Studio Code 221 | .vscode/ 222 | !.vscode/analyzersettings.psd1 223 | !.vscode/settings.json 224 | !.vscode/tasks.json 225 | 226 | # Output folder used by Webpack or other FE stuff 227 | **/node_modules/* 228 | **/wwwroot/* 229 | 230 | # SpecFlow specific 231 | *.feature.cs 232 | *.feature.xlsx.* 233 | *.Specs_*.html 234 | 235 | ##### 236 | # End of core ignore list, below put you custom 'per project' settings (patterns or path) 237 | ##### 238 | 239 | BicepNet.PS.zip 240 | BicepNet.Core/Properties/launchSettings.json 241 | BicepNet.CLI/Properties/launchSettings.json 242 | -------------------------------------------------------------------------------- /BicepNet.Core/Authentication/BicepNetTokenCredentialFactory.cs: -------------------------------------------------------------------------------- 1 | using Azure.Core; 2 | using Azure.Identity; 3 | using Bicep.Core.Configuration; 4 | using Bicep.Core.Registry.Auth; 5 | using Microsoft.Extensions.Logging; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.IdentityModel.Tokens.Jwt; 9 | using System.Linq; 10 | 11 | namespace BicepNet.Core.Authentication; 12 | 13 | public class BicepNetTokenCredentialFactory : ITokenCredentialFactory 14 | { 15 | public static string Scope { get; } = "https://management.core.windows.net/.default"; 16 | 17 | internal ILogger? Logger { get; set; } 18 | internal TokenRequestContext TokenRequestContext { get; set; } 19 | internal TokenCredential? Credential { get; set; } 20 | internal bool InteractiveAuthentication { get; set; } 21 | 22 | public TokenCredential CreateChain(IEnumerable credentialPrecedence, CredentialOptions? credentialOptions, Uri authorityUri) 23 | { 24 | // Return the credential if already authenticated in BicepNet 25 | if (Credential is not null) 26 | { 27 | return Credential; 28 | } 29 | 30 | // If not authenticated, ensure BicepConfig has a precedence 31 | if (!credentialPrecedence.Any()) 32 | { 33 | throw new ArgumentException($"At least one credential type must be provided."); 34 | } 35 | 36 | // Authenticate using BicepConfig precedence 37 | return new ChainedTokenCredential(credentialPrecedence.Select(credentialType => CreateSingle(credentialType, null, authorityUri)).ToArray()); 38 | } 39 | 40 | public TokenCredential CreateSingle(CredentialType credentialType, CredentialOptions? credentialOptions, Uri authorityUri) 41 | { 42 | Credential = credentialType switch 43 | { 44 | CredentialType.Environment => new EnvironmentCredential(new() { AuthorityHost = authorityUri }), 45 | CredentialType.ManagedIdentity => new ManagedIdentityCredential(options: new() { AuthorityHost = authorityUri }), 46 | CredentialType.VisualStudio => new VisualStudioCredential(new() { AuthorityHost = authorityUri }), 47 | CredentialType.VisualStudioCode => new VisualStudioCodeCredential(new() { AuthorityHost = authorityUri }), 48 | CredentialType.AzureCLI => new AzureCliCredential(),// AzureCLICrediential does not accept options. Azure CLI has built-in cloud profiles so AuthorityHost is not needed. 49 | CredentialType.AzurePowerShell => new AzurePowerShellCredential(new() { AuthorityHost = authorityUri }), 50 | _ => throw new NotImplementedException($"Unexpected credential type '{credentialType}'."), 51 | }; 52 | 53 | return Credential; 54 | } 55 | 56 | internal void Clear() 57 | { 58 | InteractiveAuthentication = false; 59 | 60 | if (Credential == null) 61 | { 62 | Logger?.LogInformation("No stored credential to clear."); 63 | return; 64 | } 65 | 66 | Credential = null; 67 | Logger?.LogInformation("Cleared stored credential."); 68 | } 69 | 70 | internal void SetToken(Uri activeDirectoryAuthorityUri, string? token = null, string? tenantId = null) 71 | { 72 | // User provided a token 73 | if (!string.IsNullOrWhiteSpace(token)) 74 | { 75 | Logger?.LogInformation("Token provided as authentication."); 76 | InteractiveAuthentication = false; 77 | 78 | // Try to parse JWT for expiry date 79 | try 80 | { 81 | var handler = new JwtSecurityTokenHandler(); 82 | var jwtSecurityToken = handler.ReadJwtToken(token); 83 | var tokenExp = jwtSecurityToken.Claims.First(claim => claim.Type.Equals("exp")).Value; 84 | var expDateTime = DateTimeOffset.FromUnixTimeSeconds(long.Parse(tokenExp)); 85 | 86 | Logger?.LogInformation("Successfully parsed token, expiration date is {expDateTime}.", expDateTime); 87 | Credential = new ExternalTokenCredential(token, expDateTime); 88 | } 89 | catch (Exception ex) 90 | { 91 | throw new InvalidOperationException("Could not parse token as JWT, please ensure it is provided in the correct format!", ex); 92 | } 93 | } 94 | else // User did not provide a token - interactive auth 95 | { 96 | Logger?.LogInformation("Opening interactive browser for authentication..."); 97 | 98 | // Since we cannot change the method signatures of the ITokenCredentialFactory, set properties and check them within the class 99 | InteractiveAuthentication = true; 100 | Credential = new InteractiveBrowserCredential(options: new() { AuthorityHost = activeDirectoryAuthorityUri }); 101 | TokenRequestContext = new TokenRequestContext([Scope], tenantId: tenantId); 102 | 103 | // Get token immediately to trigger browser prompt, instead of waiting until the credential is used 104 | // The token is then stored in the Credential object, here we don't care about the return value 105 | GetToken(); 106 | 107 | Logger?.LogInformation("Authentication successful."); 108 | } 109 | } 110 | 111 | public AccessToken? GetToken() 112 | { 113 | return Credential?.GetToken(TokenRequestContext, System.Threading.CancellationToken.None); 114 | } 115 | } -------------------------------------------------------------------------------- /BicepNet.Core/BicepWrapper.cs: -------------------------------------------------------------------------------- 1 | using Bicep.Cli.Logging; 2 | using Bicep.Core; 3 | using Bicep.Core.Analyzers.Interfaces; 4 | using Bicep.Core.Configuration; 5 | using Bicep.Core.Features; 6 | using Bicep.Core.FileSystem; 7 | using Bicep.Core.Modules; 8 | using Bicep.Core.Registry; 9 | using Bicep.Core.Semantics; 10 | using Bicep.Core.Semantics.Namespaces; 11 | using Bicep.Core.TypeSystem.Providers.Az; 12 | using Bicep.Core.Utils; 13 | using Bicep.Core.Workspaces; 14 | using Bicep.Decompiler; 15 | using BicepNet.Core.Authentication; 16 | using BicepNet.Core.Azure; 17 | using BicepNet.Core.Configuration; 18 | using BicepNet.Core.Models; 19 | using Microsoft.Extensions.DependencyInjection; 20 | using Microsoft.Extensions.Logging; 21 | using Microsoft.VisualStudio.Threading; 22 | using System; 23 | using System.Diagnostics; 24 | using System.IO; 25 | using System.IO.Abstractions; 26 | 27 | namespace BicepNet.Core; 28 | 29 | public partial class BicepWrapper 30 | { 31 | public string BicepVersion { get; } 32 | public string OciCachePath { get; } 33 | public string TemplateSpecsCachePath { get; } 34 | 35 | private readonly ILogger logger; 36 | private readonly DiagnosticLogger diagnosticLogger; 37 | private readonly IServiceProvider services; 38 | 39 | // Services shared between commands 40 | private readonly JoinableTaskFactory joinableTaskFactory; 41 | private readonly INamespaceProvider namespaceProvider; 42 | private readonly IContainerRegistryClientFactory clientFactory; 43 | private readonly ModuleDispatcher moduleDispatcher; 44 | private readonly IArtifactRegistryProvider moduleRegistryProvider; 45 | private readonly IArtifactReferenceFactory artifactReferenceFactory; 46 | private readonly BicepNetTokenCredentialFactory tokenCredentialFactory; 47 | private readonly AzResourceTypeLoader azResourceTypeLoader; 48 | private readonly IEnvironment environment; 49 | private readonly IFileResolver fileResolver; 50 | private readonly IFileSystem fileSystem; 51 | private readonly BicepNetConfigurationManager configurationManager; 52 | private readonly IBicepAnalyzer bicepAnalyzer; 53 | private readonly IFeatureProviderFactory featureProviderFactory; 54 | private readonly BicepCompiler compiler; 55 | private readonly BicepDecompiler decompiler; 56 | private readonly Workspace workspace; 57 | private readonly RootConfiguration configuration; 58 | private readonly AzureResourceProvider azResourceProvider; 59 | 60 | public BicepWrapper(ILogger bicepLogger) 61 | { 62 | services = new ServiceCollection() 63 | .AddBicepNet(bicepLogger) 64 | .BuildServiceProvider(); 65 | 66 | joinableTaskFactory = new JoinableTaskFactory(new JoinableTaskContext()); 67 | logger = services.GetRequiredService(); 68 | diagnosticLogger = services.GetRequiredService(); 69 | azResourceTypeLoader = services.GetRequiredService(); 70 | namespaceProvider = services.GetRequiredService(); 71 | clientFactory = services.GetRequiredService(); 72 | moduleDispatcher = services.GetRequiredService(); 73 | moduleRegistryProvider = services.GetRequiredService(); 74 | artifactReferenceFactory = services.GetRequiredService(); 75 | tokenCredentialFactory = services.GetRequiredService(); 76 | tokenCredentialFactory.Logger = services.GetRequiredService(); 77 | fileResolver = services.GetRequiredService(); 78 | fileSystem = services.GetRequiredService(); 79 | configurationManager = services.GetRequiredService(); 80 | bicepAnalyzer = services.GetRequiredService(); 81 | featureProviderFactory = services.GetRequiredService(); 82 | compiler = services.GetRequiredService(); 83 | environment = services.GetRequiredService(); 84 | 85 | decompiler = services.GetRequiredService(); 86 | 87 | workspace = services.GetRequiredService(); 88 | configuration = configurationManager.GetConfiguration(new Uri("inmemory://main.bicep")); 89 | azResourceProvider = services.GetRequiredService(); 90 | 91 | BicepVersion = FileVersionInfo.GetVersionInfo(typeof(Workspace).Assembly.Location).FileVersion ?? "dev"; 92 | OciCachePath = Path.Combine(services.GetRequiredService().GetFeatureProvider(new Uri("inmemory:///main.bicp")).CacheRootDirectory, ArtifactReferenceSchemes.Oci); 93 | TemplateSpecsCachePath = Path.Combine(services.GetRequiredService().GetFeatureProvider(new Uri("inmemory:///main.bicp")).CacheRootDirectory, ArtifactReferenceSchemes.TemplateSpecs); 94 | } 95 | 96 | public void ClearAuthentication() => tokenCredentialFactory.Clear(); 97 | public void SetAuthentication(string? token = null, string? tenantId = null) => 98 | tokenCredentialFactory.SetToken(configuration.Cloud.ActiveDirectoryAuthorityUri, token, tenantId); 99 | 100 | public BicepAccessToken? GetAccessToken() 101 | { 102 | // Gets the token using the same request context as when connecting 103 | var token = tokenCredentialFactory.Credential?.GetToken(tokenCredentialFactory.TokenRequestContext, System.Threading.CancellationToken.None); 104 | 105 | if (!token.HasValue) 106 | { 107 | logger.LogWarning("No access token currently stored!"); 108 | return null; 109 | } 110 | 111 | var tokenValue = token.Value; 112 | return new BicepAccessToken(tokenValue.Token, tokenValue.ExpiresOn); 113 | } 114 | 115 | public BicepConfigInfo GetBicepConfigInfo(BicepConfigScope scope, string path) => 116 | configurationManager.GetConfigurationInfo(scope, PathHelper.FilePathToFileUrl(path ?? "")); 117 | 118 | private DiagnosticSummary LogDiagnostics(Compilation compilation) 119 | { 120 | if (compilation is null) 121 | { 122 | throw new InvalidOperationException("Compilation is null. A compilation must exist before logging the diagnostics."); 123 | } 124 | 125 | return diagnosticLogger.LogDiagnostics( 126 | new DiagnosticOptions(Bicep.Cli.Arguments.DiagnosticsFormat.Default, false), 127 | compilation 128 | ); 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /BicepNet.Core/Azure/AzureHelpers.cs: -------------------------------------------------------------------------------- 1 | using Azure.Deployments.Core.Definitions.Identifiers; 2 | using Bicep.Core.Parsing; 3 | using Bicep.Core.Resources; 4 | using Bicep.Core.Syntax; 5 | using Bicep.LanguageServer.Providers; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Collections.Immutable; 9 | using System.Linq; 10 | using System.Text.Json; 11 | using System.Text.RegularExpressions; 12 | 13 | namespace BicepNet.Core.Azure; 14 | 15 | public static partial class AzureHelpers 16 | { 17 | 18 | [GeneratedRegex(@"^/subscriptions/(?[^/]+)/resourceGroups/(?[^/]+)$", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant)] 19 | private static partial Regex ResourceGroupId(); 20 | 21 | [GeneratedRegex(@"^/subscriptions/(?[^/]+)$", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture | RegexOptions.CultureInvariant)] 22 | private static partial Regex SubscriptionId(); 23 | 24 | public static IAzResourceProvider.AzResourceIdentifier ValidateResourceId(string id) 25 | { 26 | if (TryParseResourceId(id) is not { } resourceId) 27 | { 28 | var message = $"Failed to parse supplied resourceId \"{id}\"."; 29 | throw new InvalidOperationException(message); 30 | } 31 | return resourceId; 32 | } 33 | 34 | public static string GetResourceFriendlyName(string id) 35 | { 36 | var resourceId = ValidateResourceId(id); 37 | resourceId.Deconstruct( 38 | out _, 39 | out string fullyQualifiedType, 40 | out string fullyQualifiedName, 41 | out _, 42 | out _ 43 | ); 44 | return string.Format("{0}_{1}", fullyQualifiedType.Replace(@"/", "_"), fullyQualifiedName.Replace(@"/", "")).ToLowerInvariant(); 45 | } 46 | 47 | // Private method originally copied from InsertResourceHandler.cs 48 | internal static IAzResourceProvider.AzResourceIdentifier? TryParseResourceId(string? resourceIdString) 49 | { 50 | if (resourceIdString is null) 51 | { 52 | return null; 53 | } 54 | 55 | if (ResourceId.TryParse(resourceIdString, out var resourceId)) 56 | { 57 | return new( 58 | resourceId.FullyQualifiedId, 59 | resourceId.FormatFullyQualifiedType(), 60 | resourceId.FormatName(), 61 | resourceId.NameHierarchy.Last(), 62 | string.Empty); 63 | } 64 | 65 | var rgRegexMatch = ResourceGroupId().Match(resourceIdString); 66 | if (rgRegexMatch.Success) 67 | { 68 | return new( 69 | resourceIdString, 70 | "Microsoft.Resources/resourceGroups", 71 | rgRegexMatch.Groups["rgName"].Value, 72 | rgRegexMatch.Groups["rgName"].Value, 73 | rgRegexMatch.Groups["subId"].Value); 74 | } 75 | 76 | var subRegexMatch = SubscriptionId().Match(resourceIdString); 77 | if (subRegexMatch.Success) 78 | { 79 | IAzResourceProvider.AzResourceIdentifier resource = new( 80 | resourceIdString, 81 | "Microsoft.Management/managementGroups/subscriptions", 82 | subRegexMatch.Groups["subId"].Value, 83 | subRegexMatch.Groups["subId"].Value, 84 | subRegexMatch.Groups["subId"].Value); 85 | return resource; 86 | } 87 | 88 | return null; 89 | } 90 | 91 | // Private method originally copied from InsertResourceHandler.cs 92 | internal static ResourceDeclarationSyntax CreateResourceSyntax(JsonElement resource, IAzResourceProvider.AzResourceIdentifier resourceId, ResourceTypeReference typeReference) 93 | { 94 | var properties = new List(); 95 | foreach (var property in resource.EnumerateObject()) 96 | { 97 | switch (property.Name.ToLowerInvariant()) 98 | { 99 | case "id": 100 | case "type": 101 | case "apiVersion": 102 | // Don't add these to the resource properties - they're part of the resource declaration. 103 | break; 104 | case "name": 105 | // Use the fully-qualified name instead of the name returned by the RP. 106 | properties.Add(SyntaxFactory.CreateObjectProperty( 107 | "name", 108 | SyntaxFactory.CreateStringLiteral(resourceId.FullyQualifiedName))); 109 | break; 110 | default: 111 | properties.Add(SyntaxFactory.CreateObjectProperty( 112 | property.Name, 113 | ConvertJsonElement(property.Value))); 114 | break; 115 | } 116 | } 117 | 118 | var description = SyntaxFactory.CreateDecorator( 119 | "description", 120 | SyntaxFactory.CreateStringLiteral($"Generated from {resourceId.FullyQualifiedId}")); 121 | 122 | return new ResourceDeclarationSyntax( 123 | [description, SyntaxFactory.NewlineToken,], 124 | SyntaxFactory.CreateIdentifierToken("resource"), 125 | SyntaxFactory.CreateIdentifier(NotLetters().Replace(resourceId.UnqualifiedName, "")), 126 | SyntaxFactory.CreateStringLiteral(typeReference.FormatName()), 127 | null, 128 | SyntaxFactory.CreateToken(TokenType.Assignment), 129 | [], 130 | SyntaxFactory.CreateObject(properties)); 131 | } 132 | 133 | // Private method originally copied from InsertResourceHandler.cs 134 | private static SyntaxBase ConvertJsonElement(JsonElement element) 135 | { 136 | switch (element.ValueKind) 137 | { 138 | case JsonValueKind.Object: 139 | var properties = new List(); 140 | foreach (var property in element.EnumerateObject()) 141 | { 142 | properties.Add(SyntaxFactory.CreateObjectProperty(property.Name, ConvertJsonElement(property.Value))); 143 | } 144 | return SyntaxFactory.CreateObject(properties); 145 | case JsonValueKind.Array: 146 | var items = new List(); 147 | foreach (var value in element.EnumerateArray()) 148 | { 149 | items.Add(ConvertJsonElement(value)); 150 | } 151 | return SyntaxFactory.CreateArray(items); 152 | case JsonValueKind.String: 153 | return SyntaxFactory.CreateStringLiteral(element.GetString()!); 154 | case JsonValueKind.Number: 155 | if (element.TryGetInt64(out long intValue)) 156 | { 157 | return SyntaxFactory.CreatePositiveOrNegativeInteger(intValue); 158 | } 159 | 160 | return SyntaxFactory.CreateFunctionCall( 161 | "json", 162 | SyntaxFactory.CreateStringLiteral(element.ToString())); 163 | case JsonValueKind.True: 164 | return SyntaxFactory.CreateToken(TokenType.TrueKeyword); 165 | case JsonValueKind.False: 166 | return SyntaxFactory.CreateToken(TokenType.FalseKeyword); 167 | case JsonValueKind.Null: 168 | return SyntaxFactory.CreateToken(TokenType.NullKeyword); 169 | default: 170 | throw new InvalidOperationException($"Failed to deserialize JSON"); 171 | } 172 | } 173 | 174 | [GeneratedRegex("[^a-zA-Z]")] 175 | private static partial Regex NotLetters(); 176 | } 177 | -------------------------------------------------------------------------------- /BicepNet.Core/Configuration/BicepNetConfigurationManager.Clone.cs: -------------------------------------------------------------------------------- 1 | using Bicep.Core; 2 | using Bicep.Core.Configuration; 3 | using Bicep.Core.Diagnostics; 4 | using Bicep.Core.Extensions; 5 | using Bicep.Core.FileSystem; 6 | using Bicep.Core.Json; 7 | using System; 8 | using System.Collections.Concurrent; 9 | using System.Collections.Generic; 10 | using System.IO; 11 | using System.IO.Abstractions; 12 | using System.Security; 13 | using System.Text.Json; 14 | 15 | namespace BicepNet.Core.Configuration; 16 | 17 | // This is a full copy of Bicep.Core.Configuration.ConfigurationManager where only GetDefaultConfiguration() is removed 18 | // We have implemented our own GetDefaultConfiguration() instead 19 | public partial class BicepNetConfigurationManager(IFileSystem fileSystem) : IConfigurationManager 20 | { 21 | private readonly ConcurrentDictionary configFileUriToLoadedConfigCache = new(); 22 | private readonly ConcurrentDictionary templateUriToConfigUriCache = new(); 23 | private readonly IFileSystem fileSystem = fileSystem; 24 | 25 | public RootConfiguration GetConfiguration(Uri sourceFileUri) 26 | { 27 | var (config, diagnosticBuilders) = GetConfigurationFromCache(sourceFileUri); 28 | return WithLoadDiagnostics(config, diagnosticBuilders); 29 | } 30 | 31 | public void PurgeCache() 32 | { 33 | PurgeLookupCache(); 34 | configFileUriToLoadedConfigCache.Clear(); 35 | } 36 | 37 | public void PurgeLookupCache() => templateUriToConfigUriCache.Clear(); 38 | 39 | public (RootConfiguration prevConfiguration, RootConfiguration newConfiguration)? RefreshConfigCacheEntry(Uri configUri) 40 | { 41 | (RootConfiguration, RootConfiguration)? returnVal = null; 42 | configFileUriToLoadedConfigCache.AddOrUpdate(configUri, LoadConfiguration, (uri, prev) => 43 | { 44 | var reloaded = LoadConfiguration(uri); 45 | if (prev.config is {} prevConfig && reloaded.Item1 is {} newConfig) 46 | { 47 | returnVal = (prevConfig, newConfig); 48 | } 49 | return reloaded; 50 | }); 51 | 52 | return returnVal; 53 | } 54 | 55 | public void RemoveConfigCacheEntry(Uri configUri) 56 | { 57 | if (configFileUriToLoadedConfigCache.TryRemove(configUri, out _)) 58 | { 59 | // If a config file has been removed from a workspace, the lookup cache is no longer valid. 60 | PurgeLookupCache(); 61 | } 62 | } 63 | 64 | private (RootConfiguration, List) GetConfigurationFromCache(Uri sourceFileUri) 65 | { 66 | List diagnostics = []; 67 | 68 | var (configFileUri, lookupDiagnostic) = templateUriToConfigUriCache.GetOrAdd(sourceFileUri, LookupConfiguration); 69 | if (lookupDiagnostic is not null) 70 | { 71 | diagnostics.Add(lookupDiagnostic); 72 | } 73 | 74 | if (configFileUri is not null) 75 | { 76 | var (config, loadError) = configFileUriToLoadedConfigCache.GetOrAdd(configFileUri, LoadConfiguration); 77 | if (loadError is not null) 78 | { 79 | diagnostics.Add(loadError); 80 | } 81 | 82 | if (config is not null) 83 | { 84 | return (config, diagnostics); 85 | } 86 | } 87 | 88 | return (GetDefaultConfiguration(), diagnostics); 89 | } 90 | 91 | private static RootConfiguration WithLoadDiagnostics(RootConfiguration configuration, List diagnostics) 92 | { 93 | if (diagnostics.Count > 0) 94 | { 95 | return new( 96 | configuration.Cloud, 97 | configuration.ModuleAliases, 98 | configuration.ProviderAliases, 99 | configuration.ProvidersConfig, 100 | configuration.ImplicitProvidersConfig, 101 | configuration.Analyzers, 102 | configuration.CacheRootDirectory, 103 | configuration.ExperimentalFeaturesEnabled, 104 | configuration.Formatting, 105 | configuration.ConfigFileUri, 106 | diagnostics); 107 | } 108 | 109 | return configuration; 110 | } 111 | 112 | //private RootConfiguration GetDefaultConfiguration() => IConfigurationManager.GetBuiltInConfiguration(); 113 | 114 | private (RootConfiguration?, DiagnosticBuilder.DiagnosticBuilderDelegate?) LoadConfiguration(Uri configurationUri) 115 | { 116 | try 117 | { 118 | using var stream = fileSystem.FileStream.New(configurationUri.LocalPath, FileMode.Open, FileAccess.Read); 119 | var element = BuiltInConfigurationElement.Merge(JsonElementFactory.CreateElementFromStream(stream)); 120 | 121 | return (RootConfiguration.Bind(element, configurationUri), null); 122 | } 123 | catch (ConfigurationException exception) 124 | { 125 | return (null, x => x.InvalidBicepConfigFile(configurationUri.LocalPath, exception.Message)); 126 | } 127 | catch (JsonException exception) 128 | { 129 | return (null, x => x.UnparsableBicepConfigFile(configurationUri.LocalPath, exception.Message)); 130 | } 131 | catch (Exception exception) 132 | { 133 | return (null, x => x.UnloadableBicepConfigFile(configurationUri.LocalPath, exception.Message)); 134 | } 135 | } 136 | 137 | private ConfigLookupResult LookupConfiguration(Uri sourceFileUri) 138 | { 139 | DiagnosticBuilder.DiagnosticBuilderDelegate? lookupDiagnostic = null; 140 | if (sourceFileUri.Scheme == Uri.UriSchemeFile) 141 | { 142 | string? currentDirectory = fileSystem.Path.GetDirectoryName(sourceFileUri.LocalPath); 143 | while (!string.IsNullOrEmpty(currentDirectory)) 144 | { 145 | var configurationPath = this.fileSystem.Path.Combine(currentDirectory, LanguageConstants.BicepConfigurationFileName); 146 | 147 | if (this.fileSystem.File.Exists(configurationPath)) 148 | { 149 | return new(PathHelper.FilePathToFileUrl(configurationPath), lookupDiagnostic); 150 | } 151 | 152 | try 153 | { 154 | // Catching Directory.GetParent alone because it is the only one that throws IO related exceptions. 155 | // Path.Combine only throws ArgumentNullException which indicates a bug in our code. 156 | // File.Exists will not throw exceptions regardless the existence of path or if the user has permissions to read the file. 157 | currentDirectory = this.fileSystem.Directory.GetParent(currentDirectory)?.FullName; 158 | } 159 | catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or SecurityException) 160 | { 161 | // The exception could happen in senarios where users may not have read permission on the parent folder. 162 | // We should not throw ConfigurationException in such cases since it will block compilation. 163 | lookupDiagnostic = x => x.PotentialConfigDirectoryCouldNotBeScanned(currentDirectory, exception.Message); 164 | break; 165 | } 166 | } 167 | } 168 | 169 | return new(null, lookupDiagnostic); 170 | } 171 | 172 | private record ConfigLookupResult(Uri? ConfigFileUri = null, DiagnosticBuilder.DiagnosticBuilderDelegate? LookupDiagnostic = null); 173 | } 174 | -------------------------------------------------------------------------------- /BicepNet.Core/BicepWrapper.FindModule.cs: -------------------------------------------------------------------------------- 1 | using Azure; 2 | using Azure.Containers.ContainerRegistry; 3 | using Bicep.Core.FileSystem; 4 | using Bicep.Core.Registry; 5 | using Bicep.Core.Tracing; 6 | using Bicep.Core.Workspaces; 7 | using BicepNet.Core.Models; 8 | using Microsoft.Extensions.Logging; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.IO; 12 | using System.Linq; 13 | 14 | namespace BicepNet.Core; 15 | 16 | public partial class BicepWrapper 17 | { 18 | /// 19 | /// Find modules in registries by using a specific endpoints or by seraching a bicep file. 20 | /// 21 | public IList FindModules(string inputString, bool isRegistryEndpoint) 22 | { 23 | List endpoints = []; 24 | 25 | // If a registry is specified, only add that 26 | if (isRegistryEndpoint) 27 | { 28 | endpoints.Add(inputString); 29 | } 30 | else // Otherwise search a file for valid references 31 | { 32 | logger?.LogInformation("Searching file {inputString} for endpoints", inputString); 33 | var inputUri = PathHelper.FilePathToFileUrl(inputString); 34 | 35 | var sourceFileGrouping = SourceFileGroupingBuilder.Build( 36 | fileResolver, 37 | moduleDispatcher, 38 | configurationManager, 39 | workspace, 40 | inputUri, 41 | featureProviderFactory, 42 | false); 43 | var moduleReferences = ArtifactHelper.GetValidArtifactReferences(sourceFileGrouping.GetArtifactsToRestore()); 44 | 45 | // FullyQualifiedReferences are already unwrapped from potential local aliases 46 | var fullReferences = moduleReferences.Select(m => m.FullyQualifiedReference); 47 | // Create objects with all module references grouped by endpoint 48 | // Format endpoint from "br:example.azurecr.io/repository/template:tag" to "example.azurecr.io" 49 | endpoints.AddRange(fullReferences.Select(r => r[3..].Split('/').First()).Distinct()); 50 | } 51 | 52 | return FindModulesByEndpoints(endpoints); 53 | } 54 | 55 | /// 56 | /// Find modules in registries by using endpoints restored to cache. 57 | /// 58 | public IList FindModules() 59 | { 60 | List endpoints = []; 61 | 62 | logger?.LogInformation("Searching cache {OciCachePath} for endpoints", OciCachePath); 63 | var directories = Directory.GetDirectories(OciCachePath); 64 | foreach (var directoryPath in directories) 65 | { 66 | var directoryName = Path.GetFileName(directoryPath); 67 | if(directoryName != "mcr.microsoft.com") { 68 | logger?.LogInformation("Found endpoint {directoryName}", directoryName); 69 | endpoints.Add(directoryName); 70 | } 71 | } 72 | 73 | return FindModulesByEndpoints(endpoints); 74 | } 75 | 76 | private List FindModulesByEndpoints(IList endpoints) 77 | { 78 | if (endpoints.Count > 0) 79 | { 80 | logger?.LogInformation("Found endpoints:\n{joinedEndpoints}", string.Join("\n", endpoints)); 81 | } 82 | else 83 | { 84 | logger?.LogInformation("Found no endpoints in file"); 85 | } 86 | 87 | // Create credential and options 88 | var cred = tokenCredentialFactory.CreateChain(configuration.Cloud.CredentialPrecedence, null, configuration.Cloud.ActiveDirectoryAuthorityUri); 89 | var options = new ContainerRegistryClientOptions(); 90 | options.Diagnostics.ApplySharedContainerRegistrySettings(); 91 | options.Audience = new ContainerRegistryAudience(configuration.Cloud.ResourceManagerAudience); 92 | 93 | var repos = new List(); 94 | foreach (var endpoint in endpoints.Distinct()) 95 | { 96 | try 97 | { 98 | logger?.LogInformation("Searching endpoint {endpoint}", endpoint); 99 | var client = new ContainerRegistryClient(new Uri($"https://{endpoint}"), cred, options); 100 | var repositoryNames = client.GetRepositoryNames(); 101 | 102 | foreach (var repositoryName in repositoryNames) 103 | { 104 | logger?.LogInformation("Searching module {repositoryName}", repositoryName); 105 | 106 | // Create model repository to output 107 | BicepRepository bicepRepository = new(endpoint, repositoryName); 108 | 109 | var repository = client.GetRepository(repositoryName); 110 | var repositoryManifests = repository.GetAllManifestProperties(); 111 | 112 | var manifestCount = repositoryManifests.Count(); 113 | logger?.LogInformation("{manifestCount} manifest(s) found.", manifestCount); 114 | 115 | foreach (var moduleManifest in repositoryManifests) 116 | { 117 | var artifact = repository.GetArtifact(moduleManifest.Digest); 118 | var tags = artifact.GetTagPropertiesCollection(); 119 | 120 | List tagList = []; 121 | // All artifacts don't have tags, but the tags variable will not be null because of the pageable 122 | // This means we can't compare null 123 | try 124 | { 125 | foreach (var tag in tags) 126 | { 127 | logger?.LogInformation("Found tag \"{tag.Name}\"", tag.Name); 128 | tagList.Add(new BicepRepositoryModuleTag( 129 | name: tag.Name, 130 | digest: tag.Digest, 131 | updatedOn: tag.LastUpdatedOn, 132 | createdOn: tag.CreatedOn, 133 | target: $"br:{endpoint}/{repositoryName}:{tag.Name}" 134 | )); 135 | } 136 | } // When there are no tags, we cannot enumerate null - disregard this error and continue 137 | catch (InvalidOperationException ex) when (ex.TargetSite?.Name == "EnumerateArray" || ex.TargetSite?.Name == "ThrowJsonElementWrongTypeException") { 138 | logger?.LogInformation("No tags found for manifest with digest {moduleManifest.Digest}", moduleManifest.Digest); 139 | } 140 | 141 | var bicepModule = new BicepRepositoryModule( 142 | digest: moduleManifest.Digest, 143 | repository: repositoryName, 144 | tags: tagList, 145 | createdOn: moduleManifest.CreatedOn, 146 | updatedOn: moduleManifest.LastUpdatedOn 147 | ); 148 | bicepRepository.ModuleVersions.Add(bicepModule); 149 | } 150 | 151 | bicepRepository.ModuleVersions = [.. bicepRepository.ModuleVersions.OrderByDescending(t => t.UpdatedOn)]; 152 | 153 | repos.Add(bicepRepository); 154 | } 155 | } 156 | catch (RequestFailedException ex) 157 | { 158 | switch (ex.Status) 159 | { 160 | case 401: 161 | logger?.LogWarning("The credentials provided are not authorized to the following registry: {endpoint}", endpoint); 162 | break; 163 | default: 164 | logger?.LogError(ex, "Could not get modules from endpoint {endpoint}!", endpoint); 165 | break; 166 | } 167 | } 168 | catch (AggregateException ex) 169 | { 170 | if (ex.InnerException != null) 171 | { 172 | logger?.LogWarning("{message}", ex.InnerException.Message); 173 | } 174 | else 175 | { 176 | logger?.LogError(ex, "Could not get modules from endpoint {endpoint}!", endpoint); 177 | } 178 | } 179 | catch (Exception ex) 180 | { 181 | logger?.LogError(ex, "Could not get modules from endpoint {endpoint}!", endpoint); 182 | } 183 | } 184 | 185 | return repos; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /BicepNet.Core/Azure/RoleHelper.cs: -------------------------------------------------------------------------------- 1 | using Azure; 2 | using Azure.Core; 3 | using Azure.ResourceManager; 4 | using Azure.ResourceManager.Authorization; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text.Json; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace BicepNet.Core.Azure; 13 | 14 | internal static class RoleHelper 15 | { 16 | public static async Task> ListRoleDefinitionsAsync(ResourceIdentifier scopeResourceId, ArmClient armClient, RoleDefinitionType roleDefinitionType, CancellationToken cancellationToken) 17 | { 18 | return (string)scopeResourceId.ResourceType switch 19 | { 20 | "Microsoft.Management/managementGroups" => await ListManagementGroupRoleDefinitionAsync(scopeResourceId, armClient, roleDefinitionType, cancellationToken), 21 | "Microsoft.Resources/subscriptions" => await ListSubscriptionRoleDefinitionAsync(scopeResourceId, armClient, roleDefinitionType, cancellationToken), 22 | _ => throw new InvalidOperationException($"Failed to list RoleDefinitions on scope '{scopeResourceId}' with type '{scopeResourceId.ResourceType}") 23 | }; 24 | } 25 | 26 | public static async Task> ListRoleAssignmentsAsync(ResourceIdentifier scopeResourceId, ArmClient armClient, CancellationToken cancellationToken) 27 | { 28 | return (string)scopeResourceId.ResourceType switch 29 | { 30 | "Microsoft.Management/managementGroups" => await ListManagementGroupRoleAssignmentsAsync(scopeResourceId, armClient, cancellationToken), 31 | "Microsoft.Resources/subscriptions" => await ListSubscriptionRoleAssignmentsAsync(scopeResourceId, armClient, cancellationToken), 32 | "Microsoft.Resources/resourceGroups" => await ListResourceGroupRoleAssignmentsAsync(scopeResourceId, armClient, cancellationToken), 33 | _ => await ListResourceRoleAssignmentsAsync(scopeResourceId, armClient, cancellationToken) // Default to "resource" scope 34 | }; 35 | } 36 | 37 | private static async Task> ListManagementGroupRoleDefinitionAsync(ResourceIdentifier resourceIdentifier, ArmClient armClient, RoleDefinitionType roleDefinitionType, CancellationToken cancellationToken) 38 | => await GetRoleDefinitionResourcesAsync(armClient.GetManagementGroupResource(resourceIdentifier).GetAuthorizationRoleDefinitions(), roleDefinitionType, cancellationToken); 39 | 40 | private static async Task> ListSubscriptionRoleDefinitionAsync(ResourceIdentifier resourceIdentifier, ArmClient armClient, RoleDefinitionType roleDefinitionType, CancellationToken cancellationToken) 41 | => await GetRoleDefinitionResourcesAsync(armClient.GetSubscriptionResource(resourceIdentifier).GetAuthorizationRoleDefinitions(), roleDefinitionType, cancellationToken); 42 | 43 | private static async Task> ListManagementGroupRoleAssignmentsAsync(ResourceIdentifier resourceIdentifier, ArmClient armClient, CancellationToken cancellationToken) 44 | => await GetRoleAssignmentResourcesAsync(armClient.GetManagementGroupResource(resourceIdentifier).GetRoleAssignments(), cancellationToken); 45 | 46 | private static async Task> ListSubscriptionRoleAssignmentsAsync(ResourceIdentifier resourceIdentifier, ArmClient armClient, CancellationToken cancellationToken) 47 | => await GetRoleAssignmentResourcesAsync(armClient.GetSubscriptionResource(resourceIdentifier).GetRoleAssignments(), cancellationToken); 48 | 49 | private static async Task> ListResourceGroupRoleAssignmentsAsync(ResourceIdentifier resourceIdentifier, ArmClient armClient, CancellationToken cancellationToken) 50 | => await GetRoleAssignmentResourcesAsync(armClient.GetResourceGroupResource(resourceIdentifier).GetRoleAssignments(), cancellationToken); 51 | 52 | private static async Task> ListResourceRoleAssignmentsAsync(ResourceIdentifier resourceIdentifier, ArmClient armClient, CancellationToken cancellationToken) 53 | => await GetRoleAssignmentResourcesAsync(armClient.GetGenericResource(resourceIdentifier).GetRoleAssignments(), cancellationToken); 54 | 55 | private static async Task> GetRoleAssignmentResourcesAsync(RoleAssignmentCollection collection, CancellationToken cancellationToken) 56 | { 57 | var result = new Dictionary(); 58 | var list = collection.GetAllAsync(filter: "atScope()", cancellationToken: cancellationToken); 59 | 60 | JsonElement element; 61 | 62 | var taskList = new Dictionary>>(); 63 | await foreach (var item in list) 64 | { 65 | taskList.Add(item.Id.ToString(), item.GetAsync(cancellationToken: cancellationToken)); 66 | } 67 | var responseList = await GetResponseDictionaryAsync(taskList); 68 | foreach (var id in responseList.Keys) 69 | { 70 | var policyItemResponse = responseList[id]; 71 | var resourceId = AzureHelpers.ValidateResourceId(id); 72 | if (policyItemResponse is null || 73 | policyItemResponse.GetRawResponse().ContentStream is not { } contentStream) 74 | { 75 | throw new InvalidOperationException($"Failed to fetch resource from Id '{resourceId.FullyQualifiedId}'"); 76 | } 77 | contentStream.Position = 0; 78 | element = await JsonSerializer.DeserializeAsync(contentStream, cancellationToken: cancellationToken); 79 | result.Add(id, element); 80 | } 81 | 82 | return result; 83 | } 84 | 85 | private static async Task> GetRoleDefinitionResourcesAsync(AuthorizationRoleDefinitionCollection collection, RoleDefinitionType roleDefinitionType, CancellationToken cancellationToken) 86 | { 87 | var result = new Dictionary(); 88 | var list = collection.GetAllAsync(filter: $"type eq '{roleDefinitionType}'", cancellationToken: cancellationToken); 89 | 90 | JsonElement element; 91 | 92 | var taskList = new Dictionary>>(); 93 | await foreach (var item in list) 94 | { 95 | taskList.Add(item.Id.ToString(), item.GetAsync(cancellationToken: cancellationToken)); 96 | } 97 | var responseList = await GetResponseDictionaryAsync(taskList); 98 | foreach (var id in responseList.Keys) 99 | { 100 | var policyItemResponse = responseList[id]; 101 | var resourceId = AzureHelpers.ValidateResourceId(id); 102 | if (policyItemResponse is null || 103 | policyItemResponse.GetRawResponse().ContentStream is not { } contentStream) 104 | { 105 | throw new InvalidOperationException($"Failed to fetch resource from Id '{resourceId.FullyQualifiedId}'"); 106 | } 107 | contentStream.Position = 0; 108 | element = await JsonSerializer.DeserializeAsync(contentStream, cancellationToken: cancellationToken); 109 | result.Add(id, element); 110 | } 111 | 112 | return result; 113 | } 114 | 115 | public static async Task GetRoleDefinitionAsync(ResourceIdentifier resourceIdentifier, ArmClient armClient, CancellationToken cancellationToken) 116 | { 117 | try 118 | { 119 | var resource = armClient.GetAuthorizationRoleDefinitionResource(resourceIdentifier); 120 | var roleDefResponse = await resource.GetAsync(cancellationToken); 121 | 122 | if (roleDefResponse is null || roleDefResponse.GetRawResponse().ContentStream is not { } contentStream) 123 | { 124 | throw new InvalidOperationException($"Failed to fetch resource from Id '{resourceIdentifier}'"); 125 | } 126 | contentStream.Position = 0; 127 | return await JsonSerializer.DeserializeAsync(contentStream, cancellationToken: cancellationToken); 128 | } 129 | catch (Exception ex) 130 | { 131 | throw new InvalidOperationException($"Failed to list RoleDefinitions on scope '{resourceIdentifier}' with type '{resourceIdentifier.ResourceType}", ex); 132 | } 133 | } 134 | 135 | public static async Task GetRoleAssignmentAsync(ResourceIdentifier resourceIdentifier, ArmClient armClient, CancellationToken cancellationToken) 136 | { 137 | var pa = armClient.GetRoleAssignmentResource(resourceIdentifier); 138 | var paResponse = await pa.GetAsync(cancellationToken: cancellationToken); 139 | if (paResponse is null || paResponse.GetRawResponse().ContentStream is not { } paContentStream) 140 | { 141 | throw new InvalidOperationException($"Failed to fetch resource from Id '{resourceIdentifier}'"); 142 | } 143 | paContentStream.Position = 0; 144 | return await JsonSerializer.DeserializeAsync(paContentStream, cancellationToken: cancellationToken); 145 | } 146 | 147 | private static async Task>> GetResponseDictionaryAsync(Dictionary>> taskList) 148 | { 149 | var resultListPairs = await Task.WhenAll(taskList.Select(async result => 150 | new { result.Key, Value = await result.Value })); 151 | return resultListPairs.ToDictionary(result => result.Key, result => result.Value); 152 | } 153 | } -------------------------------------------------------------------------------- /BicepNet.Core/Azure/AzureResourceProvider.cs: -------------------------------------------------------------------------------- 1 | using Azure.Core; 2 | using Azure.ResourceManager; 3 | using Bicep.Core.Configuration; 4 | using Bicep.Core.Diagnostics; 5 | using Bicep.Core.Parsing; 6 | using Bicep.Core.PrettyPrint; 7 | using Bicep.Core.PrettyPrint.Options; 8 | using Bicep.Core.Registry.Auth; 9 | using Bicep.Core.Resources; 10 | using Bicep.Core.Syntax; 11 | using Bicep.LanguageServer.Providers; 12 | using System; 13 | using System.Collections.Generic; 14 | using System.Runtime.CompilerServices; 15 | using System.Text.Json; 16 | using System.Threading; 17 | using System.Threading.Tasks; 18 | 19 | namespace BicepNet.Core.Azure; 20 | public class AzureResourceProvider(ITokenCredentialFactory credentialFactory) : IAzResourceProvider 21 | { 22 | private readonly ITokenCredentialFactory credentialFactory = credentialFactory; 23 | private AccessToken accessToken; 24 | 25 | private async Task UpdateAccessTokenAsync(RootConfiguration configuration, CancellationToken cancellationToken) 26 | { 27 | var credential = credentialFactory.CreateChain(configuration.Cloud.CredentialPrecedence, null, configuration.Cloud.ActiveDirectoryAuthorityUri); 28 | var tokenRequestContext = new TokenRequestContext([configuration.Cloud.AuthenticationScope], configuration.Cloud.ResourceManagerEndpointUri.ToString()); 29 | accessToken = await credential.GetTokenAsync(tokenRequestContext, cancellationToken); 30 | } 31 | 32 | private ArmClient CreateArmClient(RootConfiguration configuration, string subscriptionId, (string resourceType, string? apiVersion) resourceTypeApiVersionMapping) 33 | { 34 | var options = new ArmClientOptions 35 | { 36 | Environment = new ArmEnvironment(configuration.Cloud.ResourceManagerEndpointUri, configuration.Cloud.AuthenticationScope) 37 | }; 38 | if (resourceTypeApiVersionMapping.apiVersion is not null) 39 | { 40 | options.SetApiVersion(new ResourceType(resourceTypeApiVersionMapping.resourceType), resourceTypeApiVersionMapping.apiVersion); 41 | } 42 | 43 | var credential = credentialFactory.CreateChain(configuration.Cloud.CredentialPrecedence, null, configuration.Cloud.ActiveDirectoryAuthorityUri); 44 | 45 | return new ArmClient(credential, subscriptionId, options); 46 | } 47 | 48 | public async IAsyncEnumerable<(string, JsonElement)> GetChildResourcesAsync(RootConfiguration configuration, IAzResourceProvider.AzResourceIdentifier scopeResourceId, [EnumeratorCancellation] CancellationToken cancellationToken) 49 | { 50 | (string resourceType, string? apiVersion) resourceTypeApiVersionMapping = (scopeResourceId.FullyQualifiedType, null); 51 | 52 | if (string.IsNullOrEmpty(accessToken.Token) || accessToken.ExpiresOn.UtcDateTime < DateTimeOffset.UtcNow.AddMinutes(10)) 53 | { 54 | await UpdateAccessTokenAsync(configuration, cancellationToken); 55 | } 56 | 57 | var armClient = CreateArmClient(configuration, scopeResourceId.subscriptionId, resourceTypeApiVersionMapping); 58 | var scopeResourceIdentifier = new ResourceIdentifier(scopeResourceId.FullyQualifiedId); 59 | 60 | List>> tasks = []; 61 | 62 | switch ((string)scopeResourceIdentifier.ResourceType) 63 | { 64 | case "Microsoft.Management/managementGroups": 65 | var resourceIdList = ManagementGroupHelper.GetManagementGroupDescendantsAsync(scopeResourceIdentifier, armClient, cancellationToken); 66 | await foreach (string id in resourceIdList) 67 | { 68 | var resourceId = AzureHelpers.ValidateResourceId(id); 69 | var resource = await GetGenericResource(configuration, resourceId, null, cancellationToken: cancellationToken); 70 | yield return (id, resource); 71 | } 72 | 73 | // Setup tasks of all dictionaries to loop through 74 | tasks = [ 75 | PolicyHelper.ListPolicyDefinitionsAsync(scopeResourceIdentifier, armClient, cancellationToken), 76 | PolicyHelper.ListPolicyInitiativesAsync(scopeResourceIdentifier, armClient, cancellationToken), 77 | PolicyHelper.ListPolicyAssignmentsAsync(scopeResourceIdentifier, armClient, cancellationToken), 78 | RoleHelper.ListRoleAssignmentsAsync(scopeResourceIdentifier, armClient, cancellationToken), 79 | RoleHelper.ListRoleDefinitionsAsync(scopeResourceIdentifier, armClient, RoleDefinitionType.CustomRole, cancellationToken) 80 | ]; 81 | break; 82 | case "Microsoft.Resources/subscriptions": 83 | tasks = [ 84 | PolicyHelper.ListPolicyDefinitionsAsync(scopeResourceIdentifier, armClient, cancellationToken), 85 | PolicyHelper.ListPolicyInitiativesAsync(scopeResourceIdentifier, armClient, cancellationToken), 86 | PolicyHelper.ListPolicyAssignmentsAsync(scopeResourceIdentifier, armClient, cancellationToken), 87 | RoleHelper.ListRoleAssignmentsAsync(scopeResourceIdentifier, armClient, cancellationToken), 88 | RoleHelper.ListRoleDefinitionsAsync(scopeResourceIdentifier, armClient, RoleDefinitionType.CustomRole, cancellationToken) 89 | ]; 90 | break; 91 | case "Microsoft.Resources/resourceGroups": 92 | tasks = [ 93 | PolicyHelper.ListPolicyAssignmentsAsync(scopeResourceIdentifier, armClient, cancellationToken), 94 | RoleHelper.ListRoleAssignmentsAsync(scopeResourceIdentifier, armClient, cancellationToken) 95 | ]; 96 | break; 97 | } 98 | // Return all resources found 99 | foreach (var task in tasks) 100 | { 101 | foreach (var entry in await task) 102 | { 103 | yield return (entry.Key, entry.Value); 104 | } 105 | } 106 | } 107 | 108 | public async Task GetGenericResource(RootConfiguration configuration, IAzResourceProvider.AzResourceIdentifier resourceId, string? apiVersion, CancellationToken cancellationToken) 109 | { 110 | (string resourceType, string? apiVersion) resourceTypeApiVersionMapping = (resourceId.FullyQualifiedType, apiVersion); 111 | 112 | var armClient = CreateArmClient(configuration, resourceId.subscriptionId, resourceTypeApiVersionMapping); 113 | var resourceIdentifier = new ResourceIdentifier(resourceId.FullyQualifiedId); 114 | 115 | switch (resourceIdentifier.ResourceType) 116 | { 117 | case "Microsoft.Management/managementGroups": 118 | return await ManagementGroupHelper.GetManagementGroupAsync(resourceIdentifier, armClient, cancellationToken); 119 | case "Microsoft.Authorization/policyDefinitions": 120 | return await PolicyHelper.GetPolicyDefinitionAsync(resourceIdentifier, armClient, cancellationToken); 121 | case "Microsoft.Resources/subscriptions": 122 | return await SubscriptionHelper.GetSubscriptionAsync(resourceIdentifier, armClient, cancellationToken); 123 | case "Microsoft.Authorization/roleAssignments": 124 | return await RoleHelper.GetRoleAssignmentAsync(resourceIdentifier, armClient, cancellationToken); 125 | case "Microsoft.Authorization/roleDefinitions": 126 | return await RoleHelper.GetRoleDefinitionAsync(resourceIdentifier, armClient, cancellationToken); 127 | case "Microsoft.Management/managementGroups/subscriptions": 128 | if (string.IsNullOrEmpty(accessToken.Token)) 129 | { 130 | await UpdateAccessTokenAsync(configuration, cancellationToken); 131 | } 132 | return await SubscriptionHelper.GetManagementGroupSubscriptionAsync(resourceIdentifier, accessToken, cancellationToken); 133 | default: 134 | var genericResourceResponse = await armClient.GetGenericResource(resourceIdentifier).GetAsync(cancellationToken); 135 | if (genericResourceResponse is null || 136 | genericResourceResponse.GetRawResponse().ContentStream is not { } contentStream) 137 | { 138 | throw new InvalidOperationException($"Failed to fetch resource from Id '{resourceId.FullyQualifiedId}'"); 139 | } 140 | 141 | contentStream.Position = 0; 142 | return await JsonSerializer.DeserializeAsync(contentStream, cancellationToken: cancellationToken); 143 | } 144 | } 145 | 146 | public static string GenerateBicepTemplate(IAzResourceProvider.AzResourceIdentifier resourceId, ResourceTypeReference resourceType, JsonElement resource, bool includeTargetScope = false) 147 | { 148 | var resourceIdentifier = new ResourceIdentifier(resourceId.FullyQualifiedId); 149 | string targetScope = (string?)(resourceIdentifier.Parent?.ResourceType) switch 150 | { 151 | "Microsoft.Resources/resourceGroups" => $"targetScope = 'resourceGroup'{Environment.NewLine}", 152 | "Microsoft.Resources/subscriptions" => $"targetScope = 'subscription'{Environment.NewLine}", 153 | "Microsoft.Management/managementGroups" => $"targetScope = 'managementGroup'{Environment.NewLine}", 154 | _ => $"targetScope = 'tenant'{Environment.NewLine}", 155 | }; 156 | if (resourceIdentifier.ResourceType == "Microsoft.Management/managementGroups" || resourceIdentifier.ResourceType == "Microsoft.Management/managementGroups/subscriptions") 157 | { 158 | targetScope = $"targetScope = 'tenant'{Environment.NewLine}"; 159 | } 160 | 161 | var resourceDeclaration = AzureHelpers.CreateResourceSyntax(resource, resourceId, resourceType); 162 | 163 | var printOptions = new PrettyPrintOptions(NewlineOption.LF, IndentKindOption.Space, 2, false); 164 | var program = new ProgramSyntax( 165 | [resourceDeclaration], 166 | SyntaxFactory.CreateToken(TokenType.EndOfFile)); 167 | var template = PrettyPrinter.PrintProgram(program, printOptions, EmptyDiagnosticLookup.Instance, EmptyDiagnosticLookup.Instance); 168 | 169 | return includeTargetScope ? targetScope + template : template; 170 | } 171 | } -------------------------------------------------------------------------------- /BicepNet.Core/Azure/PolicyHelper.cs: -------------------------------------------------------------------------------- 1 | using Azure; 2 | using Azure.Core; 3 | using Azure.ResourceManager; 4 | using Azure.ResourceManager.Resources; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Text.Json; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace BicepNet.Core.Azure; 13 | 14 | internal static class PolicyHelper 15 | { 16 | public static async Task> ListPolicyDefinitionsAsync(ResourceIdentifier scopeResourceId, ArmClient armClient, CancellationToken cancellationToken) 17 | { 18 | return (string)scopeResourceId.ResourceType switch 19 | { 20 | "Microsoft.Management/managementGroups" => await ListManagementGroupPolicyDefinitionAsync(scopeResourceId, armClient, cancellationToken), 21 | "Microsoft.Resources/subscriptions" => await ListSubscriptionPolicyDefinitionAsync(scopeResourceId, armClient, cancellationToken), 22 | _ => throw new InvalidOperationException($"Failed to list PolicyDefinitions on scope '{scopeResourceId}' with type '{scopeResourceId.ResourceType}"), 23 | }; 24 | } 25 | 26 | public static async Task> ListPolicyInitiativesAsync(ResourceIdentifier scopeResourceId, ArmClient armClient, CancellationToken cancellationToken) 27 | { 28 | return (string)scopeResourceId.ResourceType switch 29 | { 30 | "Microsoft.Management/managementGroups" => await ListManagementGroupPolicyInitiativeAsync(scopeResourceId, armClient, cancellationToken), 31 | "Microsoft.Resources/subscriptions" => await ListSubscriptionPolicyInitiativeAsync(scopeResourceId, armClient, cancellationToken), 32 | _ => throw new InvalidOperationException($"Failed to list PolicyDefinitions on scope '{scopeResourceId}' with type '{scopeResourceId.ResourceType}"), 33 | }; 34 | } 35 | 36 | public static async Task> ListPolicyAssignmentsAsync(ResourceIdentifier scopeResourceId, ArmClient armClient, CancellationToken cancellationToken) 37 | { 38 | return (string)scopeResourceId.ResourceType switch 39 | { 40 | "Microsoft.Management/managementGroups" => await ListManagementGroupPolicyAssignmentAsync(scopeResourceId, armClient, cancellationToken), 41 | "Microsoft.Resources/subscriptions" => await ListSubscriptionPolicyAssignmentAsync(scopeResourceId, armClient, cancellationToken), 42 | "Microsoft.Resources/resourceGroups" => await ListResourceGroupPolicyAssignmentAsync(scopeResourceId, armClient, cancellationToken), 43 | _ => throw new InvalidOperationException($"Failed to list PolicyDefinitions on scope '{scopeResourceId}' with type '{scopeResourceId.ResourceType}"), 44 | }; 45 | } 46 | 47 | private static async Task> ListManagementGroupPolicyDefinitionAsync(ResourceIdentifier resourceIdentifier, ArmClient armClient, CancellationToken cancellationToken) 48 | { 49 | var result = new Dictionary(); 50 | var mg = armClient.GetManagementGroupResource(resourceIdentifier); 51 | 52 | var collection = mg.GetManagementGroupPolicyDefinitions(); 53 | var list = collection.GetAllAsync(filter: "atExactScope()", cancellationToken: cancellationToken); 54 | 55 | JsonElement element; 56 | 57 | var taskList = new Dictionary>>(); 58 | await foreach (var item in list) 59 | { 60 | taskList.Add(item.Id.ToString(), item.GetAsync(cancellationToken: cancellationToken)); 61 | } 62 | 63 | var responseList = await GetResponseDictionaryAsync(taskList); 64 | 65 | foreach (var id in responseList.Keys) 66 | { 67 | var policyItemResponse = responseList[id]; 68 | var resourceId = AzureHelpers.ValidateResourceId(id); 69 | if (policyItemResponse is null || 70 | policyItemResponse.GetRawResponse().ContentStream is not { } contentStream) 71 | { 72 | throw new InvalidOperationException($"Failed to fetch resource from Id '{resourceId.FullyQualifiedId}'"); 73 | } 74 | contentStream.Position = 0; 75 | element = await JsonSerializer.DeserializeAsync(contentStream, cancellationToken: cancellationToken); 76 | result.Add(id, element); 77 | } 78 | return result; 79 | } 80 | 81 | private static async Task> ListManagementGroupPolicyInitiativeAsync(ResourceIdentifier resourceIdentifier, ArmClient armClient, CancellationToken cancellationToken) 82 | { 83 | var result = new Dictionary(); 84 | var mg = armClient.GetManagementGroupResource(resourceIdentifier); 85 | 86 | var collection = mg.GetManagementGroupPolicySetDefinitions(); 87 | var list = collection.GetAllAsync(filter: "atExactScope()", cancellationToken: cancellationToken); 88 | 89 | JsonElement element; 90 | 91 | var taskList = new Dictionary>>(); 92 | await foreach (var item in list) 93 | { 94 | taskList.Add(item.Id.ToString(), item.GetAsync(cancellationToken: cancellationToken)); 95 | } 96 | var responseList = await GetResponseDictionaryAsync(taskList); 97 | foreach (var id in responseList.Keys) 98 | { 99 | var policyItemResponse = responseList[id]; 100 | var resourceId = AzureHelpers.ValidateResourceId(id); 101 | if (policyItemResponse is null || 102 | policyItemResponse.GetRawResponse().ContentStream is not { } contentStream) 103 | { 104 | throw new InvalidOperationException($"Failed to fetch resource from Id '{resourceId.FullyQualifiedId}'"); 105 | } 106 | contentStream.Position = 0; 107 | element = await JsonSerializer.DeserializeAsync(contentStream, cancellationToken: cancellationToken); 108 | result.Add(id, element); 109 | } 110 | return result; 111 | } 112 | 113 | private static async Task> ListSubscriptionPolicyDefinitionAsync(ResourceIdentifier resourceIdentifier, ArmClient armClient, CancellationToken cancellationToken) 114 | { 115 | var result = new Dictionary(); 116 | var sub = armClient.GetSubscriptionResource(resourceIdentifier); 117 | 118 | var collection = sub.GetSubscriptionPolicyDefinitions(); 119 | var list = collection.GetAllAsync(filter: "atExactScope()", cancellationToken: cancellationToken); 120 | 121 | JsonElement element; 122 | 123 | var taskList = new Dictionary>>(); 124 | await foreach (var item in list) 125 | { 126 | taskList.Add(item.Id.ToString(), item.GetAsync(cancellationToken: cancellationToken)); 127 | } 128 | var responseList = await GetResponseDictionaryAsync(taskList); 129 | foreach (var id in responseList.Keys) 130 | { 131 | var policyItemResponse = responseList[id]; 132 | var resourceId = AzureHelpers.ValidateResourceId(id); 133 | if (policyItemResponse is null || 134 | policyItemResponse.GetRawResponse().ContentStream is not { } contentStream) 135 | { 136 | throw new InvalidOperationException($"Failed to fetch resource from Id '{resourceId.FullyQualifiedId}'"); 137 | } 138 | contentStream.Position = 0; 139 | element = await JsonSerializer.DeserializeAsync(contentStream, cancellationToken: cancellationToken); 140 | result.Add(id, element); 141 | } 142 | return result; 143 | } 144 | 145 | private static async Task> ListSubscriptionPolicyInitiativeAsync(ResourceIdentifier resourceIdentifier, ArmClient armClient, CancellationToken cancellationToken) 146 | { 147 | var result = new Dictionary(); 148 | var sub = armClient.GetSubscriptionResource(resourceIdentifier); 149 | 150 | var collection = sub.GetSubscriptionPolicySetDefinitions(); 151 | var list = collection.GetAllAsync(filter: "atExactScope()", cancellationToken: cancellationToken); 152 | 153 | JsonElement element; 154 | 155 | var taskList = new Dictionary>>(); 156 | await foreach (var item in list) 157 | { 158 | taskList.Add(item.Id.ToString(), item.GetAsync(cancellationToken: cancellationToken)); 159 | } 160 | var responseList = await GetResponseDictionaryAsync(taskList); 161 | foreach (var id in responseList.Keys) 162 | { 163 | var policyItemResponse = responseList[id]; 164 | var resourceId = AzureHelpers.ValidateResourceId(id); 165 | if (policyItemResponse is null || 166 | policyItemResponse.GetRawResponse().ContentStream is not { } contentStream) 167 | { 168 | throw new InvalidOperationException($"Failed to fetch resource from Id '{resourceId.FullyQualifiedId}'"); 169 | } 170 | contentStream.Position = 0; 171 | element = await JsonSerializer.DeserializeAsync(contentStream, cancellationToken: cancellationToken); 172 | result.Add(id, element); 173 | } 174 | return result; 175 | } 176 | 177 | private static async Task> ListManagementGroupPolicyAssignmentAsync(ResourceIdentifier resourceIdentifier, ArmClient armClient, CancellationToken cancellationToken) 178 | { 179 | var mg = armClient.GetManagementGroupResource(resourceIdentifier); 180 | var collection = mg.GetPolicyAssignments(); 181 | 182 | return await ListPolicyAssignmentAsync(collection, cancellationToken); 183 | } 184 | 185 | private static async Task> ListSubscriptionPolicyAssignmentAsync(ResourceIdentifier resourceIdentifier, ArmClient armClient, CancellationToken cancellationToken) 186 | { 187 | var sub = armClient.GetSubscriptionResource(resourceIdentifier); 188 | var collection = sub.GetPolicyAssignments(); 189 | 190 | return await ListPolicyAssignmentAsync(collection, cancellationToken); 191 | } 192 | 193 | private static async Task> ListResourceGroupPolicyAssignmentAsync(ResourceIdentifier resourceIdentifier, ArmClient armClient, CancellationToken cancellationToken) 194 | { 195 | var rg = armClient.GetResourceGroupResource(resourceIdentifier); 196 | var collection = rg.GetPolicyAssignments(); 197 | 198 | return await ListPolicyAssignmentAsync(collection, cancellationToken); 199 | } 200 | 201 | private static async Task> ListPolicyAssignmentAsync(PolicyAssignmentCollection collection, CancellationToken cancellationToken) 202 | { 203 | var result = new Dictionary(); 204 | var list = collection.GetAllAsync(filter: "atExactScope()", cancellationToken: cancellationToken); 205 | 206 | JsonElement element; 207 | 208 | var taskList = new Dictionary>>(); 209 | await foreach (var item in list) 210 | { 211 | taskList.Add(item.Id.ToString(), item.GetAsync(cancellationToken: cancellationToken)); 212 | } 213 | var responseList = await GetResponseDictionaryAsync(taskList); 214 | foreach (var id in responseList.Keys) 215 | { 216 | var policyItemResponse = responseList[id]; 217 | var resourceId = AzureHelpers.ValidateResourceId(id); 218 | if (policyItemResponse is null || 219 | policyItemResponse.GetRawResponse().ContentStream is not { } contentStream) 220 | { 221 | throw new InvalidOperationException($"Failed to fetch resource from Id '{resourceId.FullyQualifiedId}'"); 222 | } 223 | contentStream.Position = 0; 224 | element = await JsonSerializer.DeserializeAsync(contentStream, cancellationToken: cancellationToken); 225 | result.Add(id, element); 226 | } 227 | return result; 228 | } 229 | 230 | public static async Task GetPolicyDefinitionAsync(ResourceIdentifier resourceIdentifier, ArmClient armClient, CancellationToken cancellationToken) 231 | { 232 | switch (resourceIdentifier.Parent?.ResourceType) 233 | { 234 | case "Microsoft.Resources/subscriptions": 235 | var subPolicyDef = armClient.GetSubscriptionPolicyDefinitionResource(resourceIdentifier); 236 | var subPolicyDefResponse = await subPolicyDef.GetAsync(cancellationToken); 237 | 238 | if (subPolicyDefResponse is null || subPolicyDefResponse.GetRawResponse().ContentStream is not { } subContentStream) 239 | { 240 | throw new InvalidOperationException($"Failed to fetch resource from Id '{resourceIdentifier}'"); 241 | } 242 | subContentStream.Position = 0; 243 | return await JsonSerializer.DeserializeAsync(subContentStream, cancellationToken: cancellationToken); 244 | case "Microsoft.Management/managementGroups": 245 | var mgPolicyDef = armClient.GetManagementGroupPolicyDefinitionResource(resourceIdentifier); 246 | var mgPolicyDefResponse = await mgPolicyDef.GetAsync(cancellationToken); 247 | 248 | if (mgPolicyDefResponse is null || mgPolicyDefResponse.GetRawResponse().ContentStream is not { } mgContentStream) 249 | { 250 | throw new InvalidOperationException($"Failed to fetch resource from Id '{resourceIdentifier}'"); 251 | } 252 | mgContentStream.Position = 0; 253 | return await JsonSerializer.DeserializeAsync(mgContentStream, cancellationToken: cancellationToken); 254 | case "Microsoft.Resources/tenants": 255 | var tenantPolicyDef = armClient.GetTenantPolicyDefinitionResource(resourceIdentifier); 256 | var tenantPolicyDefResponse = await tenantPolicyDef.GetAsync(cancellationToken); 257 | 258 | if (tenantPolicyDefResponse is null || tenantPolicyDefResponse.GetRawResponse().ContentStream is not { } tenantContentStream) 259 | { 260 | throw new InvalidOperationException($"Failed to fetch resource from Id '{resourceIdentifier}'"); 261 | } 262 | tenantContentStream.Position = 0; 263 | return await JsonSerializer.DeserializeAsync(tenantContentStream, cancellationToken: cancellationToken); 264 | default: 265 | throw new InvalidOperationException($"Failed to fetch resource from Id '{resourceIdentifier}' and parent '{resourceIdentifier.Parent?.ResourceType}"); 266 | } 267 | } 268 | 269 | public static async Task GetPolicyAssignmentAsync(ResourceIdentifier resourceIdentifier, ArmClient armClient, CancellationToken cancellationToken) 270 | { 271 | var pa = armClient.GetPolicyAssignmentResource(resourceIdentifier); 272 | var paResponse = await pa.GetAsync(cancellationToken: cancellationToken); 273 | if (paResponse is null || paResponse.GetRawResponse().ContentStream is not { } paContentStream) 274 | { 275 | throw new InvalidOperationException($"Failed to fetch resource from Id '{resourceIdentifier}'"); 276 | } 277 | paContentStream.Position = 0; 278 | return await JsonSerializer.DeserializeAsync(paContentStream, cancellationToken: cancellationToken); 279 | } 280 | 281 | private static async Task>> GetResponseDictionaryAsync(Dictionary>> taskList) 282 | { 283 | var resultListPairs = await Task.WhenAll(taskList.Select(async result => 284 | new { result.Key, Value = await result.Value })); 285 | return resultListPairs.ToDictionary(result => result.Key, result => result.Value); 286 | } 287 | } -------------------------------------------------------------------------------- /Resolve-Dependency.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .DESCRIPTION 3 | Bootstrap script for PSDepend. 4 | 5 | .PARAMETER DependencyFile 6 | Specifies the configuration file for the this script. The default value is 7 | 'RequiredModules.psd1' relative to this script's path. 8 | 9 | .PARAMETER PSDependTarget 10 | Path for PSDepend to be bootstrapped and save other dependencies. 11 | Can also be CurrentUser or AllUsers if you wish to install the modules in 12 | such scope. The default value is 'output/RequiredModules' relative to 13 | this script's path. 14 | 15 | .PARAMETER Proxy 16 | Specifies the URI to use for Proxy when attempting to bootstrap 17 | PackageProvider and PowerShellGet. 18 | 19 | .PARAMETER ProxyCredential 20 | Specifies the credential to contact the Proxy when provided. 21 | 22 | .PARAMETER Scope 23 | Specifies the scope to bootstrap the PackageProvider and PSGet if not available. 24 | THe default value is 'CurrentUser'. 25 | 26 | .PARAMETER Gallery 27 | Specifies the gallery to use when bootstrapping PackageProvider, PSGet and 28 | when calling PSDepend (can be overridden in Dependency files). The default 29 | value is 'PSGallery'. 30 | 31 | .PARAMETER GalleryCredential 32 | Specifies the credentials to use with the Gallery specified above. 33 | 34 | .PARAMETER AllowOldPowerShellGetModule 35 | Allow you to use a locally installed version of PowerShellGet older than 36 | 1.6.0 (not recommended). Default it will install the latest PowerShellGet 37 | if an older version than 2.0 is detected. 38 | 39 | .PARAMETER MinimumPSDependVersion 40 | Allow you to specify a minimum version fo PSDepend, if you're after specific 41 | features. 42 | 43 | .PARAMETER AllowPrerelease 44 | Not yet written. 45 | 46 | .PARAMETER WithYAML 47 | Not yet written. 48 | 49 | .NOTES 50 | Load defaults for parameters values from Resolve-Dependency.psd1 if not 51 | provided as parameter. 52 | #> 53 | [CmdletBinding()] 54 | param 55 | ( 56 | [Parameter()] 57 | [System.String] 58 | $DependencyFile = 'RequiredModules.psd1', 59 | 60 | [Parameter()] 61 | [System.String] 62 | $PSDependTarget = (Join-Path -Path $PSScriptRoot -ChildPath 'output/RequiredModules'), 63 | 64 | [Parameter()] 65 | [System.Uri] 66 | $Proxy, 67 | 68 | [Parameter()] 69 | [System.Management.Automation.PSCredential] 70 | $ProxyCredential, 71 | 72 | [Parameter()] 73 | [ValidateSet('CurrentUser', 'AllUsers')] 74 | [System.String] 75 | $Scope = 'CurrentUser', 76 | 77 | [Parameter()] 78 | [System.String] 79 | $Gallery = 'PSGallery', 80 | 81 | [Parameter()] 82 | [System.Management.Automation.PSCredential] 83 | $GalleryCredential, 84 | 85 | [Parameter()] 86 | [System.Management.Automation.SwitchParameter] 87 | $AllowOldPowerShellGetModule, 88 | 89 | [Parameter()] 90 | [System.String] 91 | $MinimumPSDependVersion, 92 | 93 | [Parameter()] 94 | [System.Management.Automation.SwitchParameter] 95 | $AllowPrerelease, 96 | 97 | [Parameter()] 98 | [System.Management.Automation.SwitchParameter] 99 | $WithYAML, 100 | 101 | [Parameter()] 102 | [System.Collections.Hashtable] 103 | $RegisterGallery 104 | ) 105 | 106 | try 107 | { 108 | if ($PSVersionTable.PSVersion.Major -le 5) 109 | { 110 | if (-not (Get-Command -Name 'Import-PowerShellDataFile' -ErrorAction 'SilentlyContinue')) 111 | { 112 | Import-Module -Name Microsoft.PowerShell.Utility -RequiredVersion '3.1.0.0' 113 | } 114 | 115 | <# 116 | Making sure the imported PackageManagement module is not from PS7 module 117 | path. The VSCode PS extension is changing the $env:PSModulePath and 118 | prioritize the PS7 path. This is an issue with PowerShellGet because 119 | it loads an old version if available (or fail to load latest). 120 | #> 121 | Get-Module -ListAvailable PackageManagement | 122 | Where-Object -Property 'ModuleBase' -NotMatch 'powershell.7' | 123 | Select-Object -First 1 | 124 | Import-Module -Force 125 | } 126 | 127 | Write-Verbose -Message 'Importing Bootstrap default parameters from ''$PSScriptRoot/Resolve-Dependency.psd1''.' 128 | 129 | $resolveDependencyConfigPath = Join-Path -Path $PSScriptRoot -ChildPath '.\Resolve-Dependency.psd1' -Resolve -ErrorAction 'Stop' 130 | 131 | $resolveDependencyDefaults = Import-PowerShellDataFile -Path $resolveDependencyConfigPath 132 | 133 | $parameterToDefault = $MyInvocation.MyCommand.ParameterSets.Where{ $_.Name -eq $PSCmdlet.ParameterSetName }.Parameters.Keys 134 | 135 | if ($parameterToDefault.Count -eq 0) 136 | { 137 | $parameterToDefault = $MyInvocation.MyCommand.Parameters.Keys 138 | } 139 | 140 | # Set the parameters available in the Parameter Set, or it's not possible to choose yet, so all parameters are an option. 141 | foreach ($parameterName in $parameterToDefault) 142 | { 143 | if (-not $PSBoundParameters.Keys.Contains($parameterName) -and $resolveDependencyDefaults.ContainsKey($parameterName)) 144 | { 145 | Write-Verbose -Message "Setting parameter '$parameterName' to value '$($resolveDependencyDefaults[$parameterName])'." 146 | 147 | try 148 | { 149 | $variableValue = $resolveDependencyDefaults[$parameterName] 150 | 151 | if ($variableValue -is [System.String]) 152 | { 153 | $variableValue = $ExecutionContext.InvokeCommand.ExpandString($variableValue) 154 | } 155 | 156 | $PSBoundParameters.Add($parameterName, $variableValue) 157 | 158 | Set-Variable -Name $parameterName -Value $variableValue -Force -ErrorAction 'SilentlyContinue' 159 | } 160 | catch 161 | { 162 | Write-Verbose -Message "Error adding default for $parameterName : $($_.Exception.Message)." 163 | } 164 | } 165 | } 166 | } 167 | catch 168 | { 169 | Write-Warning -Message "Error attempting to import Bootstrap's default parameters from '$resolveDependencyConfigPath': $($_.Exception.Message)." 170 | } 171 | 172 | Write-Progress -Activity 'Bootstrap:' -PercentComplete 0 -CurrentOperation 'NuGet Bootstrap' 173 | 174 | $importModuleParameters = @{ 175 | Name = 'PowerShellGet' 176 | MinimumVersion = '2.0' 177 | ErrorAction = 'SilentlyContinue' 178 | PassThru = $true 179 | } 180 | 181 | if ($AllowOldPowerShellGetModule) 182 | { 183 | $importModuleParameters.Remove('MinimumVersion') 184 | } 185 | 186 | $powerShellGetModule = Import-Module @importModuleParameters 187 | 188 | # Install the package provider if it is not available. 189 | $nuGetProvider = Get-PackageProvider -Name 'NuGet' -ListAvailable -ErrorAction 'SilentlyContinue' | 190 | Select-Object -First 1 191 | 192 | if (-not $powerShellGetModule -and -not $nuGetProvider) 193 | { 194 | $providerBootstrapParameters = @{ 195 | Name = 'NuGet' 196 | Force = $true 197 | ForceBootstrap = $true 198 | ErrorAction = 'Stop' 199 | Scope = $Scope 200 | } 201 | 202 | switch ($PSBoundParameters.Keys) 203 | { 204 | 'Proxy' 205 | { 206 | $providerBootstrapParameters.Add('Proxy', $Proxy) 207 | } 208 | 209 | 'ProxyCredential' 210 | { 211 | $providerBootstrapParameters.Add('ProxyCredential', $ProxyCredential) 212 | } 213 | 214 | 'AllowPrerelease' 215 | { 216 | $providerBootstrapParameters.Add('AllowPrerelease', $AllowPrerelease) 217 | } 218 | } 219 | 220 | Write-Information -MessageData 'Bootstrap: Installing NuGet Package Provider from the web (Make sure Microsoft addresses/ranges are allowed).' 221 | 222 | $null = Install-PackageProvider @providerBootstrapParameters 223 | 224 | $nuGetProvider = Get-PackageProvider -Name 'NuGet' -ListAvailable | Select-Object -First 1 225 | 226 | $nuGetProviderVersion = $nuGetProvider.Version.ToString() 227 | 228 | Write-Information -MessageData "Bootstrap: Importing NuGet Package Provider version $nuGetProviderVersion to current session." 229 | 230 | $Null = Import-PackageProvider -Name 'NuGet' -RequiredVersion $nuGetProviderVersion -Force 231 | } 232 | 233 | if ($RegisterGallery) 234 | { 235 | if ($RegisterGallery.ContainsKey('Name') -and -not [System.String]::IsNullOrEmpty($RegisterGallery.Name)) 236 | { 237 | $Gallery = $RegisterGallery.Name 238 | } 239 | else 240 | { 241 | $RegisterGallery.Name = $Gallery 242 | } 243 | 244 | Write-Progress -Activity 'Bootstrap:' -PercentComplete 7 -CurrentOperation "Verifying private package repository '$Gallery'" -Completed 245 | 246 | $previousRegisteredRepository = Get-PSRepository -Name $Gallery -ErrorAction 'SilentlyContinue' 247 | 248 | if ($previousRegisteredRepository.SourceLocation -ne $RegisterGallery.SourceLocation) 249 | { 250 | if ($previousRegisteredRepository) 251 | { 252 | Write-Progress -Activity 'Bootstrap:' -PercentComplete 9 -CurrentOperation "Re-registrering private package repository '$Gallery'" -Completed 253 | 254 | Unregister-PSRepository -Name $Gallery 255 | 256 | $unregisteredPreviousRepository = $true 257 | } 258 | else 259 | { 260 | Write-Progress -Activity 'Bootstrap:' -PercentComplete 9 -CurrentOperation "Registering private package repository '$Gallery'" -Completed 261 | } 262 | 263 | Register-PSRepository @RegisterGallery 264 | } 265 | } 266 | 267 | Write-Progress -Activity 'Bootstrap:' -PercentComplete 10 -CurrentOperation "Ensuring Gallery $Gallery is trusted" 268 | 269 | # Fail if the given PSGallery is not registered. 270 | $previousGalleryInstallationPolicy = (Get-PSRepository -Name $Gallery -ErrorAction 'Stop').InstallationPolicy 271 | 272 | if ($previousGalleryInstallationPolicy -ne 'Trusted') 273 | { 274 | # Only change policy if the repository is not trusted 275 | Set-PSRepository -Name $Gallery -InstallationPolicy 'Trusted' -ErrorAction 'Ignore' 276 | } 277 | 278 | try 279 | { 280 | Write-Progress -Activity 'Bootstrap:' -PercentComplete 25 -CurrentOperation 'Checking PowerShellGet' 281 | 282 | # Ensure the module is loaded and retrieve the version you have. 283 | $powerShellGetVersion = (Import-Module -Name 'PowerShellGet' -PassThru -ErrorAction 'SilentlyContinue').Version 284 | 285 | Write-Verbose -Message "Bootstrap: The PowerShellGet version is $powerShellGetVersion" 286 | 287 | # Versions below 2.0 are considered old, unreliable & not recommended 288 | if (-not $powerShellGetVersion -or ($powerShellGetVersion -lt [System.Version] '2.0' -and -not $AllowOldPowerShellGetModule)) 289 | { 290 | Write-Progress -Activity 'Bootstrap:' -PercentComplete 40 -CurrentOperation 'Fetching newer version of PowerShellGet' 291 | 292 | # PowerShellGet module not found, installing or saving it. 293 | if ($PSDependTarget -in 'CurrentUser', 'AllUsers') 294 | { 295 | Write-Debug -Message "PowerShellGet module not found. Attempting to install from Gallery $Gallery." 296 | 297 | Write-Warning -Message "Installing PowerShellGet in $PSDependTarget Scope." 298 | 299 | $installPowerShellGetParameters = @{ 300 | Name = 'PowerShellGet' 301 | Force = $true 302 | SkipPublisherCheck = $true 303 | AllowClobber = $true 304 | Scope = $Scope 305 | Repository = $Gallery 306 | } 307 | 308 | switch ($PSBoundParameters.Keys) 309 | { 310 | 'Proxy' 311 | { 312 | $installPowerShellGetParameters.Add('Proxy', $Proxy) 313 | } 314 | 315 | 'ProxyCredential' 316 | { 317 | $installPowerShellGetParameters.Add('ProxyCredential', $ProxyCredential) 318 | } 319 | 320 | 'GalleryCredential' 321 | { 322 | $installPowerShellGetParameters.Add('Credential', $GalleryCredential) 323 | } 324 | } 325 | 326 | Write-Progress -Activity 'Bootstrap:' -PercentComplete 60 -CurrentOperation 'Installing newer version of PowerShellGet' 327 | 328 | Install-Module @installPowerShellGetParameters 329 | } 330 | else 331 | { 332 | Write-Debug -Message "PowerShellGet module not found. Attempting to Save from Gallery $Gallery to $PSDependTarget" 333 | 334 | $saveModuleParameters = @{ 335 | Name = 'PowerShellGet' 336 | Repository = $Gallery 337 | Path = $PSDependTarget 338 | Force = $true 339 | } 340 | 341 | Write-Progress -Activity 'Bootstrap:' -PercentComplete 60 -CurrentOperation "Saving PowerShellGet from $Gallery to $Scope" 342 | 343 | Save-Module @saveModuleParameters 344 | } 345 | 346 | Write-Debug -Message 'Removing previous versions of PowerShellGet and PackageManagement from session' 347 | 348 | Get-Module -Name 'PowerShellGet' -All | Remove-Module -Force -ErrorAction 'SilentlyContinue' 349 | Get-Module -Name 'PackageManagement' -All | Remove-Module -Force 350 | 351 | Write-Progress -Activity 'Bootstrap:' -PercentComplete 65 -CurrentOperation 'Loading latest version of PowerShellGet' 352 | 353 | Write-Debug -Message 'Importing latest PowerShellGet and PackageManagement versions into session' 354 | 355 | if ($AllowOldPowerShellGetModule) 356 | { 357 | $powerShellGetModule = Import-Module -Name 'PowerShellGet' -Force -PassThru 358 | } 359 | else 360 | { 361 | Import-Module -Name 'PackageManagement' -MinimumVersion '1.4.8.1' -Force 362 | 363 | $powerShellGetModule = Import-Module -Name 'PowerShellGet' -MinimumVersion '2.2.5' -Force -PassThru 364 | } 365 | 366 | $powerShellGetVersion = $powerShellGetModule.Version.ToString() 367 | 368 | Write-Information -MessageData "Bootstrap: PowerShellGet version loaded is $powerShellGetVersion" 369 | } 370 | 371 | # Try to import the PSDepend module from the available modules. 372 | $getModuleParameters = @{ 373 | Name = 'PSDepend' 374 | ListAvailable = $true 375 | } 376 | 377 | $psDependModule = Get-Module @getModuleParameters 378 | 379 | if ($PSBoundParameters.ContainsKey('MinimumPSDependVersion')) 380 | { 381 | try 382 | { 383 | $psDependModule = $psDependModule | Where-Object -FilterScript { $_.Version -ge $MinimumPSDependVersion } 384 | } 385 | catch 386 | { 387 | throw ('There was a problem finding the minimum version of PSDepend. Error: {0}' -f $_) 388 | } 389 | } 390 | 391 | if (-not $psDependModule) 392 | { 393 | # PSDepend module not found, installing or saving it. 394 | if ($PSDependTarget -in 'CurrentUser', 'AllUsers') 395 | { 396 | Write-Debug -Message "PSDepend module not found. Attempting to install from Gallery '$Gallery'." 397 | 398 | Write-Warning -Message "Installing PSDepend in $PSDependTarget Scope." 399 | 400 | $installPSDependParameters = @{ 401 | Name = 'PSDepend' 402 | Repository = $Gallery 403 | Force = $true 404 | Scope = $PSDependTarget 405 | SkipPublisherCheck = $true 406 | AllowClobber = $true 407 | } 408 | 409 | if ($MinimumPSDependVersion) 410 | { 411 | $installPSDependParameters.Add('MinimumVersion', $MinimumPSDependVersion) 412 | } 413 | 414 | Write-Progress -Activity 'Bootstrap:' -PercentComplete 75 -CurrentOperation "Installing PSDepend from $Gallery" 415 | 416 | Install-Module @installPSDependParameters 417 | } 418 | else 419 | { 420 | Write-Debug -Message "PSDepend module not found. Attempting to Save from Gallery $Gallery to $PSDependTarget" 421 | 422 | $saveModuleParameters = @{ 423 | Name = 'PSDepend' 424 | Repository = $Gallery 425 | Path = $PSDependTarget 426 | Force = $true 427 | } 428 | 429 | if ($MinimumPSDependVersion) 430 | { 431 | $saveModuleParameters.add('MinimumVersion', $MinimumPSDependVersion) 432 | } 433 | 434 | Write-Progress -Activity 'Bootstrap:' -PercentComplete 75 -CurrentOperation "Saving PSDepend from $Gallery to $Scope" 435 | 436 | Save-Module @saveModuleParameters 437 | } 438 | } 439 | 440 | Write-Progress -Activity 'Bootstrap:' -PercentComplete 80 -CurrentOperation 'Loading PSDepend' 441 | 442 | $importModulePSDependParameters = @{ 443 | Name = 'PSDepend' 444 | ErrorAction = 'Stop' 445 | Force = $true 446 | } 447 | 448 | if ($PSBoundParameters.ContainsKey('MinimumPSDependVersion')) 449 | { 450 | $importModulePSDependParameters.Add('MinimumVersion', $MinimumPSDependVersion) 451 | } 452 | 453 | # We should have successfully bootstrapped PSDepend. Fail if not available. 454 | $null = Import-Module @importModulePSDependParameters 455 | 456 | if ($WithYAML) 457 | { 458 | Write-Progress -Activity 'Bootstrap:' -PercentComplete 82 -CurrentOperation 'Verifying PowerShell module PowerShell-Yaml' 459 | 460 | if (-not (Get-Module -ListAvailable -Name 'PowerShell-Yaml')) 461 | { 462 | Write-Progress -Activity 'Bootstrap:' -PercentComplete 85 -CurrentOperation 'Installing PowerShell module PowerShell-Yaml' 463 | 464 | Write-Verbose -Message "PowerShell-Yaml module not found. Attempting to Save from Gallery '$Gallery' to '$PSDependTarget'." 465 | 466 | $SaveModuleParam = @{ 467 | Name = 'PowerShell-Yaml' 468 | Repository = $Gallery 469 | Path = $PSDependTarget 470 | Force = $true 471 | } 472 | 473 | Save-Module @SaveModuleParam 474 | } 475 | else 476 | { 477 | Write-Verbose -Message 'PowerShell-Yaml is already available' 478 | } 479 | 480 | Write-Progress -Activity 'Bootstrap:' -PercentComplete 88 -CurrentOperation 'Importing PowerShell module PowerShell-Yaml' 481 | } 482 | 483 | Write-Progress -Activity 'Bootstrap:' -PercentComplete 90 -CurrentOperation 'Invoke PSDepend' 484 | 485 | Write-Progress -Activity 'PSDepend:' -PercentComplete 0 -CurrentOperation 'Restoring Build Dependencies' 486 | 487 | if (Test-Path -Path $DependencyFile) 488 | { 489 | $psDependParameters = @{ 490 | Force = $true 491 | Path = $DependencyFile 492 | } 493 | 494 | # TODO: Handle when the Dependency file is in YAML, and -WithYAML is specified. 495 | Invoke-PSDepend @psDependParameters 496 | } 497 | 498 | Write-Progress -Activity 'PSDepend:' -PercentComplete 100 -CurrentOperation 'Dependencies restored' -Completed 499 | 500 | Write-Progress -Activity 'Bootstrap:' -PercentComplete 100 -CurrentOperation 'Bootstrap complete' -Completed 501 | } 502 | finally 503 | { 504 | if ($RegisterGallery) 505 | { 506 | Write-Verbose -Message "Removing private package repository '$Gallery'." 507 | Unregister-PSRepository -Name $Gallery 508 | } 509 | 510 | if ($unregisteredPreviousRepository) 511 | { 512 | Write-Verbose -Message "Reverting private package repository '$Gallery' to previous location URI:s." 513 | 514 | $registerPSRepositoryParameters = @{ 515 | Name = $previousRegisteredRepository.Name 516 | InstallationPolicy = $previousRegisteredRepository.InstallationPolicy 517 | } 518 | 519 | if ($previousRegisteredRepository.SourceLocation) 520 | { 521 | $registerPSRepositoryParameters.SourceLocation = $previousRegisteredRepository.SourceLocation 522 | } 523 | 524 | if ($previousRegisteredRepository.PublishLocation) 525 | { 526 | $registerPSRepositoryParameters.PublishLocation = $previousRegisteredRepository.PublishLocation 527 | } 528 | 529 | if ($previousRegisteredRepository.ScriptSourceLocation) 530 | { 531 | $registerPSRepositoryParameters.ScriptSourceLocation = $previousRegisteredRepository.ScriptSourceLocation 532 | } 533 | 534 | if ($previousRegisteredRepository.ScriptPublishLocation) 535 | { 536 | $registerPSRepositoryParameters.ScriptPublishLocation = $previousRegisteredRepository.ScriptPublishLocation 537 | } 538 | 539 | Register-PSRepository @registerPSRepositoryParameters 540 | } 541 | 542 | # Only try to revert installation policy if the repository exist 543 | if ((Get-PSRepository -Name $Gallery -ErrorAction 'SilentlyContinue')) 544 | { 545 | if ($previousGalleryInstallationPolicy -and $previousGalleryInstallationPolicy -ne 'Trusted') 546 | { 547 | # Reverting the Installation Policy for the given gallery if it was not already trusted 548 | Set-PSRepository -Name $Gallery -InstallationPolicy $previousGalleryInstallationPolicy 549 | } 550 | } 551 | 552 | Write-Verbose -Message 'Project Bootstrapped, returning to Invoke-Build.' 553 | } 554 | --------------------------------------------------------------------------------