From 3f2483356772755e34a2fbbe25f34d61cb60d42b Mon Sep 17 00:00:00 2001 From: Dilmurod Makhamadaliev <104784252+DilmurodMak@users.noreply.github.com> Date: Fri, 4 Nov 2022 10:45:23 -0400 Subject: [PATCH 01/12] GitHub Actions Pull Request Workflow - On Pull Request to Specific File Path, only the required file path will be linted instead of running all linting workflow (#11) * change to linting workflow filter path * test doc linting wrokflow * test doc and dotnet linting wrokflow * test python and dotnet linting wrokflow * test python pull request workflow * test dotnet pull request workflow * test dotnet pull request workflow * test dotnet pull request workflow * test dotnet and docs request workflow * test docs pull request workflow * moving github actions to private repo * change linting workflow trigger to pull request only * update to linting docs * change to megalinter config * change to megalinter config * change to megalinter config * change to megalinter config --- .github/workflows/pr_docs.yaml | 69 +++++++++--------- .github/workflows/pr_dotnet.yaml | 103 +++++++++++++-------------- .github/workflows/pr_python.yaml | 63 ++++++++-------- .mega-linter.yml | 2 +- docs/getting_started.md | 2 +- docs/github_actions_lint_workflow.md | 24 ++----- 6 files changed, 120 insertions(+), 143 deletions(-) mode change 100644 => 100755 .github/workflows/pr_docs.yaml mode change 100644 => 100755 .github/workflows/pr_dotnet.yaml mode change 100644 => 100755 .github/workflows/pr_python.yaml mode change 100644 => 100755 docs/github_actions_lint_workflow.md diff --git a/.github/workflows/pr_docs.yaml b/.github/workflows/pr_docs.yaml old mode 100644 new mode 100755 index 4f5922e..304a87d --- a/.github/workflows/pr_docs.yaml +++ b/.github/workflows/pr_docs.yaml @@ -1,36 +1,33 @@ -name: pr_docs - -on: - push: - paths: - - "docs/**.md" - - "**.md" - pull_request: - paths-ignore: - - "docs/**.md" - - "**.md" - branches: - - main -jobs: - lint: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} - - - name: MegaLinter documentation flavor - uses: oxsecurity/megalinter/flavors/documentation@v6.12.0 - env: - IGNORE_GITIGNORED_FILES: true - VALIDATE_ALL_CODEBASE: true - PRINT_ALL_FILES: true - DISABLE: COPYPASTE,YAML - SPELL_CSPELL_CONFIG_FILE: /config/megalinter/.cspell.json - MARKDOWN_MARKDOWN_LINK_CHECK_CONFIG_FILE: /config/megalinter/.markdown-link-check.json - DISABLE_LINTERS: REPOSITORY_CHECKOV,REPOSITORY_TRIVY - FILTER_REGEX_EXCLUDE: '(BenchPress/|engine/|examples/|\.github/workflows|\.devcontainer|\.editorconfig|\.gitmodules|/framework/|samples/|\.sln|LICENSE)' - FILTER_REGEX_INCLUDE: '(docs/|\**.md)' - REPORT_OUTPUT_FOLDER: ${GITHUB_WORKSPACE}/megalinter-reports \ No newline at end of file +name: pr_docs + +on: + pull_request: + paths: + - "docs/**.md" + - "./**.md" + - ".github/workflows/pr_docs.yaml" + branches: + - main +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} + + - name: MegaLinter documentation flavor + uses: oxsecurity/megalinter/flavors/documentation@v6.12.0 + env: + IGNORE_GITIGNORED_FILES: true + VALIDATE_ALL_CODEBASE: true + PRINT_ALL_FILES: true + DISABLE: COPYPASTE,YAML + SPELL_CSPELL_CONFIG_FILE: /config/megalinter/.cspell.json + MARKDOWN_MARKDOWN_LINK_CHECK_CONFIG_FILE: /config/megalinter/.markdown-link-check.json + DISABLE_LINTERS: REPOSITORY_CHECKOV,REPOSITORY_TRIVY + FILTER_REGEX_EXCLUDE: '(BenchPress/|engine/|examples/|\.devcontainer|\.editorconfig|\.gitmodules|/framework/|samples/|\.sln|LICENSE)' + FILTER_REGEX_INCLUDE: '(docs/|\**.md)' + REPORT_OUTPUT_FOLDER: ${GITHUB_WORKSPACE}/megalinter-reports diff --git a/.github/workflows/pr_dotnet.yaml b/.github/workflows/pr_dotnet.yaml old mode 100644 new mode 100755 index 75ca869..91fa09b --- a/.github/workflows/pr_dotnet.yaml +++ b/.github/workflows/pr_dotnet.yaml @@ -1,53 +1,50 @@ -name: pr_dotnet - -on: - push: - paths: - - "framework/dotnet/**" - - "samples/dotnet/**" - pull_request: - paths-ignore: - - "framework/dotnet/**" - - "samples/dotnet/**" - branches: - - main - -env: - DOTNET_VERSION: '6.0.x' - -jobs: - lint: - name: lint-${{matrix.os}} - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ ubuntu-latest ] - - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} - - - name: MegaLinter dotnet flavor - uses: oxsecurity/megalinter/flavors/dotnet@v6.12.0 - env: - IGNORE_GITIGNORED_FILES: true - VALIDATE_ALL_CODEBASE: true - PRINT_ALL_FILES: true - DISABLE: SPELL,COPYPASTE,YAML - DISABLE_LINTERS: REPOSITORY_CHECKOV,REPOSITORY_TRIVY - FILTER_REGEX_EXCLUDE: '(BenchPress/|engine/|examples/|/docs|\.github/workflows|\.devcontainer|\.editorconfig|\.gitmodules|\.sln|\.md|LICENSE|/framework/python|samples/python)' - FILTER_REGEX_INCLUDE: '(framework/dotnet|samples/dotnet)' - REPORT_OUTPUT_FOLDER: ${GITHUB_WORKSPACE}/megalinter-reports - - - name: Setup dotnet - uses: actions/setup-dotnet@v1 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - - - name: Install dependencies - run: dotnet restore - - - name: Build - run: dotnet build --configuration Release --no-restore +name: pr_dotnet + +on: + pull_request: + paths: + - "framework/dotnet/**" + - "samples/dotnet/**" + - ".github/workflows/pr_dotnet.yaml" + branches: + - main + +env: + DOTNET_VERSION: '6.0.x' + +jobs: + lint: + name: lint-${{matrix.os}} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest ] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} + + - name: MegaLinter dotnet flavor + uses: oxsecurity/megalinter/flavors/dotnet@v6.12.0 + env: + IGNORE_GITIGNORED_FILES: true + VALIDATE_ALL_CODEBASE: true + PRINT_ALL_FILES: true + DISABLE: SPELL,COPYPASTE,YAML + DISABLE_LINTERS: REPOSITORY_CHECKOV,REPOSITORY_TRIVY + FILTER_REGEX_EXCLUDE: '(BenchPress/|engine/|examples/|/docs|\.devcontainer|\.editorconfig|\.gitmodules|\.sln|\.md|LICENSE|/framework/python|samples/python)' + FILTER_REGEX_INCLUDE: '(framework/dotnet|samples/dotnet)' + REPORT_OUTPUT_FOLDER: ${GITHUB_WORKSPACE}/megalinter-reports + + - name: Setup dotnet + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Install dependencies + run: dotnet restore + + - name: Build + run: dotnet build --configuration Release --no-restore diff --git a/.github/workflows/pr_python.yaml b/.github/workflows/pr_python.yaml old mode 100644 new mode 100755 index 878087b..a1f108e --- a/.github/workflows/pr_python.yaml +++ b/.github/workflows/pr_python.yaml @@ -1,34 +1,31 @@ -name: pr_python - -on: - push: - paths: - - "framework/python/**" - - "samples/python/**" - pull_request: - paths-ignore: - - "framework/python/**" - - "samples/python/**" - branches: - - main -jobs: - lint: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} - - - name: MegaLinter Python flavor - uses: oxsecurity/megalinter/flavors/python@v6.12.0 - env: - IGNORE_GITIGNORED_FILES: true - VALIDATE_ALL_CODEBASE: true - PRINT_ALL_FILES: true - DISABLE: SPELL,COPYPASTE,YAML - DISABLE_LINTERS: PYTHON_MYPY,REPOSITORY_CHECKOV,REPOSITORY_TRIVY - FILTER_REGEX_EXCLUDE: '(BenchPress/|engine/|examples/|\.github/workflows|\.devcontainer|\.editorconfig|\.gitmodules|/docs|/framework/dotnet|samples/dotnet|\.sln|\.md|LICENSE)' - FILTER_REGEX_INCLUDE: '(framework/python|samples/python)' +name: pr_python + +on: + pull_request: + paths: + - "framework/python/**" + - "samples/python/**" + - ".github/workflows/pr_python.yaml" + branches: + - main +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} + + - name: MegaLinter Python flavor + uses: oxsecurity/megalinter/flavors/python@v6.12.0 + env: + IGNORE_GITIGNORED_FILES: true + VALIDATE_ALL_CODEBASE: true + PRINT_ALL_FILES: true + DISABLE: SPELL,COPYPASTE,YAML + DISABLE_LINTERS: PYTHON_MYPY,REPOSITORY_CHECKOV,REPOSITORY_TRIVY + FILTER_REGEX_EXCLUDE: '(BenchPress/|engine/|examples/|\.devcontainer|\.editorconfig|\.gitmodules|/docs|/framework/dotnet|samples/dotnet|\.sln|\.md|LICENSE)' + FILTER_REGEX_INCLUDE: '(framework/python|samples/python)' REPORT_OUTPUT_FOLDER: ${GITHUB_WORKSPACE}/megalinter-reports \ No newline at end of file diff --git a/.mega-linter.yml b/.mega-linter.yml index 0d9c153..1b57670 100644 --- a/.mega-linter.yml +++ b/.mega-linter.yml @@ -5,7 +5,7 @@ APPLY_FIXES: all # all, none, or list of linter keys # DISABLE: # - COPYPASTE # Uncomment to disable checks of excessive copy-pastes # - SPELL # Uncomment to disable checks of spelling mistakes -DISABLE_LINTERS: +DISABLE_LINTERS: - PYTHON_PYRIGHT - PYTHON_MYPY SHOW_ELAPSED_TIME: true diff --git a/docs/getting_started.md b/docs/getting_started.md index ef8683f..7de5625 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -11,7 +11,7 @@ Visual Studio Code supports compilation and development on a container known as If you’re using VS Code, please install see the installation guide to install Docker and VS Code extension: https://code.visualstudio.com/docs/remote/containers#_installation -Then launch the environment by opening the command pallete Shift+Command+P (Mac) / Ctrl+Shift+P (Windows/Linux) and running `Dev Containers: Open Folder in Container` +Then launch the environment by opening the command palette Shift+Command+P (Mac) / Ctrl+Shift+P (Windows/Linux) and running `Dev Containers: Open Folder in Container` The Dev Container configuration also contains VS Code extensions for linting/formatting/testing/compilation. diff --git a/docs/github_actions_lint_workflow.md b/docs/github_actions_lint_workflow.md old mode 100644 new mode 100755 index 0d3665a..cc02db3 --- a/docs/github_actions_lint_workflow.md +++ b/docs/github_actions_lint_workflow.md @@ -33,10 +33,6 @@ This is an example of running `.github/workflows/pr_dotnet.yml` file to lint dot name: pr_dotnet on: - push: - paths: - - "framework/dotnet/**" - - "samples/dotnet/**" pull_request: paths-ignore: - "framework/dotnet/**" @@ -74,25 +70,15 @@ act pull_request --workflows .\.github\workflows\pr_docs.yaml #### Linting overview -When new code gets pushed to dotnet directories, the linting gets triggered - -```yaml -on: - push: - paths: - - "framework/dotnet/**" - - "samples/dotnet/**" -``` - When pull_request is created, following directories gets skipped from linting to allow the merge ```yaml pull_request: - paths-ignore: - - "framework/dotnet/**" - - "samples/dotnet/**" - branches: - - main + paths-ignore: + - "framework/dotnet/**" + - "samples/dotnet/**" + branches: + - main ``` In this pr_dotnet workflow, we are using Megalinter flavor for dotnet From 29925e8d0660cbbf3ebe29c3746a7d73f5367cd8 Mon Sep 17 00:00:00 2001 From: jessica-ern <107070686+jessica-ern@users.noreply.github.com> Date: Fri, 4 Nov 2022 09:55:10 -0500 Subject: [PATCH 02/12] basic engine layout (#1) * basic engine layout * addressing PR comments * add engine and proto files to the github workflows * fix linter erros hopefully * trying line endings again * we don't actually need that file anyway * remove push jobs * updated the docs with issues I ran into * fix docker files * add back in accidentally removed line * added some stuff to the getting started doc * remove some stuff from the getting started doc --- .devcontainer/Dockerfile | 3 - .devcontainer/devcontainer.json | 11 +- .github/workflows/pr_docs.yaml | 66 ++++++------ .github/workflows/pr_dotnet.yaml | 102 +++++++++--------- .github/workflows/pr_python.yaml | 63 +++++------ docs/getting_started.md | 33 +++--- .../BenchPress.TestEngine.Tests.csproj | 5 + .../BicepServiceTests.cs | 41 +++++++ .../Helpers/MockServerCallContext.cs | 33 ++++++ .../ResourceGroupServiceTests.cs | 26 +++++ .../BenchPress.TestEngine.Tests/UnitTest1.cs | 10 -- engine/BenchPress.TestEngine.Tests/Usings.cs | 6 +- .../BenchPress.TestEngine.csproj | 14 ++- engine/BenchPress.TestEngine/Program.cs | 27 ++++- .../Services/BicepService.cs | 21 ++++ .../Services/ResourceGroupService.cs | 16 +++ engine/BenchPress.TestEngine/Usings.cs | 2 + .../appsettings.Development.json | 8 ++ engine/BenchPress.TestEngine/appsettings.json | 14 +++ protos/bicep.proto | 29 +++++ protos/resource_group.proto | 19 ++++ 21 files changed, 398 insertions(+), 151 deletions(-) create mode 100644 engine/BenchPress.TestEngine.Tests/BicepServiceTests.cs create mode 100644 engine/BenchPress.TestEngine.Tests/Helpers/MockServerCallContext.cs create mode 100644 engine/BenchPress.TestEngine.Tests/ResourceGroupServiceTests.cs delete mode 100644 engine/BenchPress.TestEngine.Tests/UnitTest1.cs create mode 100644 engine/BenchPress.TestEngine/Services/BicepService.cs create mode 100644 engine/BenchPress.TestEngine/Services/ResourceGroupService.cs create mode 100644 engine/BenchPress.TestEngine/Usings.cs create mode 100644 engine/BenchPress.TestEngine/appsettings.Development.json create mode 100644 engine/BenchPress.TestEngine/appsettings.json create mode 100644 protos/bicep.proto create mode 100644 protos/resource_group.proto diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 25c33ca..d8deaf9 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,6 +1,3 @@ # [Choice] .NET version: 6.0, 5.0, 3.1, 6.0-bullseye, 5.0-bullseye, 3.1-bullseye, 6.0-focal, 5.0-focal, 3.1-focal ARG VARIANT="6.0" FROM mcr.microsoft.com/vscode/devcontainers/dotnet:${VARIANT} -SHELL ["pwsh", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] -RUN Install-Module -Name Pester -Force -SkipPublisherCheck -RUN Install-Module -Name Az -Force -SkipPublisherCheck diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 74f8c9d..ab328e2 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -7,6 +7,7 @@ "VARIANT": "6.0" } }, + // Configure tool-specific properties. "customizations": { // Configure properties specific to VS Code. @@ -27,7 +28,7 @@ "python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle", "python.linting.pylintPath": "/usr/local/py-utils/bin/pylint" }, - + // Add the IDs of extensions you want installed when the container is created. "extensions": [ // ARM tools @@ -38,6 +39,7 @@ // C# "ms-dotnettools.csharp", + // Python "ms-python.python", "ms-python.vscode-pylance", @@ -52,9 +54,10 @@ // Use 'postCreateCommand' to run commands after the container is created. // Installs: - // 1. Mega Linter` - // 2. Configures benchpress Python module - "postCreateCommand": "npm install -g mega-linter-runner && pip install --editable ./framework/python/", + // 1. Mega Linter + // 2. Pester + // 3. Configures benchpress Python module + "postCreateCommand": "npm install -g mega-linter-runner && pwsh -command Install-Module -Name Pester -Force -SkipPublisherCheck && pip install --editable ./framework/python/", // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode", diff --git a/.github/workflows/pr_docs.yaml b/.github/workflows/pr_docs.yaml index 304a87d..b855353 100755 --- a/.github/workflows/pr_docs.yaml +++ b/.github/workflows/pr_docs.yaml @@ -1,33 +1,33 @@ -name: pr_docs - -on: - pull_request: - paths: - - "docs/**.md" - - "./**.md" - - ".github/workflows/pr_docs.yaml" - branches: - - main -jobs: - lint: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} - - - name: MegaLinter documentation flavor - uses: oxsecurity/megalinter/flavors/documentation@v6.12.0 - env: - IGNORE_GITIGNORED_FILES: true - VALIDATE_ALL_CODEBASE: true - PRINT_ALL_FILES: true - DISABLE: COPYPASTE,YAML - SPELL_CSPELL_CONFIG_FILE: /config/megalinter/.cspell.json - MARKDOWN_MARKDOWN_LINK_CHECK_CONFIG_FILE: /config/megalinter/.markdown-link-check.json - DISABLE_LINTERS: REPOSITORY_CHECKOV,REPOSITORY_TRIVY - FILTER_REGEX_EXCLUDE: '(BenchPress/|engine/|examples/|\.devcontainer|\.editorconfig|\.gitmodules|/framework/|samples/|\.sln|LICENSE)' - FILTER_REGEX_INCLUDE: '(docs/|\**.md)' - REPORT_OUTPUT_FOLDER: ${GITHUB_WORKSPACE}/megalinter-reports +name: pr_docs + +on: + pull_request: + paths: + - "docs/**.md" + - "./**.md" + - ".github/workflows/pr_docs.yaml" + branches: + - main +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} + + - name: MegaLinter documentation flavor + uses: oxsecurity/megalinter/flavors/documentation@v6.12.0 + env: + IGNORE_GITIGNORED_FILES: true + VALIDATE_ALL_CODEBASE: true + PRINT_ALL_FILES: true + DISABLE: COPYPASTE,YAML + SPELL_CSPELL_CONFIG_FILE: /config/megalinter/.cspell.json + MARKDOWN_MARKDOWN_LINK_CHECK_CONFIG_FILE: /config/megalinter/.markdown-link-check.json + DISABLE_LINTERS: REPOSITORY_CHECKOV,REPOSITORY_TRIVY + FILTER_REGEX_EXCLUDE: '(BenchPress/|engine/|examples/|\.devcontainer|\.editorconfig|\.gitmodules|/framework/|samples/|\.sln|LICENSE)' + FILTER_REGEX_INCLUDE: '(docs/|\**.md)' + REPORT_OUTPUT_FOLDER: ${GITHUB_WORKSPACE}/megalinter-reports diff --git a/.github/workflows/pr_dotnet.yaml b/.github/workflows/pr_dotnet.yaml index 91fa09b..1c0561b 100755 --- a/.github/workflows/pr_dotnet.yaml +++ b/.github/workflows/pr_dotnet.yaml @@ -1,50 +1,52 @@ -name: pr_dotnet - -on: - pull_request: - paths: - - "framework/dotnet/**" - - "samples/dotnet/**" - - ".github/workflows/pr_dotnet.yaml" - branches: - - main - -env: - DOTNET_VERSION: '6.0.x' - -jobs: - lint: - name: lint-${{matrix.os}} - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ ubuntu-latest ] - - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} - - - name: MegaLinter dotnet flavor - uses: oxsecurity/megalinter/flavors/dotnet@v6.12.0 - env: - IGNORE_GITIGNORED_FILES: true - VALIDATE_ALL_CODEBASE: true - PRINT_ALL_FILES: true - DISABLE: SPELL,COPYPASTE,YAML - DISABLE_LINTERS: REPOSITORY_CHECKOV,REPOSITORY_TRIVY - FILTER_REGEX_EXCLUDE: '(BenchPress/|engine/|examples/|/docs|\.devcontainer|\.editorconfig|\.gitmodules|\.sln|\.md|LICENSE|/framework/python|samples/python)' - FILTER_REGEX_INCLUDE: '(framework/dotnet|samples/dotnet)' - REPORT_OUTPUT_FOLDER: ${GITHUB_WORKSPACE}/megalinter-reports - - - name: Setup dotnet - uses: actions/setup-dotnet@v1 - with: - dotnet-version: ${{ env.DOTNET_VERSION }} - - - name: Install dependencies - run: dotnet restore - - - name: Build - run: dotnet build --configuration Release --no-restore +name: pr_dotnet + +on: + pull_request: + paths: + - "framework/dotnet/**" + - "samples/dotnet/**" + - "engine/**" + - "protos/**" + - ".github/workflows/pr_dotnet.yaml" + branches: + - main + +env: + DOTNET_VERSION: '6.0.x' + +jobs: + lint: + name: lint-${{matrix.os}} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest ] + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} + + - name: MegaLinter dotnet flavor + uses: oxsecurity/megalinter/flavors/dotnet@v6.12.0 + env: + IGNORE_GITIGNORED_FILES: true + VALIDATE_ALL_CODEBASE: true + PRINT_ALL_FILES: true + DISABLE: SPELL,COPYPASTE,YAML + DISABLE_LINTERS: REPOSITORY_CHECKOV,REPOSITORY_TRIVY + FILTER_REGEX_EXCLUDE: '(BenchPress/|examples/|/docs|\.devcontainer|\.editorconfig|\.gitmodules|\.sln|\.md|LICENSE|/framework/python|samples/python)' + FILTER_REGEX_INCLUDE: '(framework/dotnet|samples/dotnet|engine/)' + REPORT_OUTPUT_FOLDER: ${GITHUB_WORKSPACE}/megalinter-reports + + - name: Setup dotnet + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Install dependencies + run: dotnet restore + + - name: Build + run: dotnet build --configuration Release --no-restore diff --git a/.github/workflows/pr_python.yaml b/.github/workflows/pr_python.yaml index a1f108e..6189a7f 100755 --- a/.github/workflows/pr_python.yaml +++ b/.github/workflows/pr_python.yaml @@ -1,31 +1,32 @@ -name: pr_python - -on: - pull_request: - paths: - - "framework/python/**" - - "samples/python/**" - - ".github/workflows/pr_python.yaml" - branches: - - main -jobs: - lint: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} - - - name: MegaLinter Python flavor - uses: oxsecurity/megalinter/flavors/python@v6.12.0 - env: - IGNORE_GITIGNORED_FILES: true - VALIDATE_ALL_CODEBASE: true - PRINT_ALL_FILES: true - DISABLE: SPELL,COPYPASTE,YAML - DISABLE_LINTERS: PYTHON_MYPY,REPOSITORY_CHECKOV,REPOSITORY_TRIVY - FILTER_REGEX_EXCLUDE: '(BenchPress/|engine/|examples/|\.devcontainer|\.editorconfig|\.gitmodules|/docs|/framework/dotnet|samples/dotnet|\.sln|\.md|LICENSE)' - FILTER_REGEX_INCLUDE: '(framework/python|samples/python)' - REPORT_OUTPUT_FOLDER: ${GITHUB_WORKSPACE}/megalinter-reports \ No newline at end of file +name: pr_python + +on: + pull_request: + paths: + - "framework/python/**" + - "samples/python/**" + - "protos/**" + - ".github/workflows/pr_python.yaml" + branches: + - main +jobs: + lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + with: + token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} + + - name: MegaLinter Python flavor + uses: oxsecurity/megalinter/flavors/python@v6.12.0 + env: + IGNORE_GITIGNORED_FILES: true + VALIDATE_ALL_CODEBASE: true + PRINT_ALL_FILES: true + DISABLE: SPELL,COPYPASTE,YAML + DISABLE_LINTERS: PYTHON_MYPY,REPOSITORY_CHECKOV,REPOSITORY_TRIVY + FILTER_REGEX_EXCLUDE: '(BenchPress/|engine/|examples/|\.devcontainer|\.editorconfig|\.gitmodules|/docs|/framework/dotnet|samples/dotnet|\.sln|\.md|LICENSE)' + FILTER_REGEX_INCLUDE: '(framework/python|samples/python)' + REPORT_OUTPUT_FOLDER: ${GITHUB_WORKSPACE}/megalinter-reports diff --git a/docs/getting_started.md b/docs/getting_started.md index 7de5625..4892d41 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -1,21 +1,27 @@ # Getting started This guide walks you through the process of starting development on *Benchpress*. -## Setting up the development environment +## Development environment setup within VS Code +If you’re using [Visual Studio Code](https://code.visualstudio.com/) as your IDE of choice, then this project contains all of the necessary configurations to bootstrap your development environment using a container known as a [Dev Container](https://code.visualstudio.com/docs/remote/containers). -If you’re using [Visual Studio Code](https://code.visualstudio.com/) as your IDE of choice, then this project contains all of the necessary configurations to bootstrap your development environment. See the section on [Development environment setup within VS Code -](#development-environment-setup-within-vs-code) - -### Development environment setup within VS Code -Visual Studio Code supports compilation and development on a container known as [Dev Containers](https://code.visualstudio.com/docs/remote/containers). - -If you’re using VS Code, please install see the installation guide to install Docker and VS Code extension: https://code.visualstudio.com/docs/remote/containers#_installation +To use the Dev Container, please follow the installation guide to install Docker and the VS Code extension: https://code.visualstudio.com/docs/remote/containers#_installation Then launch the environment by opening the command palette Shift+Command+P (Mac) / Ctrl+Shift+P (Windows/Linux) and running `Dev Containers: Open Folder in Container` -The Dev Container configuration also contains VS Code extensions for linting/formatting/testing/compilation. +The Dev Container configuration also contains VS Code extensions for linting, formatting, testing, and compilation. -### Development dependencies +### Authenticating git within the dev container + +For MacOs, make sure your ssh key is properly added to your key-chain + +1. Call `ssh-add -l` in your **host** terminal. If your key is not in your key-chain, it will say `The agent has no identities` or the identities listed will not include the key you use to authenticate with git. +2. To add your key to the key-chain, call `shh-add ` (most likely, `ssh-add ~/.ssh/id_rsa`). +3. Call `ssh-add -l` on your **host** terminal again to verify the identity has been added. +4. Call `ssh-add -l` in your **dev container** terminal to verify that it is accessible in the container. Now you should be able to authenticate with git from within the dev container. + +For Windows, try following [this stack overflow post](https://stackoverflow.com/questions/56490194/vs-code-bitbucket-ssh-permission-denied-publickey/72029153#72029153) + +## Development dependencies if not using VS Code Depending on the feature/language you are working on, you may need to download and install language-specific packages, e.g., Python 3. List of requirements on development machine: @@ -24,11 +30,10 @@ List of requirements on development machine: - DotNet Core (version 6.0) - Node (>= version 14) - PowerShell 8 -- Python 3 +- Python 3.5 +- pip 9.0.1 -#### Python setup +### Python setup From the root directory, execute to install benchpress as a module that can be referenced: > pip install --editable ./framework/python/ - - diff --git a/engine/BenchPress.TestEngine.Tests/BenchPress.TestEngine.Tests.csproj b/engine/BenchPress.TestEngine.Tests/BenchPress.TestEngine.Tests.csproj index 8368121..26d1072 100644 --- a/engine/BenchPress.TestEngine.Tests/BenchPress.TestEngine.Tests.csproj +++ b/engine/BenchPress.TestEngine.Tests/BenchPress.TestEngine.Tests.csproj @@ -10,6 +10,7 @@ + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -25,4 +26,8 @@ + + + + diff --git a/engine/BenchPress.TestEngine.Tests/BicepServiceTests.cs b/engine/BenchPress.TestEngine.Tests/BicepServiceTests.cs new file mode 100644 index 0000000..12ffcde --- /dev/null +++ b/engine/BenchPress.TestEngine.Tests/BicepServiceTests.cs @@ -0,0 +1,41 @@ +namespace BenchPress.TestEngine.Tests; + + +public class BicepServiceTests +{ + private readonly BicepService bicepService; + private readonly ServerCallContext context; + + public BicepServiceTests() + { + var logger = new Mock>().Object; + bicepService = new BicepService(logger); + context = new MockServerCallContext(); + } + + [Fact(Skip = "Not Implemented")] + public async Task DeploymentGroupCreate_DeploysResourceGroup() + { + var request = new DeploymentGroupRequest + { + BicepFilePath = "main.bicep", + ParameterFilePath = "parameters.json", + ResourceGroupName = "test-rg", + SubscriptionNameOrId = new Guid().ToString() + }; + var result = await bicepService.DeploymentGroupCreate(request, context); + Assert.True(result.Success); + } + + [Fact(Skip = "Not Implemented")] + public async Task DeleteGroup_DeletesAllResources() + { + var request = new DeleteGroupRequest + { + ResourceGroupName = "test-rg", + SubscriptionNameOrId = new Guid().ToString() + }; + var result = await bicepService.DeleteGroup(request, context); + Assert.True(result.Success); + } +} \ No newline at end of file diff --git a/engine/BenchPress.TestEngine.Tests/Helpers/MockServerCallContext.cs b/engine/BenchPress.TestEngine.Tests/Helpers/MockServerCallContext.cs new file mode 100644 index 0000000..eb106ff --- /dev/null +++ b/engine/BenchPress.TestEngine.Tests/Helpers/MockServerCallContext.cs @@ -0,0 +1,33 @@ +namespace BenchPress.TestEngine.Tests; + +public class MockServerCallContext : ServerCallContext +{ + protected override string MethodCore => throw new NotImplementedException(); + + protected override string HostCore => throw new NotImplementedException(); + + protected override string PeerCore => throw new NotImplementedException(); + + protected override DateTime DeadlineCore => throw new NotImplementedException(); + + protected override Metadata RequestHeadersCore => throw new NotImplementedException(); + + protected override CancellationToken CancellationTokenCore => throw new NotImplementedException(); + + protected override Metadata ResponseTrailersCore => throw new NotImplementedException(); + + protected override Status StatusCore { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + protected override WriteOptions WriteOptionsCore { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + protected override AuthContext AuthContextCore => throw new NotImplementedException(); + + protected override ContextPropagationToken CreatePropagationTokenCore(ContextPropagationOptions options) + { + throw new NotImplementedException(); + } + + protected override Task WriteResponseHeadersAsyncCore(Metadata responseHeaders) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/engine/BenchPress.TestEngine.Tests/ResourceGroupServiceTests.cs b/engine/BenchPress.TestEngine.Tests/ResourceGroupServiceTests.cs new file mode 100644 index 0000000..369fa0d --- /dev/null +++ b/engine/BenchPress.TestEngine.Tests/ResourceGroupServiceTests.cs @@ -0,0 +1,26 @@ +namespace BenchPress.TestEngine.Tests; + +public class ResourceGroupServiceTests +{ + private readonly ResourceGroupService resourceGroupService; + private readonly ServerCallContext context; + + public ResourceGroupServiceTests() + { + var logger = new Mock>().Object; + resourceGroupService = new ResourceGroupService(logger); + context = new MockServerCallContext(); + } + + [Fact(Skip = "Not Implemented")] + public async Task GetResourceGroup_ResturnsResoureGroup() + { + var request = new ResourceGroupRequest + { + ResourceGroupName = "test-rg", + SubscriptionNameOrId = new Guid().ToString() + }; + var result = await resourceGroupService.GetResourceGroup(request, context); + Assert.True(result.Existed); + } +} \ No newline at end of file diff --git a/engine/BenchPress.TestEngine.Tests/UnitTest1.cs b/engine/BenchPress.TestEngine.Tests/UnitTest1.cs deleted file mode 100644 index 3380951..0000000 --- a/engine/BenchPress.TestEngine.Tests/UnitTest1.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace BenchPress.TestEngine.Tests; - -public class UnitTest1 -{ - [Fact] - public void Test1() - { - - } -} \ No newline at end of file diff --git a/engine/BenchPress.TestEngine.Tests/Usings.cs b/engine/BenchPress.TestEngine.Tests/Usings.cs index 8c927eb..4141a67 100644 --- a/engine/BenchPress.TestEngine.Tests/Usings.cs +++ b/engine/BenchPress.TestEngine.Tests/Usings.cs @@ -1 +1,5 @@ -global using Xunit; \ No newline at end of file +global using Xunit; +global using Moq; +global using Grpc.Core; +global using BenchPress.TestEngine.Services; +global using Microsoft.Extensions.Logging; \ No newline at end of file diff --git a/engine/BenchPress.TestEngine/BenchPress.TestEngine.csproj b/engine/BenchPress.TestEngine/BenchPress.TestEngine.csproj index d2f4073..38cb6e2 100644 --- a/engine/BenchPress.TestEngine/BenchPress.TestEngine.csproj +++ b/engine/BenchPress.TestEngine/BenchPress.TestEngine.csproj @@ -1,12 +1,20 @@ - + - Exe net6.0 - enable enable + enable + + + + + + + + + diff --git a/engine/BenchPress.TestEngine/Program.cs b/engine/BenchPress.TestEngine/Program.cs index 3751555..c90e594 100644 --- a/engine/BenchPress.TestEngine/Program.cs +++ b/engine/BenchPress.TestEngine/Program.cs @@ -1,2 +1,25 @@ -// See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World!"); +using BenchPress.TestEngine.Services; +using Microsoft.AspNetCore.Server.Kestrel.Core; + +var builder = WebApplication.CreateBuilder(args); + +// Additional configuration is required to successfully run gRPC on macOS. +// For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 +builder.WebHost.ConfigureKestrel(options => +{ + // Setup a HTTP/2 endpoint without TLS. + options.ListenLocalhost(5152, o => o.Protocols = + HttpProtocols.Http2); +}); + +// Add services to the container. +builder.Services.AddGrpc(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +app.MapGrpcService(); +app.MapGrpcService(); +app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); + +app.Run(); diff --git a/engine/BenchPress.TestEngine/Services/BicepService.cs b/engine/BenchPress.TestEngine/Services/BicepService.cs new file mode 100644 index 0000000..ed6f5a6 --- /dev/null +++ b/engine/BenchPress.TestEngine/Services/BicepService.cs @@ -0,0 +1,21 @@ +namespace BenchPress.TestEngine.Services; + +public class BicepService : Bicep.BicepBase +{ + private readonly ILogger logger; + + public BicepService(ILogger logger) + { + this.logger = logger; + } + + public override async Task DeploymentGroupCreate(DeploymentGroupRequest request, ServerCallContext context) + { + throw new NotImplementedException(); + } + + public override async Task DeleteGroup(DeleteGroupRequest request, ServerCallContext context) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/engine/BenchPress.TestEngine/Services/ResourceGroupService.cs b/engine/BenchPress.TestEngine/Services/ResourceGroupService.cs new file mode 100644 index 0000000..29c9f31 --- /dev/null +++ b/engine/BenchPress.TestEngine/Services/ResourceGroupService.cs @@ -0,0 +1,16 @@ +namespace BenchPress.TestEngine.Services; + +public class ResourceGroupService : ResourceGroup.ResourceGroupBase +{ + private readonly ILogger logger; + + public ResourceGroupService(ILogger logger) + { + this.logger = logger; + } + + public override async Task GetResourceGroup(ResourceGroupRequest rg, ServerCallContext context) + { + throw new NotImplementedException(); + } +} diff --git a/engine/BenchPress.TestEngine/Usings.cs b/engine/BenchPress.TestEngine/Usings.cs new file mode 100644 index 0000000..eeca8db --- /dev/null +++ b/engine/BenchPress.TestEngine/Usings.cs @@ -0,0 +1,2 @@ +global using Grpc.Core; +global using BenchPress.TestEngine; diff --git a/engine/BenchPress.TestEngine/appsettings.Development.json b/engine/BenchPress.TestEngine/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/engine/BenchPress.TestEngine/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/engine/BenchPress.TestEngine/appsettings.json b/engine/BenchPress.TestEngine/appsettings.json new file mode 100644 index 0000000..1aef507 --- /dev/null +++ b/engine/BenchPress.TestEngine/appsettings.json @@ -0,0 +1,14 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "Kestrel": { + "EndpointDefaults": { + "Protocols": "Http2" + } + } +} diff --git a/protos/bicep.proto b/protos/bicep.proto new file mode 100644 index 0000000..31efa87 --- /dev/null +++ b/protos/bicep.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +package benchpress; + +option csharp_namespace = "BenchPress.TestEngine"; + +// Currently only supports deployments with the target scope of resource group. +// Other scopes: subscription, management group, and tenant. +service Bicep { + rpc DeploymentGroupCreate (DeploymentGroupRequest) returns (DeploymentResult); + rpc DeleteGroup (DeleteGroupRequest) returns (DeploymentResult); +} + +message DeploymentGroupRequest { + string bicep_file_path = 1; + string parameter_file_path = 2; + string resource_group_name = 3; + string subscription_name_or_id = 4; +} + +message DeleteGroupRequest { + string resource_group_name = 1; + string subscription_name_or_id = 2; +} + +message DeploymentResult { + bool success = 1; + string error_message = 2; +} diff --git a/protos/resource_group.proto b/protos/resource_group.proto new file mode 100644 index 0000000..bee036c --- /dev/null +++ b/protos/resource_group.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package benchpress; + +option csharp_namespace = "BenchPress.TestEngine"; + +service ResourceGroup { + rpc GetResourceGroup (ResourceGroupRequest) returns (ResourceGroupResponse); +} + +message ResourceGroupRequest { + string resource_group_name = 1; + string subscription_name_or_id = 2; +} + +message ResourceGroupResponse { + bool existed = 1; + string resource_json = 2; +} From 25e107cd93f40131ba2f730a689ee6aa0985772e Mon Sep 17 00:00:00 2001 From: Uffaz Nathaniel Date: Fri, 4 Nov 2022 15:55:40 -0700 Subject: [PATCH 03/12] Dev Container: Add a post create shell script for post-creation actions (#12) * A post-create script that gets executed when the container first launches * Post create shell script Co-authored-by: Uffaz --- .devcontainer/Dockerfile | 4 ++++ .devcontainer/devcontainer.json | 3 ++- .devcontainer/scripts/post-create.sh | 10 ++++++++++ 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 .devcontainer/scripts/post-create.sh diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index d8deaf9..4007ef6 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,3 +1,7 @@ # [Choice] .NET version: 6.0, 5.0, 3.1, 6.0-bullseye, 5.0-bullseye, 3.1-bullseye, 6.0-focal, 5.0-focal, 3.1-focal ARG VARIANT="6.0" FROM mcr.microsoft.com/vscode/devcontainers/dotnet:${VARIANT} + +COPY ./scripts/post-create.sh /benchpress/ +RUN chmod +x /benchpress/post-create.sh + diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index ab328e2..43f0f46 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -57,7 +57,7 @@ // 1. Mega Linter // 2. Pester // 3. Configures benchpress Python module - "postCreateCommand": "npm install -g mega-linter-runner && pwsh -command Install-Module -Name Pester -Force -SkipPublisherCheck && pip install --editable ./framework/python/", + "postCreateCommand": "/benchpress/post-create.sh", // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. "remoteUser": "vscode", @@ -85,3 +85,4 @@ } } } + diff --git a/.devcontainer/scripts/post-create.sh b/.devcontainer/scripts/post-create.sh new file mode 100644 index 0000000..c578397 --- /dev/null +++ b/.devcontainer/scripts/post-create.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +# Install Mega Linter +npm install -g mega-linter-runner + +# Install Pester +pwsh -command Install-Module -Name Pester -Force -SkipPublisherCheck + +# Configures benchpress Python module +pip install --editable ./framework/python/ From 3aed3b129b54c15d64c22b27d80abe9f003c65c4 Mon Sep 17 00:00:00 2001 From: jessica-ern <107070686+jessica-ern@users.noreply.github.com> Date: Tue, 8 Nov 2022 10:41:23 -0600 Subject: [PATCH 04/12] Deploy (an ARM Template) to a Resource Group (#13) * Injected ArmClient into the BicepService * added resource group deployment code. Includes temporary testing code in the Framework * wrapped the ArmClient in a service to ease mocking * Add some deployment group tests * add tests for the ArmDeploymentService * remove testing code from framework, but keep the gRPC packages * bicep will not be transpiling the parameter files * add comment * line ending * addressing PR comments: parameter validation and variable renames * rename test * cut down duplicate lines --- .../ArmDeploymentServiceTests.cs | 240 ++++++++++++++++++ .../BenchPress.TestEngine.Tests.csproj | 1 + .../BicepServiceTests.cs | 155 ++++++++++- .../SampleFiles/params.json | 9 + .../storage-account-needs-params.json | 62 +++++ .../SampleFiles/storage-account.json | 63 +++++ .../BenchPress.TestEngine.csproj | 4 + engine/BenchPress.TestEngine/Program.cs | 8 + .../Services/ArmDeploymentService.cs | 59 +++++ .../Services/BicepService.cs | 31 ++- .../Services/IArmDeploymentService.cs | 6 + engine/BenchPress.TestEngine/Usings.cs | 2 + .../BenchPress.TestFramework.csproj | 14 + .../BenchPress.TestFramework/Program.cs | 3 +- 14 files changed, 639 insertions(+), 18 deletions(-) create mode 100644 engine/BenchPress.TestEngine.Tests/ArmDeploymentServiceTests.cs create mode 100644 engine/BenchPress.TestEngine.Tests/SampleFiles/params.json create mode 100644 engine/BenchPress.TestEngine.Tests/SampleFiles/storage-account-needs-params.json create mode 100644 engine/BenchPress.TestEngine.Tests/SampleFiles/storage-account.json create mode 100644 engine/BenchPress.TestEngine/Services/ArmDeploymentService.cs create mode 100644 engine/BenchPress.TestEngine/Services/IArmDeploymentService.cs diff --git a/engine/BenchPress.TestEngine.Tests/ArmDeploymentServiceTests.cs b/engine/BenchPress.TestEngine.Tests/ArmDeploymentServiceTests.cs new file mode 100644 index 0000000..eb95ebb --- /dev/null +++ b/engine/BenchPress.TestEngine.Tests/ArmDeploymentServiceTests.cs @@ -0,0 +1,240 @@ +using Azure; +using Azure.ResourceManager; +using Azure.ResourceManager.Resources; +using Azure.ResourceManager.Resources.Models; + +namespace BenchPress.TestEngine.Tests; + +public class ArmDeploymentServiceTests { + private readonly ArmDeploymentService armDeploymentService; + private readonly Mock armClientMock; + private readonly Mock groupDeploymentsMock; + private readonly Mock subscriptionDeploymentsMock; + private const string validSubId = "a3a01f37-665c-4ee8-9bc3-3adf7ebcec0d"; + private const string validRgName = "test-rg"; + private const string smapleFiles = "../../../SampleFiles"; + private const string standaloneTemplate = $"{smapleFiles}/storage-account.json"; + private const string templateWithParams = $"{smapleFiles}/storage-account-needs-params.json"; + private const string parameters = $"{smapleFiles}/params.json"; + + public ArmDeploymentServiceTests() + { + armClientMock = new Mock(MockBehavior.Strict); + groupDeploymentsMock = new Mock(); + subscriptionDeploymentsMock = new Mock(); + armDeploymentService = new TestArmDeploymentService(groupDeploymentsMock.Object, subscriptionDeploymentsMock.Object, armClientMock.Object); + } + + [Fact] + public async Task DeployArmToResourceGroupAsync_Deploys() + { + var subMock = SetUpSubscriptionMock(validSubId); + var rgMock = SetUpResourceGroupMock(subMock, validRgName); + SetUpDeploymentsMock(groupDeploymentsMock); + await armDeploymentService.DeployArmToResourceGroupAsync(validSubId, validRgName, templateWithParams, parameters); + VerifyDeploymentsMock(groupDeploymentsMock); + } + + [Theory] + [InlineData("main.bicep", "", "a3a01f37-665c-4ee8-9bc3-3adf7ebcec0d")] + [InlineData("", "rg-test", "a3a01f37-665c-4ee8-9bc3-3adf7ebcec0d")] + [InlineData("main.bicep", "rg-test", "")] + public async Task DeployArmToResourceGroupAsync_MissingParameter_ThrowsException(string templatePath, string rgName, string subId) + { + var subMock = SetUpSubscriptionMock(subId); + var rgMock = SetUpResourceGroupMock(subMock, rgName); + SetUpDeploymentsMock(groupDeploymentsMock); + var ex = await Assert.ThrowsAsync( + async () => await armDeploymentService.DeployArmToResourceGroupAsync(subId, rgName, templatePath) + ); + Assert.Equal("One or more parameters were missing or empty", ex.Message); + } + + [Fact] + public async Task DeployArmToResourceGroupAsync_InvalidTemplate_ThrowsException() + { + var subMock = SetUpSubscriptionMock(validSubId); + var rgMock = SetUpResourceGroupMock(subMock, validRgName); + var excepectedMessage = "Deployment template validation failed"; + SetUpDeploymentExceptionMock(groupDeploymentsMock, new RequestFailedException(excepectedMessage)); + var ex = await Assert.ThrowsAsync( + async () => await armDeploymentService.DeployArmToResourceGroupAsync(validSubId, validRgName, templateWithParams) + ); + Assert.Equal(excepectedMessage, ex.Message); + } + + [Fact] + public async Task DeployArmToResourceGroupAsync_SubscriptionNotFound_ThrowsException() + { + var subMock = SetUpSubscriptionMock(validSubId); + var rgMock = SetUpResourceGroupMock(subMock, validRgName); + SetUpDeploymentsMock(groupDeploymentsMock); + var ex = await Assert.ThrowsAsync( + async () => await armDeploymentService.DeployArmToResourceGroupAsync("The Wrong Subscription", validRgName, standaloneTemplate) + ); + Assert.Equal("Subscription Not Found", ex.Message); + } + + [Fact] + public async Task DeployArmToResourceGroupAsync_ResourceGroupNotFound_ThrowsException() + { + var subMock = SetUpSubscriptionMock(validSubId); + var rgMock = SetUpResourceGroupMock(subMock, validRgName); + SetUpDeploymentsMock(groupDeploymentsMock); + var ex = await Assert.ThrowsAsync( + async () => await armDeploymentService.DeployArmToResourceGroupAsync(validSubId, "the-wrong-rg", standaloneTemplate) + ); + Assert.Equal("Resource Group Not Found", ex.Message); + } + + [Fact] + public async Task DeployArmToSubscriptionAsync_Deploys() + { + var subMock = SetUpSubscriptionMock(validSubId); + SetUpDeploymentsMock(subscriptionDeploymentsMock); + await armDeploymentService.DeployArmToSubscriptionAsync(validSubId, templateWithParams, parameters); + VerifyDeploymentsMock(subscriptionDeploymentsMock); + } + + [Theory] + [InlineData("", "a3a01f37-665c-4ee8-9bc3-3adf7ebcec0d")] + [InlineData("main.bicep", "")] + public async Task DeployArmToSubscriptionAsync_MissingParameter_ThrowsException(string templatePath, string subId) + { + var subMock = SetUpSubscriptionMock(subId); + SetUpDeploymentsMock(subscriptionDeploymentsMock); + var ex = await Assert.ThrowsAsync( + async () => await armDeploymentService.DeployArmToSubscriptionAsync(subId, templatePath) + ); + Assert.Equal("One or more parameters were missing or empty", ex.Message); + } + + [Fact] + public async Task DeployArmToSubscriptionAsync_InvalidTemplate_ThrowsException() + { + var subMock = SetUpSubscriptionMock(validSubId); + var excepectedMessage = "Deployment template validation failed"; + SetUpDeploymentExceptionMock(subscriptionDeploymentsMock, new RequestFailedException(excepectedMessage)); + var ex = await Assert.ThrowsAsync( + async () => await armDeploymentService.DeployArmToSubscriptionAsync(validSubId, templateWithParams) + ); + Assert.Equal(excepectedMessage, ex.Message); + } + + [Fact] + public async Task DeployArmToSubscriptionAsync_SubscriptionNotFound_ThrowsException() + { + var subMock = SetUpSubscriptionMock(validSubId); + SetUpDeploymentsMock(subscriptionDeploymentsMock); + var ex = await Assert.ThrowsAsync( + async () => await armDeploymentService.DeployArmToSubscriptionAsync("The Wrong Subscription", standaloneTemplate) + ); + Assert.Equal("Subscription Not Found", ex.Message); + } + + [Fact] + public async Task CreateDeploymentContent_WithoutParameters() + { + var subMock = SetUpSubscriptionMock(validSubId); + var rgMock = SetUpResourceGroupMock(subMock, validRgName); + SetUpDeploymentsMock(groupDeploymentsMock); + await armDeploymentService.DeployArmToResourceGroupAsync(validSubId, validRgName, standaloneTemplate); + VerifyDeploymentsMock(groupDeploymentsMock); + } + + [Fact] + public async Task CreateDeploymentContent_TemplateNotFound_ThrowsException() + { + var subMock = SetUpSubscriptionMock(validSubId); + var rgMock = SetUpResourceGroupMock(subMock, validRgName); + SetUpDeploymentsMock(groupDeploymentsMock); + var ex = await Assert.ThrowsAsync( + async () => await armDeploymentService.DeployArmToResourceGroupAsync(validSubId, validRgName, "invalid-file.json") + ); + } + + [Fact] + public async Task CreateDeploymentContent_ParametersNotFound_ThrowsException() + { + var subMock = SetUpSubscriptionMock(validSubId); + var rgMock = SetUpResourceGroupMock(subMock, validRgName); + SetUpDeploymentsMock(groupDeploymentsMock); + var ex = await Assert.ThrowsAsync( + async () => await armDeploymentService.DeployArmToResourceGroupAsync(validSubId, validRgName, standaloneTemplate, "invalid-file.json") + ); + } + + private Mock SetUpSubscriptionMock(string nameOrId) + { + var subMock = new Mock(); + var collectionMock = new Mock(); + var response = Azure.Response.FromValue(subMock.Object, Mock.Of()); + collectionMock.Setup(x => x.GetAsync(It.IsAny(), default)).ThrowsAsync(new Azure.RequestFailedException("Subscription Not Found")); + collectionMock.Setup(x => x.GetAsync(nameOrId, default)).ReturnsAsync(response); + armClientMock.Setup(x => x.GetSubscriptions()).Returns(collectionMock.Object); + return subMock; + } + + private Mock SetUpResourceGroupMock(Mock subMock, string rgName) + { + var rgMock = new Mock(); + var collectionMock = new Mock(); + var response = Azure.Response.FromValue(rgMock.Object, Mock.Of()); + collectionMock.Setup(x => x.GetAsync(It.IsAny(), default)).ThrowsAsync(new Azure.RequestFailedException("Resource Group Not Found")); + collectionMock.Setup(x => x.GetAsync(rgName, default)).ReturnsAsync(response); + subMock.Setup(x => x.GetResourceGroups()).Returns(collectionMock.Object); + return rgMock; + } + + private void SetUpDeploymentsMock(Mock deploymentsMock) + { + deploymentsMock.Setup(x => x.CreateOrUpdateAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + default)) + .ReturnsAsync(Mock.Of>()); + } + + private void SetUpDeploymentExceptionMock(Mock deploymentsMock, T exception) where T : Exception + { + deploymentsMock.Setup(x => x.CreateOrUpdateAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + default)) + .ThrowsAsync(exception); + } + + private void VerifyDeploymentsMock(Mock deploymentsMock) + { + deploymentsMock.Verify(x => x.CreateOrUpdateAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + default), + Times.Once); + } + + // This test class mocks the extension methods for GetArmDeployments() + private class TestArmDeploymentService : ArmDeploymentService { + private readonly ArmDeploymentCollection rgDeploymentCollection; + private readonly ArmDeploymentCollection subDeploymentCollection; + + public TestArmDeploymentService(ArmDeploymentCollection rgDeploymentCollection, ArmDeploymentCollection subDeploymentCollection, ArmClient client) : base(client) + { + this.rgDeploymentCollection = rgDeploymentCollection; + this.subDeploymentCollection = subDeploymentCollection; + } + + protected override Task> CreateGroupDeployment(ResourceGroupResource rg, WaitUntil waitUntil, string deploymentName, ArmDeploymentContent deploymentContent) + { + return rgDeploymentCollection.CreateOrUpdateAsync(waitUntil, deploymentName, deploymentContent); + } + + protected override Task> CreateSubscriptionDeployment(SubscriptionResource sub, WaitUntil waitUntil, string deploymentName, ArmDeploymentContent deploymentContent) + { + return subDeploymentCollection.CreateOrUpdateAsync(waitUntil, deploymentName, deploymentContent); + } + } +} \ No newline at end of file diff --git a/engine/BenchPress.TestEngine.Tests/BenchPress.TestEngine.Tests.csproj b/engine/BenchPress.TestEngine.Tests/BenchPress.TestEngine.Tests.csproj index 26d1072..268c90f 100644 --- a/engine/BenchPress.TestEngine.Tests/BenchPress.TestEngine.Tests.csproj +++ b/engine/BenchPress.TestEngine.Tests/BenchPress.TestEngine.Tests.csproj @@ -9,6 +9,7 @@ + diff --git a/engine/BenchPress.TestEngine.Tests/BicepServiceTests.cs b/engine/BenchPress.TestEngine.Tests/BicepServiceTests.cs index 12ffcde..1cb89e1 100644 --- a/engine/BenchPress.TestEngine.Tests/BicepServiceTests.cs +++ b/engine/BenchPress.TestEngine.Tests/BicepServiceTests.cs @@ -1,30 +1,80 @@ -namespace BenchPress.TestEngine.Tests; +using Azure.ResourceManager; +using Azure.ResourceManager.Resources; +namespace BenchPress.TestEngine.Tests; public class BicepServiceTests { private readonly BicepService bicepService; private readonly ServerCallContext context; + private readonly Mock armDeploymentMock; public BicepServiceTests() { - var logger = new Mock>().Object; - bicepService = new BicepService(logger); + var logger = Mock.Of>(); + armDeploymentMock = new Mock(MockBehavior.Strict); + bicepService = new BicepService(logger, armDeploymentMock.Object); context = new MockServerCallContext(); } - [Fact(Skip = "Not Implemented")] - public async Task DeploymentGroupCreate_DeploysResourceGroup() + [Fact] + public async Task DeploymentGroupCreate_DeploysResourceGroup_WithTranspiledFiles() { - var request = new DeploymentGroupRequest - { - BicepFilePath = "main.bicep", - ParameterFilePath = "parameters.json", - ResourceGroupName = "test-rg", - SubscriptionNameOrId = new Guid().ToString() - }; - var result = await bicepService.DeploymentGroupCreate(request, context); + // TODO: set up successful transpilation + var templatePath = validGroupRequest.BicepFilePath; + SetUpSuccessfulGroupDeployment(validGroupRequest, templatePath); + var result = await bicepService.DeploymentGroupCreate(validGroupRequest, context); Assert.True(result.Success); + VerifyGroupDeployment(validGroupRequest, templatePath); + } + + [Theory] + [InlineData("main.bicep", "", "a3a01f37-665c-4ee8-9bc3-3adf7ebcec0d")] + [InlineData("", "rg-test", "a3a01f37-665c-4ee8-9bc3-3adf7ebcec0d")] + [InlineData("main.bicep", "rg-test", "")] + public async Task DeploymentGroupCreate_FailsOnMissingParameters(string bicepFilePath, string resourceGroupName, string subscriptionNameOrId) + { + var request = SetUpGroupRequest(bicepFilePath, resourceGroupName, subscriptionNameOrId); + // TODO: set up successful transpilation + var templatePath = request.BicepFilePath; + SetUpSuccessfulGroupDeployment(request, templatePath); + var result = await bicepService.DeploymentGroupCreate(request, context); + Assert.False(result.Success); + // TODO: verify transpile wasn't called + VerifyNoDeployments(); + } + + [Fact(Skip = "Not Fully Implemented")] + public async Task DeploymentGroupCreate_ReturnsFailureOnTranspileException() + { + var expectedMessage = "the bicep file was malformed"; + // TODO: set up exception throwing transpilation + SetUpSuccessfulGroupDeployment(validGroupRequest, "template.json"); + var result = await bicepService.DeploymentGroupCreate(validGroupRequest, context); + Assert.False(result.Success); + Assert.Equal(expectedMessage, result.ErrorMessage); + } + + [Fact] + public async Task DeploymentGroupCreate_ReturnsFailureOnFailedDeployment() + { + // TODO: set up successful transpilation + var expectedReason = "Failure occured during deployment"; + SetUpFailedGroupDeployment(expectedReason); + var result = await bicepService.DeploymentGroupCreate(validGroupRequest, context); + Assert.False(result.Success); + Assert.Equal(expectedReason, result.ErrorMessage); + } + + [Fact] + public async Task DeploymentGroupCreate_ReturnsFailureOnDeploymentException() + { + // TODO: set up successful transpilation + var expectedMessage = "the template was malformed"; + SetUpExceptionThrowingGroupDeployment(new Exception(expectedMessage)); + var result = await bicepService.DeploymentGroupCreate(validGroupRequest, context); + Assert.False(result.Success); + Assert.Equal(expectedMessage, result.ErrorMessage); } [Fact(Skip = "Not Implemented")] @@ -33,9 +83,86 @@ public class BicepServiceTests var request = new DeleteGroupRequest { ResourceGroupName = "test-rg", - SubscriptionNameOrId = new Guid().ToString() + SubscriptionNameOrId = Guid.NewGuid().ToString() }; var result = await bicepService.DeleteGroup(request, context); Assert.True(result.Success); } + + private readonly DeploymentGroupRequest validGroupRequest = new DeploymentGroupRequest + { + BicepFilePath = "main.bicep", + ResourceGroupName = "test-rg", + SubscriptionNameOrId = Guid.NewGuid().ToString() + }; + + private DeploymentGroupRequest SetUpGroupRequest(string bicepFilePath, string resourceGroupName, string subscriptionNameOrId) { + return new DeploymentGroupRequest + { + BicepFilePath = bicepFilePath, + ResourceGroupName = resourceGroupName, + SubscriptionNameOrId = subscriptionNameOrId + }; + } + + private ArmOperation SetupDeploymentOperation(bool success, string reason) { + var responseMock = new Mock(); + responseMock.Setup(x => x.IsError).Returns(!success); + responseMock.Setup(x => x.ReasonPhrase).Returns(reason); + var operationMock = new Mock>(); + operationMock.Setup(x => x.WaitForCompletionResponse(default)).Returns(responseMock.Object); + return operationMock.Object; + } + + private void SetUpSuccessfulGroupDeployment(DeploymentGroupRequest request, string templatePath) { + var operation = SetupDeploymentOperation(true, "OK"); + armDeploymentMock.Setup(x => x.DeployArmToResourceGroupAsync( + request.SubscriptionNameOrId, + request.ResourceGroupName, + templatePath, + request.ParameterFilePath, + It.IsAny())) + .ReturnsAsync(operation); + } + + private void SetUpFailedGroupDeployment(string reason) { + var operation = SetupDeploymentOperation(false, reason); + armDeploymentMock.Setup(x => x.DeployArmToResourceGroupAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(operation); + } + + private void SetUpExceptionThrowingGroupDeployment(Exception ex) { + armDeploymentMock.Setup(x => x.DeployArmToResourceGroupAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ThrowsAsync(ex); + } + + private void VerifyGroupDeployment(DeploymentGroupRequest request, string templatePath) { + armDeploymentMock.Verify(x => x.DeployArmToResourceGroupAsync( + request.SubscriptionNameOrId, + request.ResourceGroupName, + templatePath, + request.ParameterFilePath, + It.IsAny()), + Times.Once); + } + + private void VerifyNoDeployments() { + armDeploymentMock.Verify(x => x.DeployArmToResourceGroupAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); + } } \ No newline at end of file diff --git a/engine/BenchPress.TestEngine.Tests/SampleFiles/params.json b/engine/BenchPress.TestEngine.Tests/SampleFiles/params.json new file mode 100644 index 0000000..c3cef68 --- /dev/null +++ b/engine/BenchPress.TestEngine.Tests/SampleFiles/params.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "projectName": { + "value": "jern" + } + } + } \ No newline at end of file diff --git a/engine/BenchPress.TestEngine.Tests/SampleFiles/storage-account-needs-params.json b/engine/BenchPress.TestEngine.Tests/SampleFiles/storage-account-needs-params.json new file mode 100644 index 0000000..e571d01 --- /dev/null +++ b/engine/BenchPress.TestEngine.Tests/SampleFiles/storage-account-needs-params.json @@ -0,0 +1,62 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "projectName": { + "type": "string", + "minLength": 3, + "maxLength": 11, + "metadata": { + "description": "Specify a project name that is used to generate resource names." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Specify a location for the resources." + } + }, + "storageSKU": { + "type": "string", + "defaultValue": "Standard_LRS", + "allowedValues": [ + "Standard_LRS", + "Standard_GRS", + "Standard_RAGRS", + "Standard_ZRS", + "Premium_LRS", + "Premium_ZRS", + "Standard_GZRS", + "Standard_RAGZRS" + ], + "metadata": { + "description": "Specify the storage account type." + } + } + }, + "variables": { + "storageAccountName": "[concat(parameters('projectName'), uniqueString(resourceGroup().id))]" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2021-04-01", + "name": "[variables('storageAccountName')]", + "location": "[parameters('location')]", + "sku": { + "name": "[parameters('storageSKU')]" + }, + "kind": "StorageV2", + "properties": { + "supportsHttpsTrafficOnly": true + } + } + ], + "outputs": { + "storageEndpoint": { + "type": "object", + "value": "[reference(variables('storageAccountName')).primaryEndpoints]" + } + } + } \ No newline at end of file diff --git a/engine/BenchPress.TestEngine.Tests/SampleFiles/storage-account.json b/engine/BenchPress.TestEngine.Tests/SampleFiles/storage-account.json new file mode 100644 index 0000000..d596c90 --- /dev/null +++ b/engine/BenchPress.TestEngine.Tests/SampleFiles/storage-account.json @@ -0,0 +1,63 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "projectName": { + "type": "string", + "defaultValue": "jern", + "minLength": 3, + "maxLength": 11, + "metadata": { + "description": "Specify a project name that is used to generate resource names." + } + }, + "location": { + "type": "string", + "defaultValue": "[resourceGroup().location]", + "metadata": { + "description": "Specify a location for the resources." + } + }, + "storageSKU": { + "type": "string", + "defaultValue": "Standard_LRS", + "allowedValues": [ + "Standard_LRS", + "Standard_GRS", + "Standard_RAGRS", + "Standard_ZRS", + "Premium_LRS", + "Premium_ZRS", + "Standard_GZRS", + "Standard_RAGZRS" + ], + "metadata": { + "description": "Specify the storage account type." + } + } + }, + "variables": { + "storageAccountName": "[concat(parameters('projectName'), uniqueString(resourceGroup().id))]" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2021-04-01", + "name": "[variables('storageAccountName')]", + "location": "[parameters('location')]", + "sku": { + "name": "[parameters('storageSKU')]" + }, + "kind": "StorageV2", + "properties": { + "supportsHttpsTrafficOnly": true + } + } + ], + "outputs": { + "storageEndpoint": { + "type": "object", + "value": "[reference(variables('storageAccountName')).primaryEndpoints]" + } + } + } \ No newline at end of file diff --git a/engine/BenchPress.TestEngine/BenchPress.TestEngine.csproj b/engine/BenchPress.TestEngine/BenchPress.TestEngine.csproj index 38cb6e2..3fe4e03 100644 --- a/engine/BenchPress.TestEngine/BenchPress.TestEngine.csproj +++ b/engine/BenchPress.TestEngine/BenchPress.TestEngine.csproj @@ -12,7 +12,11 @@ + + + + diff --git a/engine/BenchPress.TestEngine/Program.cs b/engine/BenchPress.TestEngine/Program.cs index c90e594..3fd710c 100644 --- a/engine/BenchPress.TestEngine/Program.cs +++ b/engine/BenchPress.TestEngine/Program.cs @@ -1,5 +1,7 @@ using BenchPress.TestEngine.Services; using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.Azure; +using Azure.Identity; var builder = WebApplication.CreateBuilder(args); @@ -14,6 +16,12 @@ builder.WebHost.ConfigureKestrel(options => // Add services to the container. builder.Services.AddGrpc(); +builder.Services.AddAzureClients(builder => { + builder.AddClient(options => { + return new ArmClient(new DefaultAzureCredential()); + }); +}); +builder.Services.AddSingleton(); var app = builder.Build(); diff --git a/engine/BenchPress.TestEngine/Services/ArmDeploymentService.cs b/engine/BenchPress.TestEngine/Services/ArmDeploymentService.cs new file mode 100644 index 0000000..abca0c2 --- /dev/null +++ b/engine/BenchPress.TestEngine/Services/ArmDeploymentService.cs @@ -0,0 +1,59 @@ +using Azure; +using Azure.ResourceManager.Resources.Models; + +namespace BenchPress.TestEngine.Services; + +public class ArmDeploymentService : IArmDeploymentService { + private readonly ArmClient client; + private string NewDeploymentName { get { return $"benchpress-{Guid.NewGuid().ToString()}"; } } + + public ArmDeploymentService(ArmClient client) { + this.client = client; + } + + public async Task> DeployArmToResourceGroupAsync(string subscriptionNameOrId, string resourceGroupName, string armTemplatePath, string? parametersPath = null, WaitUntil waitUntil = WaitUntil.Completed) + { + ValidateParameters(subscriptionNameOrId, resourceGroupName, armTemplatePath); + SubscriptionResource sub = await client.GetSubscriptions().GetAsync(subscriptionNameOrId); + ResourceGroupResource rg = await sub.GetResourceGroups().GetAsync(resourceGroupName); + var deploymentContent = await CreateDeploymentContent(armTemplatePath, parametersPath); + return await CreateGroupDeployment(rg, waitUntil, NewDeploymentName, deploymentContent); + } + + public async Task> DeployArmToSubscriptionAsync(string subscriptionNameOrId, string armTemplatePath, string? parametersPath = null, WaitUntil waitUtil = WaitUntil.Completed) + { + ValidateParameters(subscriptionNameOrId, armTemplatePath); + SubscriptionResource sub = await client.GetSubscriptions().GetAsync(subscriptionNameOrId); + var deploymentContent = await CreateDeploymentContent(armTemplatePath, parametersPath); + return await CreateSubscriptionDeployment(sub, waitUtil, NewDeploymentName, deploymentContent); + } + + private void ValidateParameters(params string[] parameters) { + if(parameters.Any(s => string.IsNullOrWhiteSpace(s))) { + throw new ArgumentException("One or more parameters were missing or empty"); + } + } + + private async Task CreateDeploymentContent(string armTemplatePath, string? parametersPath) { + var templateContent = (await File.ReadAllTextAsync(armTemplatePath)).TrimEnd(); + var properties = new ArmDeploymentProperties(ArmDeploymentMode.Incremental) { + Template = BinaryData.FromString(templateContent) + }; + + if (!string.IsNullOrWhiteSpace(parametersPath)) { + var paramteresContent = (await File.ReadAllTextAsync(parametersPath)).TrimEnd(); + properties.Parameters = BinaryData.FromString(parametersPath); + } + + return new ArmDeploymentContent(properties); + } + + // These extension methods are wrapped to allow mocking in our tests + protected virtual async Task> CreateGroupDeployment(ResourceGroupResource rg, Azure.WaitUntil waitUntil, string deploymentName, ArmDeploymentContent deploymentContent) { + return await rg.GetArmDeployments().CreateOrUpdateAsync(waitUntil, deploymentName, deploymentContent); + } + + protected virtual async Task> CreateSubscriptionDeployment(SubscriptionResource sub, Azure.WaitUntil waitUntil, string deploymentName, ArmDeploymentContent deploymentContent) { + return await sub.GetArmDeployments().CreateOrUpdateAsync(waitUntil, deploymentName, deploymentContent); + } +} \ No newline at end of file diff --git a/engine/BenchPress.TestEngine/Services/BicepService.cs b/engine/BenchPress.TestEngine/Services/BicepService.cs index ed6f5a6..3849d63 100644 --- a/engine/BenchPress.TestEngine/Services/BicepService.cs +++ b/engine/BenchPress.TestEngine/Services/BicepService.cs @@ -3,15 +3,42 @@ namespace BenchPress.TestEngine.Services; public class BicepService : Bicep.BicepBase { private readonly ILogger logger; + private readonly IArmDeploymentService armDeploymentService; - public BicepService(ILogger logger) + public BicepService(ILogger logger, IArmDeploymentService armDeploymentService) { this.logger = logger; + this.armDeploymentService = armDeploymentService; } public override async Task DeploymentGroupCreate(DeploymentGroupRequest request, ServerCallContext context) { - throw new NotImplementedException(); + if (string.IsNullOrWhiteSpace(request.BicepFilePath) + || string.IsNullOrWhiteSpace(request.ResourceGroupName) + || string.IsNullOrWhiteSpace(request.SubscriptionNameOrId)) + { + return new DeploymentResult { + Success = false, + ErrorMessage = $"One or more of the following required parameters was missing: {nameof(request.BicepFilePath)}, {nameof(request.ResourceGroupName)}, and {nameof(request.SubscriptionNameOrId)}" + }; + } + + try { + // TODO: pass in transpiled arm template instead + var deployment = await armDeploymentService.DeployArmToResourceGroupAsync(request.SubscriptionNameOrId, request.ResourceGroupName, request.BicepFilePath, request.ParameterFilePath); + var response = deployment.WaitForCompletionResponse(); + + return new DeploymentResult { + Success = !response.IsError, + ErrorMessage = response.ReasonPhrase + }; + + } catch (Exception ex) { + return new DeploymentResult { + Success = false, + ErrorMessage = ex.Message + }; + } } public override async Task DeleteGroup(DeleteGroupRequest request, ServerCallContext context) diff --git a/engine/BenchPress.TestEngine/Services/IArmDeploymentService.cs b/engine/BenchPress.TestEngine/Services/IArmDeploymentService.cs new file mode 100644 index 0000000..505011d --- /dev/null +++ b/engine/BenchPress.TestEngine/Services/IArmDeploymentService.cs @@ -0,0 +1,6 @@ +namespace BenchPress.TestEngine.Services; + +public interface IArmDeploymentService { + Task> DeployArmToResourceGroupAsync(string subscriptionNameOrId, string resourceGroupName, string armTemplatePath, string? parametersPath = null, Azure.WaitUntil waitUtil = Azure.WaitUntil.Completed); + Task> DeployArmToSubscriptionAsync(string subscriptionNameOrId, string armTemplatePath, string? parametersPath = null, Azure.WaitUntil waitUtil = Azure.WaitUntil.Completed); +} \ No newline at end of file diff --git a/engine/BenchPress.TestEngine/Usings.cs b/engine/BenchPress.TestEngine/Usings.cs index eeca8db..4114b60 100644 --- a/engine/BenchPress.TestEngine/Usings.cs +++ b/engine/BenchPress.TestEngine/Usings.cs @@ -1,2 +1,4 @@ global using Grpc.Core; +global using Azure.ResourceManager; +global using Azure.ResourceManager.Resources; global using BenchPress.TestEngine; diff --git a/framework/dotnet/BenchPress.TestFramework/BenchPress.TestFramework.csproj b/framework/dotnet/BenchPress.TestFramework/BenchPress.TestFramework.csproj index c81a5b4..d7c2235 100644 --- a/framework/dotnet/BenchPress.TestFramework/BenchPress.TestFramework.csproj +++ b/framework/dotnet/BenchPress.TestFramework/BenchPress.TestFramework.csproj @@ -11,4 +11,18 @@ + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + diff --git a/framework/dotnet/BenchPress.TestFramework/Program.cs b/framework/dotnet/BenchPress.TestFramework/Program.cs index 3751555..37fa180 100644 --- a/framework/dotnet/BenchPress.TestFramework/Program.cs +++ b/framework/dotnet/BenchPress.TestFramework/Program.cs @@ -1,2 +1 @@ -// See https://aka.ms/new-console-template for more information -Console.WriteLine("Hello, World!"); +Console.WriteLine("Hello World!"); From 49eeb64be01f9f2ba839c7740cd0040f6e66c806 Mon Sep 17 00:00:00 2001 From: jessica-ern <107070686+jessica-ern@users.noreply.github.com> Date: Tue, 8 Nov 2022 10:47:11 -0600 Subject: [PATCH 05/12] added gRPC requirements for python (#19) * added gRPC requirements for python * move grpc install into the setup.cfg --- .devcontainer/scripts/post-create.sh | 3 +++ docs/getting_started.md | 12 ++++++++++-- framework/python/setup.cfg | 1 + 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.devcontainer/scripts/post-create.sh b/.devcontainer/scripts/post-create.sh index c578397..c1bba3b 100644 --- a/.devcontainer/scripts/post-create.sh +++ b/.devcontainer/scripts/post-create.sh @@ -6,5 +6,8 @@ npm install -g mega-linter-runner # Install Pester pwsh -command Install-Module -Name Pester -Force -SkipPublisherCheck +# Installs the gRPC Tools +python -m pip install grpcio-tools + # Configures benchpress Python module pip install --editable ./framework/python/ diff --git a/docs/getting_started.md b/docs/getting_started.md index 4892d41..609dbd1 100644 --- a/docs/getting_started.md +++ b/docs/getting_started.md @@ -11,7 +11,6 @@ Then launch the environment by opening the command palette Shift+ pip install --editable ./framework/python/ +```bash +pip install --editable ./framework/python/ +``` + diff --git a/framework/python/setup.cfg b/framework/python/setup.cfg index fa03da7..8c6e6e6 100644 --- a/framework/python/setup.cfg +++ b/framework/python/setup.cfg @@ -13,6 +13,7 @@ zip_safe = True include_package_data = True install_requires = protobuf == 4.21.9 + grpcio pytest [options.packages.find] From 55ad2e1ead0ee0c66063fb20a0737ecc4930e2a6 Mon Sep 17 00:00:00 2001 From: Dilmurod Makhamadaliev <104784252+DilmurodMak@users.noreply.github.com> Date: Wed, 9 Nov 2022 15:03:54 -0500 Subject: [PATCH 06/12] Bicep Transpile Service - Takes Bicep File, Generates Arm Template Using Bicep Submodule (#20) * transpile bicep to arm template feature * bicep submodule restructured * rename the bicepSubmodule to BicepExecute * change method signature and make async * Renaming Bicep Service - main have existing BicepService.cs * additional tests added * corrections for feedbacks * actual manual deployment is tested * changed from method wrapping Bicep.Cli into direct calling Bicep.Cli.Programs.Main * fixing linting errors * additional mock test to throw exception and dotnet workflow update * linting fix * liniting fix * linting fix * linting fix * linting fix * linting fix * linting fix * linting fix * linting fix * linting fix * linting fix * linting fix * linting fix * come on now * come on now * come on now * adding submodule checkout * turn off dotnet build * dotnet build * removing duplicate package refrence * change dotnet setup version to 3 * fetch depth 0 * test null exception change * lint fix * mock bicep submodule Co-authored-by: Jessica Ern --- .github/workflows/pr_dotnet.yaml | 8 +- .gitmodules | 2 +- .globalconfig | 428 +++++++++--------- BenchPress.sln | 6 + BenchPress/BenchPress.sln | 74 +-- BenchPress/Generators/.gitignore | 2 +- BenchPress/Helpers/Azure/Bicep.psm1 | 8 +- BenchPress/bicep | 1 + .../.powershell-psscriptanalyzer.psd1 | 14 +- .../ArmDeploymentServiceTests.cs | 2 +- .../BicepTranspileServiceTests.cs | 60 +++ ...viceTests.cs => DeploymentServiceTests.cs} | 26 +- .../Helpers/MockServerCallContext.cs | 2 +- .../ResourceGroupServiceTests.cs | 2 +- .../SampleFiles/resourceGroup.bicep | 17 + .../SampleFiles/storageAccount.bicep | 14 + engine/BenchPress.TestEngine.Tests/Usings.cs | 2 +- .../BenchPress.TestEngine.csproj | 6 +- engine/BenchPress.TestEngine/Program.cs | 68 +-- .../Services/ArmDeploymentService.cs | 2 +- .../Services/BicepExecute.cs | 11 + .../Services/BicepTranspileService.cs | 54 +++ .../{BicepService.cs => DeploymentService.cs} | 29 +- .../Services/IArmDeploymentService.cs | 2 +- .../Services/IBicepExecute.cs | 6 + .../Services/IBicepTranspileService.cs | 6 + .../BenchPress.TestFramework.csproj | 2 +- protos/{bicep.proto => deployment.proto} | 2 +- 28 files changed, 528 insertions(+), 328 deletions(-) create mode 160000 BenchPress/bicep create mode 100644 engine/BenchPress.TestEngine.Tests/BicepTranspileServiceTests.cs rename engine/BenchPress.TestEngine.Tests/{BicepServiceTests.cs => DeploymentServiceTests.cs} (87%) create mode 100644 engine/BenchPress.TestEngine.Tests/SampleFiles/resourceGroup.bicep create mode 100644 engine/BenchPress.TestEngine.Tests/SampleFiles/storageAccount.bicep create mode 100644 engine/BenchPress.TestEngine/Services/BicepExecute.cs create mode 100644 engine/BenchPress.TestEngine/Services/BicepTranspileService.cs rename engine/BenchPress.TestEngine/Services/{BicepService.cs => DeploymentService.cs} (76%) create mode 100644 engine/BenchPress.TestEngine/Services/IBicepExecute.cs create mode 100644 engine/BenchPress.TestEngine/Services/IBicepTranspileService.cs rename protos/{bicep.proto => deployment.proto} (97%) diff --git a/.github/workflows/pr_dotnet.yaml b/.github/workflows/pr_dotnet.yaml index 1c0561b..5692352 100755 --- a/.github/workflows/pr_dotnet.yaml +++ b/.github/workflows/pr_dotnet.yaml @@ -23,10 +23,12 @@ jobs: os: [ ubuntu-latest ] steps: - - name: Checkout code + - name: Checkout repository and submodules uses: actions/checkout@v3 with: token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} + fetch-depth: 0 + submodules: recursive - name: MegaLinter dotnet flavor uses: oxsecurity/megalinter/flavors/dotnet@v6.12.0 @@ -36,12 +38,12 @@ jobs: PRINT_ALL_FILES: true DISABLE: SPELL,COPYPASTE,YAML DISABLE_LINTERS: REPOSITORY_CHECKOV,REPOSITORY_TRIVY - FILTER_REGEX_EXCLUDE: '(BenchPress/|examples/|/docs|\.devcontainer|\.editorconfig|\.gitmodules|\.sln|\.md|LICENSE|/framework/python|samples/python)' FILTER_REGEX_INCLUDE: '(framework/dotnet|samples/dotnet|engine/)' + FILTER_REGEX_EXCLUDE: '(examples/|/docs|\.devcontainer|\.editorconfig|\.gitmodules|\.sln|\.md|LICENSE|/framework/python|samples/python)' REPORT_OUTPUT_FOLDER: ${GITHUB_WORKSPACE}/megalinter-reports - name: Setup dotnet - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: dotnet-version: ${{ env.DOTNET_VERSION }} diff --git a/.gitmodules b/.gitmodules index 6842586..e0d8e52 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "BenchPress/bicep"] path = BenchPress/bicep - url = https://github.com/azure/bicep + url = https://github.com/Azure/bicep.git diff --git a/.globalconfig b/.globalconfig index c2314cf..375df44 100644 --- a/.globalconfig +++ b/.globalconfig @@ -1,221 +1,221 @@ -is_global = true +# is_global = true -file_header_template = Copyright (c) Microsoft Corporation.\nLicensed under the MIT License. -csharp_using_directive_placement = outside_namespace -csharp_prefer_braces = true +# file_header_template = Copyright (c) Microsoft Corporation.\nLicensed under the MIT License. +# csharp_using_directive_placement = outside_namespace +# csharp_prefer_braces = true -# Require file header -dotnet_diagnostic.IDE0073.severity = warning -# 'using' directive placement -dotnet_diagnostic.IDE0065.severity = warning -# Add braces -dotnet_diagnostic.IDE0011.severity = warning +# # Require file header +# dotnet_diagnostic.IDE0073.severity = warning +# # 'using' directive placement +# dotnet_diagnostic.IDE0065.severity = warning +# # Add braces +# dotnet_diagnostic.IDE0011.severity = warning -# Remove unnecessary import -dotnet_diagnostic.IDE0005.severity = none +# # Remove unnecessary import +# dotnet_diagnostic.IDE0005.severity = none -# Private member is unused -dotnet_diagnostic.IDE0051.severity = warning +# # Private member is unused +# dotnet_diagnostic.IDE0051.severity = warning -# Unused local variables -dotnet_diagnostic.CA1804.severity = warning -# Private methods that are not called from any other code -dotnet_diagnostic.CA1811.severity = warning -# Avoid unused private fields -dotnet_diagnostic.CA1823.severity = warning -# Use string.Contains(char) instead of string.Contains(string) with single characters -dotnet_diagnostic.CA1847.severity = warning -# Review SQL queries for security vulnerabilities -dotnet_diagnostic.CA2100.severity = warning -# Review visible event handlers -dotnet_diagnostic.CA2109.severity = warning -# Seal methods that satisfy private interfaces -dotnet_diagnostic.CA2119.severity = warning -# Do Not Catch Corrupted State Exceptions -dotnet_diagnostic.CA2153.severity = warning -# Do not use insecure deserializer BinaryFormatter -dotnet_diagnostic.CA2300.severity = warning -# Do not call BinaryFormatter.Deserialize without first setting BinaryFormatter.Binder -dotnet_diagnostic.CA2301.severity = warning -# Ensure BinaryFormatter.Binder is set before calling BinaryFormatter.Deserialize -dotnet_diagnostic.CA2302.severity = warning -# Do not use insecure deserializer LosFormatter -dotnet_diagnostic.CA2305.severity = warning -# Do not use insecure deserializer NetDataContractSerializer -dotnet_diagnostic.CA2310.severity = warning -# Do not deserialize without first setting NetDataContractSerializer.Binder -dotnet_diagnostic.CA2311.severity = warning -# Ensure NetDataContractSerializer.Binder is set before deserializing -dotnet_diagnostic.CA2312.severity = warning -# Do not use insecure deserializer ObjectStateFormatter -dotnet_diagnostic.CA2315.severity = warning -# Do not deserialize with JavaScriptSerializer using a SimpleTypeResolver -dotnet_diagnostic.CA2321.severity = warning -# Ensure JavaScriptSerializer is not initialized with SimpleTypeResolver before deserializing -dotnet_diagnostic.CA2322.severity = warning -# Do not use TypeNameHandling values other than None -dotnet_diagnostic.CA2326.severity = warning -# Do not use insecure JsonSerializerSettings -dotnet_diagnostic.CA2327.severity = warning -# Ensure that JsonSerializerSettings are secure -dotnet_diagnostic.CA2328.severity = warning -# Do not deserialize with JsonSerializer using an insecure configuration -dotnet_diagnostic.CA2329.severity = warning -# Ensure that JsonSerializer has a secure configuration when deserializing -dotnet_diagnostic.CA2330.severity = warning -# Do not use DataTable.ReadXml() with untrusted data -dotnet_diagnostic.CA2350.severity = warning -# Do not use DataSet.ReadXml() with untrusted data -dotnet_diagnostic.CA2351.severity = warning -# Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks -dotnet_diagnostic.CA2352.severity = warning -# Unsafe DataSet or DataTable in serializable type -dotnet_diagnostic.CA2353.severity = warning -# Unsafe DataSet or DataTable in deserialized object graph can be vulnerable to remote code execution attacks -dotnet_diagnostic.CA2354.severity = warning -# Unsafe DataSet or DataTable type found in deserializable object graph -dotnet_diagnostic.CA2355.severity = warning -# Unsafe DataSet or DataTable type in web deserializable object graph -dotnet_diagnostic.CA2356.severity = warning -# Ensure autogenerated class containing DataSet.ReadXml() is not used with untrusted data -dotnet_diagnostic.CA2361.severity = warning -# Unsafe DataSet or DataTable in autogenerated serializable type can be vulnerable to remote code execution attacks -dotnet_diagnostic.CA2362.severity = warning -# Review code for SQL injection vulnerabilities -dotnet_diagnostic.CA3001.severity = warning -# Review code for XSS vulnerabilities -dotnet_diagnostic.CA3002.severity = warning -# Review code for file path injection vulnerabilities -dotnet_diagnostic.CA3003.severity = warning -# Review code for information disclosure vulnerabilities -dotnet_diagnostic.CA3004.severity = warning -# Review code for LDAP injection vulnerabilities -dotnet_diagnostic.CA3005.severity = warning -# Review code for process command injection vulnerabilities -dotnet_diagnostic.CA3006.severity = warning -# Review code for open redirect vulnerabilities -dotnet_diagnostic.CA3007.severity = warning -# Review code for XPath injection vulnerabilities -dotnet_diagnostic.CA3008.severity = warning -# Review code for XML injection vulnerabilities -dotnet_diagnostic.CA3009.severity = warning -# Review code for XAML injection vulnerabilities -dotnet_diagnostic.CA3010.severity = warning -# Review code for DLL injection vulnerabilities -dotnet_diagnostic.CA3011.severity = warning -# Review code for regex injection vulnerabilities -dotnet_diagnostic.CA3012.severity = warning -# Do Not Add Schema By URL -dotnet_diagnostic.CA3061.severity = warning -# Insecure DTD processing in XML -dotnet_diagnostic.CA3075.severity = warning -# Insecure XSLT script processing. -dotnet_diagnostic.CA3076.severity = warning -# Insecure Processing in API Design, XmlDocument and XmlTextReader -dotnet_diagnostic.CA3077.severity = warning -# Mark Verb Handlers With Validate Antiforgery Token -dotnet_diagnostic.CA3147.severity = warning -# Do Not Use Weak Cryptographic Algorithms -dotnet_diagnostic.CA5350.severity = warning -# Do Not Use Broken Cryptographic Algorithms -dotnet_diagnostic.CA5351.severity = warning -# Review cipher mode usage with cryptography experts -dotnet_diagnostic.CA5358.severity = warning -# Do Not Disable Certificate Validation -dotnet_diagnostic.CA5359.severity = warning -# Do Not Call Dangerous Methods In Deserialization -dotnet_diagnostic.CA5360.severity = warning -# Do Not Disable SChannel Use of Strong Crypto -dotnet_diagnostic.CA5361.severity = warning -# Potential reference cycle in deserialized object graph -dotnet_diagnostic.CA5362.severity = warning -# Do Not Disable Request Validation -dotnet_diagnostic.CA5363.severity = warning -# Do Not Use Deprecated Security Protocols -dotnet_diagnostic.CA5364.severity = warning -# Do Not Disable HTTP Header Checking -dotnet_diagnostic.CA5365.severity = warning -# Use XmlReader For DataSet Read Xml -dotnet_diagnostic.CA5366.severity = warning -# Do Not Serialize Types With Pointer Fields -dotnet_diagnostic.CA5367.severity = warning -# Set ViewStateUserKey For Classes Derived From Page -dotnet_diagnostic.CA5368.severity = warning -# Use XmlReader For Deserialize -dotnet_diagnostic.CA5369.severity = warning -# Use XmlReader For Validating Reader -dotnet_diagnostic.CA5370.severity = warning -# Use XmlReader For Schema Read -dotnet_diagnostic.CA5371.severity = warning -# Use XmlReader For XPathDocument -dotnet_diagnostic.CA5372.severity = warning -# Do not use obsolete key derivation function -dotnet_diagnostic.CA5373.severity = warning -# Do Not Use XslTransform -dotnet_diagnostic.CA5374.severity = warning -# Do Not Use Account Shared Access Signature -dotnet_diagnostic.CA5375.severity = warning -# Use SharedAccessProtocol HttpsOnly -dotnet_diagnostic.CA5376.severity = warning -# Use Container Level Access Policy -dotnet_diagnostic.CA5377.severity = warning -# Do not disable ServicePointManagerSecurityProtocols -dotnet_diagnostic.CA5378.severity = warning -# Do Not Use Weak Key Derivation Function Algorithm -dotnet_diagnostic.CA5379.severity = warning -# Do Not Add Certificates To Root Store -dotnet_diagnostic.CA5380.severity = warning -# Ensure Certificates Are Not Added To Root Store -dotnet_diagnostic.CA5381.severity = warning -# Use Secure Cookies In ASP.Net Core -dotnet_diagnostic.CA5382.severity = warning -# Ensure Use Secure Cookies In ASP.Net Core -dotnet_diagnostic.CA5383.severity = warning -# Do Not Use Digital Signature Algorithm (DSA) -dotnet_diagnostic.CA5384.severity = warning -# Use Rivest–Shamir–Adleman (RSA) Algorithm With Sufficient Key Size -dotnet_diagnostic.CA5385.severity = warning -# Avoid hardcoding SecurityProtocolType value -dotnet_diagnostic.CA5386.severity = warning -# Do Not Use Weak Key Derivation Function With Insufficient Iteration Count -dotnet_diagnostic.CA5387.severity = warning -# Ensure Sufficient Iteration Count When Using Weak Key Derivation Function -dotnet_diagnostic.CA5388.severity = warning -# Do Not Add Archive Item's Path To The Target File System Path -dotnet_diagnostic.CA5389.severity = warning -# Do not hard-code encryption key -dotnet_diagnostic.CA5390.severity = warning -# Use antiforgery tokens in ASP.NET Core MVC controllers -dotnet_diagnostic.CA5391.severity = warning -# Use DefaultDllImportSearchPaths attribute for P/Invokes -dotnet_diagnostic.CA5392.severity = warning -# Do not use unsafe DllImportSearchPath value -dotnet_diagnostic.CA5393.severity = warning -# Do not use insecure randomness -dotnet_diagnostic.CA5394.severity = warning -# Miss HttpVerb attribute for action methods -dotnet_diagnostic.CA5395.severity = warning -# Set HttpOnly to true for HttpCookie -dotnet_diagnostic.CA5396.severity = warning -# Do not use deprecated SslProtocols values -dotnet_diagnostic.CA5397.severity = warning -# Avoid hardcoded SslProtocols values -dotnet_diagnostic.CA5398.severity = warning -# HttpClients should enable certificate revocation list checks -dotnet_diagnostic.CA5399.severity = warning -# Ensure HttpClient certificate revocation list check is not disabled -dotnet_diagnostic.CA5400.severity = warning -# Do not use CreateEncryptor with non-default IV -dotnet_diagnostic.CA5401.severity = warning -# Use CreateEncryptor with the default IV -dotnet_diagnostic.CA5402.severity = warning -# Do not hard-code certificate -dotnet_diagnostic.CA5403.severity = warning +# # Unused local variables +# dotnet_diagnostic.CA1804.severity = warning +# # Private methods that are not called from any other code +# dotnet_diagnostic.CA1811.severity = warning +# # Avoid unused private fields +# dotnet_diagnostic.CA1823.severity = warning +# # Use string.Contains(char) instead of string.Contains(string) with single characters +# dotnet_diagnostic.CA1847.severity = warning +# # Review SQL queries for security vulnerabilities +# dotnet_diagnostic.CA2100.severity = warning +# # Review visible event handlers +# dotnet_diagnostic.CA2109.severity = warning +# # Seal methods that satisfy private interfaces +# dotnet_diagnostic.CA2119.severity = warning +# # Do Not Catch Corrupted State Exceptions +# dotnet_diagnostic.CA2153.severity = warning +# # Do not use insecure deserializer BinaryFormatter +# dotnet_diagnostic.CA2300.severity = warning +# # Do not call BinaryFormatter.Deserialize without first setting BinaryFormatter.Binder +# dotnet_diagnostic.CA2301.severity = warning +# # Ensure BinaryFormatter.Binder is set before calling BinaryFormatter.Deserialize +# dotnet_diagnostic.CA2302.severity = warning +# # Do not use insecure deserializer LosFormatter +# dotnet_diagnostic.CA2305.severity = warning +# # Do not use insecure deserializer NetDataContractSerializer +# dotnet_diagnostic.CA2310.severity = warning +# # Do not deserialize without first setting NetDataContractSerializer.Binder +# dotnet_diagnostic.CA2311.severity = warning +# # Ensure NetDataContractSerializer.Binder is set before deserializing +# dotnet_diagnostic.CA2312.severity = warning +# # Do not use insecure deserializer ObjectStateFormatter +# dotnet_diagnostic.CA2315.severity = warning +# # Do not deserialize with JavaScriptSerializer using a SimpleTypeResolver +# dotnet_diagnostic.CA2321.severity = warning +# # Ensure JavaScriptSerializer is not initialized with SimpleTypeResolver before deserializing +# dotnet_diagnostic.CA2322.severity = warning +# # Do not use TypeNameHandling values other than None +# dotnet_diagnostic.CA2326.severity = warning +# # Do not use insecure JsonSerializerSettings +# dotnet_diagnostic.CA2327.severity = warning +# # Ensure that JsonSerializerSettings are secure +# dotnet_diagnostic.CA2328.severity = warning +# # Do not deserialize with JsonSerializer using an insecure configuration +# dotnet_diagnostic.CA2329.severity = warning +# # Ensure that JsonSerializer has a secure configuration when deserializing +# dotnet_diagnostic.CA2330.severity = warning +# # Do not use DataTable.ReadXml() with untrusted data +# dotnet_diagnostic.CA2350.severity = warning +# # Do not use DataSet.ReadXml() with untrusted data +# dotnet_diagnostic.CA2351.severity = warning +# # Unsafe DataSet or DataTable in serializable type can be vulnerable to remote code execution attacks +# dotnet_diagnostic.CA2352.severity = warning +# # Unsafe DataSet or DataTable in serializable type +# dotnet_diagnostic.CA2353.severity = warning +# # Unsafe DataSet or DataTable in deserialized object graph can be vulnerable to remote code execution attacks +# dotnet_diagnostic.CA2354.severity = warning +# # Unsafe DataSet or DataTable type found in deserializable object graph +# dotnet_diagnostic.CA2355.severity = warning +# # Unsafe DataSet or DataTable type in web deserializable object graph +# dotnet_diagnostic.CA2356.severity = warning +# # Ensure autogenerated class containing DataSet.ReadXml() is not used with untrusted data +# dotnet_diagnostic.CA2361.severity = warning +# # Unsafe DataSet or DataTable in autogenerated serializable type can be vulnerable to remote code execution attacks +# dotnet_diagnostic.CA2362.severity = warning +# # Review code for SQL injection vulnerabilities +# dotnet_diagnostic.CA3001.severity = warning +# # Review code for XSS vulnerabilities +# dotnet_diagnostic.CA3002.severity = warning +# # Review code for file path injection vulnerabilities +# dotnet_diagnostic.CA3003.severity = warning +# # Review code for information disclosure vulnerabilities +# dotnet_diagnostic.CA3004.severity = warning +# # Review code for LDAP injection vulnerabilities +# dotnet_diagnostic.CA3005.severity = warning +# # Review code for process command injection vulnerabilities +# dotnet_diagnostic.CA3006.severity = warning +# # Review code for open redirect vulnerabilities +# dotnet_diagnostic.CA3007.severity = warning +# # Review code for XPath injection vulnerabilities +# dotnet_diagnostic.CA3008.severity = warning +# # Review code for XML injection vulnerabilities +# dotnet_diagnostic.CA3009.severity = warning +# # Review code for XAML injection vulnerabilities +# dotnet_diagnostic.CA3010.severity = warning +# # Review code for DLL injection vulnerabilities +# dotnet_diagnostic.CA3011.severity = warning +# # Review code for regex injection vulnerabilities +# dotnet_diagnostic.CA3012.severity = warning +# # Do Not Add Schema By URL +# dotnet_diagnostic.CA3061.severity = warning +# # Insecure DTD processing in XML +# dotnet_diagnostic.CA3075.severity = warning +# # Insecure XSLT script processing. +# dotnet_diagnostic.CA3076.severity = warning +# # Insecure Processing in API Design, XmlDocument and XmlTextReader +# dotnet_diagnostic.CA3077.severity = warning +# # Mark Verb Handlers With Validate Antiforgery Token +# dotnet_diagnostic.CA3147.severity = warning +# # Do Not Use Weak Cryptographic Algorithms +# dotnet_diagnostic.CA5350.severity = warning +# # Do Not Use Broken Cryptographic Algorithms +# dotnet_diagnostic.CA5351.severity = warning +# # Review cipher mode usage with cryptography experts +# dotnet_diagnostic.CA5358.severity = warning +# # Do Not Disable Certificate Validation +# dotnet_diagnostic.CA5359.severity = warning +# # Do Not Call Dangerous Methods In Deserialization +# dotnet_diagnostic.CA5360.severity = warning +# # Do Not Disable SChannel Use of Strong Crypto +# dotnet_diagnostic.CA5361.severity = warning +# # Potential reference cycle in deserialized object graph +# dotnet_diagnostic.CA5362.severity = warning +# # Do Not Disable Request Validation +# dotnet_diagnostic.CA5363.severity = warning +# # Do Not Use Deprecated Security Protocols +# dotnet_diagnostic.CA5364.severity = warning +# # Do Not Disable HTTP Header Checking +# dotnet_diagnostic.CA5365.severity = warning +# # Use XmlReader For DataSet Read Xml +# dotnet_diagnostic.CA5366.severity = warning +# # Do Not Serialize Types With Pointer Fields +# dotnet_diagnostic.CA5367.severity = warning +# # Set ViewStateUserKey For Classes Derived From Page +# dotnet_diagnostic.CA5368.severity = warning +# # Use XmlReader For Deserialize +# dotnet_diagnostic.CA5369.severity = warning +# # Use XmlReader For Validating Reader +# dotnet_diagnostic.CA5370.severity = warning +# # Use XmlReader For Schema Read +# dotnet_diagnostic.CA5371.severity = warning +# # Use XmlReader For XPathDocument +# dotnet_diagnostic.CA5372.severity = warning +# # Do not use obsolete key derivation function +# dotnet_diagnostic.CA5373.severity = warning +# # Do Not Use XslTransform +# dotnet_diagnostic.CA5374.severity = warning +# # Do Not Use Account Shared Access Signature +# dotnet_diagnostic.CA5375.severity = warning +# # Use SharedAccessProtocol HttpsOnly +# dotnet_diagnostic.CA5376.severity = warning +# # Use Container Level Access Policy +# dotnet_diagnostic.CA5377.severity = warning +# # Do not disable ServicePointManagerSecurityProtocols +# dotnet_diagnostic.CA5378.severity = warning +# # Do Not Use Weak Key Derivation Function Algorithm +# dotnet_diagnostic.CA5379.severity = warning +# # Do Not Add Certificates To Root Store +# dotnet_diagnostic.CA5380.severity = warning +# # Ensure Certificates Are Not Added To Root Store +# dotnet_diagnostic.CA5381.severity = warning +# # Use Secure Cookies In ASP.Net Core +# dotnet_diagnostic.CA5382.severity = warning +# # Ensure Use Secure Cookies In ASP.Net Core +# dotnet_diagnostic.CA5383.severity = warning +# # Do Not Use Digital Signature Algorithm (DSA) +# dotnet_diagnostic.CA5384.severity = warning +# # Use Rivest–Shamir–Adleman (RSA) Algorithm With Sufficient Key Size +# dotnet_diagnostic.CA5385.severity = warning +# # Avoid hardcoding SecurityProtocolType value +# dotnet_diagnostic.CA5386.severity = warning +# # Do Not Use Weak Key Derivation Function With Insufficient Iteration Count +# dotnet_diagnostic.CA5387.severity = warning +# # Ensure Sufficient Iteration Count When Using Weak Key Derivation Function +# dotnet_diagnostic.CA5388.severity = warning +# # Do Not Add Archive Item's Path To The Target File System Path +# dotnet_diagnostic.CA5389.severity = warning +# # Do not hard-code encryption key +# dotnet_diagnostic.CA5390.severity = warning +# # Use antiforgery tokens in ASP.NET Core MVC controllers +# dotnet_diagnostic.CA5391.severity = warning +# # Use DefaultDllImportSearchPaths attribute for P/Invokes +# dotnet_diagnostic.CA5392.severity = warning +# # Do not use unsafe DllImportSearchPath value +# dotnet_diagnostic.CA5393.severity = warning +# # Do not use insecure randomness +# dotnet_diagnostic.CA5394.severity = warning +# # Miss HttpVerb attribute for action methods +# dotnet_diagnostic.CA5395.severity = warning +# # Set HttpOnly to true for HttpCookie +# dotnet_diagnostic.CA5396.severity = warning +# # Do not use deprecated SslProtocols values +# dotnet_diagnostic.CA5397.severity = warning +# # Avoid hardcoded SslProtocols values +# dotnet_diagnostic.CA5398.severity = warning +# # HttpClients should enable certificate revocation list checks +# dotnet_diagnostic.CA5399.severity = warning +# # Ensure HttpClient certificate revocation list check is not disabled +# dotnet_diagnostic.CA5400.severity = warning +# # Do not use CreateEncryptor with non-default IV +# dotnet_diagnostic.CA5401.severity = warning +# # Use CreateEncryptor with the default IV +# dotnet_diagnostic.CA5402.severity = warning +# # Do not hard-code certificate +# dotnet_diagnostic.CA5403.severity = warning -# Parameter 'parameter' has no matching param tag in the XML comment for 'parameter' (but other parameters do) -dotnet_diagnostic.CS1573.severity = suggestion -# Missing XML comment for publicly visible type or member 'Type_or_Member' -dotnet_diagnostic.CS1591.severity = none +# # Parameter 'parameter' has no matching param tag in the XML comment for 'parameter' (but other parameters do) +# dotnet_diagnostic.CS1573.severity = suggestion +# # Missing XML comment for publicly visible type or member 'Type_or_Member' +# dotnet_diagnostic.CS1591.severity = none -# VSTHRD200: Use "Async" suffix for async methods -dotnet_diagnostic.VSTHRD200.severity = none +# # VSTHRD200: Use "Async" suffix for async methods +# dotnet_diagnostic.VSTHRD200.severity = none diff --git a/BenchPress.sln b/BenchPress.sln index dd802ab..05fd40d 100644 --- a/BenchPress.sln +++ b/BenchPress.sln @@ -23,6 +23,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "dotnet", "dotnet", "{4596A4 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BenchPress.Sample", "samples\dotnet\BenchPress.Sample\BenchPress.Sample.csproj", "{89C81EDE-90E0-47CD-93DB-2E5024244D88}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bicep.Cli", "BenchPress\bicep\src\Bicep.Cli\Bicep.Cli.csproj", "{4C58A322-C05A-4E83-A702-2BED16E121B6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -52,6 +54,10 @@ Global {89C81EDE-90E0-47CD-93DB-2E5024244D88}.Debug|Any CPU.Build.0 = Debug|Any CPU {89C81EDE-90E0-47CD-93DB-2E5024244D88}.Release|Any CPU.ActiveCfg = Release|Any CPU {89C81EDE-90E0-47CD-93DB-2E5024244D88}.Release|Any CPU.Build.0 = Release|Any CPU + {4C58A322-C05A-4E83-A702-2BED16E121B6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C58A322-C05A-4E83-A702-2BED16E121B6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C58A322-C05A-4E83-A702-2BED16E121B6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C58A322-C05A-4E83-A702-2BED16E121B6}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {19C5E14A-FCA3-4335-A25C-D6DA88BE3099} = {29011A2E-34D3-468C-AA6F-C5AD5597970D} diff --git a/BenchPress/BenchPress.sln b/BenchPress/BenchPress.sln index 1d35786..4b08151 100644 --- a/BenchPress/BenchPress.sln +++ b/BenchPress/BenchPress.sln @@ -1,42 +1,50 @@ -Microsoft Visual Studio Solution File, Format Version 12.00 +Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.3.32708.82 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Generators", "./Generators/Generators.csproj", "{C779329A-EF48-47D7-8EC1-5D3CA3CC2E58}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Generators", ".\Generators\Generators.csproj", "{C779329A-EF48-47D7-8EC1-5D3CA3CC2E58}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bicep.Cli", "./bicep/src/Bicep.Cli/Bicep.Cli.csproj", "{0E331097-42A8-4B2A-A66A-189035BA2AD0}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bicep.Decompiler", ".\bicep\src\Bicep.Decompiler\Bicep.Decompiler.csproj", "{2B418AB3-40D2-4F64-9795-93B4299118E8}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bicep.Decompiler", "./bicep/src/Bicep.Decompiler/Bicep.Decompiler.csproj", "{2B418AB3-40D2-4F64-9795-93B4299118E8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bicep.Core", ".\bicep\src\Bicep.Core\Bicep.Core.csproj", "{A6B26AAD-D21F-48A5-9FB3-6FD3B00DC8F1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Bicep.Core", "./bicep/src/Bicep.Core/Bicep.Core.csproj", "{A6B26AAD-D21F-48A5-9FB3-6FD3B00DC8F1}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "bicep", "bicep", "{E06A5B8A-FB46-4713-B89C-8B033C8FA400}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{02F8A98A-1887-4887-BBE8-01F3679D9EE5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bicep.Cli", "bicep\src\Bicep.Cli\Bicep.Cli.csproj", "{49CCB6E9-1C8F-4FAA-877F-9A28E7B06976}" EndProject Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C779329A-EF48-47D7-8EC1-5D3CA3CC2E58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C779329A-EF48-47D7-8EC1-5D3CA3CC2E58}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C779329A-EF48-47D7-8EC1-5D3CA3CC2E58}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C779329A-EF48-47D7-8EC1-5D3CA3CC2E58}.Release|Any CPU.Build.0 = Release|Any CPU - {0E331097-42A8-4B2A-A66A-189035BA2AD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {0E331097-42A8-4B2A-A66A-189035BA2AD0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {0E331097-42A8-4B2A-A66A-189035BA2AD0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {0E331097-42A8-4B2A-A66A-189035BA2AD0}.Release|Any CPU.Build.0 = Release|Any CPU - {2B418AB3-40D2-4F64-9795-93B4299118E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2B418AB3-40D2-4F64-9795-93B4299118E8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2B418AB3-40D2-4F64-9795-93B4299118E8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2B418AB3-40D2-4F64-9795-93B4299118E8}.Release|Any CPU.Build.0 = Release|Any CPU - {A6B26AAD-D21F-48A5-9FB3-6FD3B00DC8F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A6B26AAD-D21F-48A5-9FB3-6FD3B00DC8F1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A6B26AAD-D21F-48A5-9FB3-6FD3B00DC8F1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A6B26AAD-D21F-48A5-9FB3-6FD3B00DC8F1}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {64B8D145-6451-43EA-B0D6-B46165A4B25D} - EndGlobalSection + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C779329A-EF48-47D7-8EC1-5D3CA3CC2E58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C779329A-EF48-47D7-8EC1-5D3CA3CC2E58}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C779329A-EF48-47D7-8EC1-5D3CA3CC2E58}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C779329A-EF48-47D7-8EC1-5D3CA3CC2E58}.Release|Any CPU.Build.0 = Release|Any CPU + {2B418AB3-40D2-4F64-9795-93B4299118E8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B418AB3-40D2-4F64-9795-93B4299118E8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B418AB3-40D2-4F64-9795-93B4299118E8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B418AB3-40D2-4F64-9795-93B4299118E8}.Release|Any CPU.Build.0 = Release|Any CPU + {A6B26AAD-D21F-48A5-9FB3-6FD3B00DC8F1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A6B26AAD-D21F-48A5-9FB3-6FD3B00DC8F1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A6B26AAD-D21F-48A5-9FB3-6FD3B00DC8F1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A6B26AAD-D21F-48A5-9FB3-6FD3B00DC8F1}.Release|Any CPU.Build.0 = Release|Any CPU + {49CCB6E9-1C8F-4FAA-877F-9A28E7B06976}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {49CCB6E9-1C8F-4FAA-877F-9A28E7B06976}.Debug|Any CPU.Build.0 = Debug|Any CPU + {49CCB6E9-1C8F-4FAA-877F-9A28E7B06976}.Release|Any CPU.ActiveCfg = Release|Any CPU + {49CCB6E9-1C8F-4FAA-877F-9A28E7B06976}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {64B8D145-6451-43EA-B0D6-B46165A4B25D} + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {02F8A98A-1887-4887-BBE8-01F3679D9EE5} = {E06A5B8A-FB46-4713-B89C-8B033C8FA400} + {49CCB6E9-1C8F-4FAA-877F-9A28E7B06976} = {02F8A98A-1887-4887-BBE8-01F3679D9EE5} + EndGlobalSection EndGlobal diff --git a/BenchPress/Generators/.gitignore b/BenchPress/Generators/.gitignore index 067ab95..9afc056 100644 --- a/BenchPress/Generators/.gitignore +++ b/BenchPress/Generators/.gitignore @@ -1,4 +1,4 @@ bin obj output -.vscode \ No newline at end of file +.vscode diff --git a/BenchPress/Helpers/Azure/Bicep.psm1 b/BenchPress/Helpers/Azure/Bicep.psm1 index 1bb5c1e..4d8886d 100644 --- a/BenchPress/Helpers/Azure/Bicep.psm1 +++ b/BenchPress/Helpers/Azure/Bicep.psm1 @@ -1,11 +1,11 @@ # Bicep Feature receives three parameters. # 1. Bicep file path. 2. parameters 3. resourceGroupName -function Deploy-BicepFeature([string]$path, $params){ +function Deploy-BicepFeature([string]$path, $params) { # armPath will be assigned for same bicep file name with json extension $fileName = [System.IO.Path]::GetFileNameWithoutExtension($path) $folder = Split-Path $path - $armPath = Join-Path -Path $folder -ChildPath "$fileName.json" + $armPath = Join-Path -Path $folder -ChildPath "$fileName.json" # az bicep build will create arm template from bicep file. # Arm template will same as bicep name with json extension @@ -20,13 +20,13 @@ function Deploy-BicepFeature([string]$path, $params){ # 1. TenantDeployment 2.ResourceGroupDeployment 3. ManagementGroupDeployment 4. SubscriptionDeployment New-AzSubscriptionDeployment -Name "$deploymentName" -Location "$location" -TemplateFile "$armPath" -TemplateParameterObject $params -SkipTemplateParameterPrompt } - + Write-Host "Removing Arm template json" Remove-Item "$armPath" } -function Remove-BicepFeature($resourceGroupName){ +function Remove-BicepFeature($resourceGroupName) { Get-AzResourceGroup -Name $resourceGroupName | Remove-AzResourceGroup -Force } diff --git a/BenchPress/bicep b/BenchPress/bicep new file mode 160000 index 0000000..e3b8f88 --- /dev/null +++ b/BenchPress/bicep @@ -0,0 +1 @@ +Subproject commit e3b8f88809b8d0232625951e0bd314b174ed7154 diff --git a/config/megalinter/.powershell-psscriptanalyzer.psd1 b/config/megalinter/.powershell-psscriptanalyzer.psd1 index 006f103..58f2504 100644 --- a/config/megalinter/.powershell-psscriptanalyzer.psd1 +++ b/config/megalinter/.powershell-psscriptanalyzer.psd1 @@ -8,10 +8,12 @@ #) #IncludeDefaultRules=${true} ExcludeRules = @( - 'PSMissingModuleManifestField' + 'PSMissingModuleManifestField', + 'PSProvideCommentHelp', + 'PSAvoidUsingWriteHost', + 'PSUseSingularNouns', + 'PSAvoidUsingPositionalParameters', + 'PSAvoidUsingInvokeExpression', + 'PSUseShouldProcessForStateChangingFunctions' ) - #IncludeRules = @( - # 'PSAvoidUsingWriteHost', - # 'MyCustomRuleName' - #) -} \ No newline at end of file +} diff --git a/engine/BenchPress.TestEngine.Tests/ArmDeploymentServiceTests.cs b/engine/BenchPress.TestEngine.Tests/ArmDeploymentServiceTests.cs index eb95ebb..8ecb125 100644 --- a/engine/BenchPress.TestEngine.Tests/ArmDeploymentServiceTests.cs +++ b/engine/BenchPress.TestEngine.Tests/ArmDeploymentServiceTests.cs @@ -237,4 +237,4 @@ public class ArmDeploymentServiceTests { return subDeploymentCollection.CreateOrUpdateAsync(waitUntil, deploymentName, deploymentContent); } } -} \ No newline at end of file +} diff --git a/engine/BenchPress.TestEngine.Tests/BicepTranspileServiceTests.cs b/engine/BenchPress.TestEngine.Tests/BicepTranspileServiceTests.cs new file mode 100644 index 0000000..2654cce --- /dev/null +++ b/engine/BenchPress.TestEngine.Tests/BicepTranspileServiceTests.cs @@ -0,0 +1,60 @@ +namespace BenchPress.TestEngine.Tests; + +public class BicepTranspileServiceTests +{ + private readonly Mock mockBicepSubmodule; + private readonly BicepTranspileService bicepTranspileService; + + public BicepTranspileServiceTests() + { + mockBicepSubmodule = new Mock(); + var logger = Mock.Of>(); + bicepTranspileService = new BicepTranspileService(mockBicepSubmodule.Object, logger); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + public async Task Build_NullInputPath_Throws(string inputFile) + { + mockBicepSubmodule.Setup(p => p.ExecuteCommandAsync(It.IsAny())).ReturnsAsync(0); + + await Assert.ThrowsAsync(async () => await bicepTranspileService.BuildAsync(inputFile)); + mockBicepSubmodule.Verify(p => p.ExecuteCommandAsync(It.IsAny()), Times.Never); + } + + [Theory] + [InlineData("a/b/c/test.bicep")] + public async Task Build_GeneratedArmTemplateExist(string inputFile) + { + mockBicepSubmodule.Setup(p => p.ExecuteCommandAsync(It.IsAny())).ReturnsAsync(0); + var inputBicepFilePath = Path.GetFullPath(inputFile); + var expectedPath = Path.GetFullPath(Path.ChangeExtension(inputBicepFilePath, ".json")); + var armPath = await bicepTranspileService.BuildAsync(inputBicepFilePath); + var args = new[] { "build", inputBicepFilePath, "--outfile", expectedPath }; + + Assert.Equal(expectedPath, armPath); + mockBicepSubmodule.Verify(p => p.ExecuteCommandAsync(args), Times.Once); + } + + [Theory] + [InlineData("a/b/c/test.txt")] + public async Task Build_NonBicepFileInputPath_Throws(string inputFile) + { + mockBicepSubmodule.Setup(p => p.ExecuteCommandAsync(It.IsAny())).ReturnsAsync(0); + var inputBicepFilePath = Path.GetFullPath(inputFile); + + await Assert.ThrowsAsync(async () => await bicepTranspileService.BuildAsync(inputBicepFilePath)); + } + + [Theory] + [InlineData("a/b/c/test.bicep")] + public async Task Build_BicepModuleNotImplemented_Throws(string inputFile) + { + mockBicepSubmodule.Setup(p => p.ExecuteCommandAsync(It.IsAny())).ThrowsAsync(new ApplicationException("Bicep transpilation failed")); + var inputBicepFilePath = Path.GetFullPath(inputFile); + + await Assert.ThrowsAsync(async () => await bicepTranspileService.BuildAsync(inputBicepFilePath)); + } +} diff --git a/engine/BenchPress.TestEngine.Tests/BicepServiceTests.cs b/engine/BenchPress.TestEngine.Tests/DeploymentServiceTests.cs similarity index 87% rename from engine/BenchPress.TestEngine.Tests/BicepServiceTests.cs rename to engine/BenchPress.TestEngine.Tests/DeploymentServiceTests.cs index 1cb89e1..bfc8bab 100644 --- a/engine/BenchPress.TestEngine.Tests/BicepServiceTests.cs +++ b/engine/BenchPress.TestEngine.Tests/DeploymentServiceTests.cs @@ -3,17 +3,17 @@ using Azure.ResourceManager.Resources; namespace BenchPress.TestEngine.Tests; -public class BicepServiceTests +public class DeploymentServiceTests { - private readonly BicepService bicepService; + private readonly DeploymentService deploymentService; private readonly ServerCallContext context; private readonly Mock armDeploymentMock; - public BicepServiceTests() + public DeploymentServiceTests() { - var logger = Mock.Of>(); + var logger = Mock.Of>(); armDeploymentMock = new Mock(MockBehavior.Strict); - bicepService = new BicepService(logger, armDeploymentMock.Object); + deploymentService = new DeploymentService(logger, armDeploymentMock.Object); context = new MockServerCallContext(); } @@ -23,7 +23,7 @@ public class BicepServiceTests // TODO: set up successful transpilation var templatePath = validGroupRequest.BicepFilePath; SetUpSuccessfulGroupDeployment(validGroupRequest, templatePath); - var result = await bicepService.DeploymentGroupCreate(validGroupRequest, context); + var result = await deploymentService.DeploymentGroupCreate(validGroupRequest, context); Assert.True(result.Success); VerifyGroupDeployment(validGroupRequest, templatePath); } @@ -38,7 +38,7 @@ public class BicepServiceTests // TODO: set up successful transpilation var templatePath = request.BicepFilePath; SetUpSuccessfulGroupDeployment(request, templatePath); - var result = await bicepService.DeploymentGroupCreate(request, context); + var result = await deploymentService.DeploymentGroupCreate(request, context); Assert.False(result.Success); // TODO: verify transpile wasn't called VerifyNoDeployments(); @@ -50,7 +50,7 @@ public class BicepServiceTests var expectedMessage = "the bicep file was malformed"; // TODO: set up exception throwing transpilation SetUpSuccessfulGroupDeployment(validGroupRequest, "template.json"); - var result = await bicepService.DeploymentGroupCreate(validGroupRequest, context); + var result = await deploymentService.DeploymentGroupCreate(validGroupRequest, context); Assert.False(result.Success); Assert.Equal(expectedMessage, result.ErrorMessage); } @@ -61,7 +61,7 @@ public class BicepServiceTests // TODO: set up successful transpilation var expectedReason = "Failure occured during deployment"; SetUpFailedGroupDeployment(expectedReason); - var result = await bicepService.DeploymentGroupCreate(validGroupRequest, context); + var result = await deploymentService.DeploymentGroupCreate(validGroupRequest, context); Assert.False(result.Success); Assert.Equal(expectedReason, result.ErrorMessage); } @@ -72,7 +72,7 @@ public class BicepServiceTests // TODO: set up successful transpilation var expectedMessage = "the template was malformed"; SetUpExceptionThrowingGroupDeployment(new Exception(expectedMessage)); - var result = await bicepService.DeploymentGroupCreate(validGroupRequest, context); + var result = await deploymentService.DeploymentGroupCreate(validGroupRequest, context); Assert.False(result.Success); Assert.Equal(expectedMessage, result.ErrorMessage); } @@ -85,7 +85,7 @@ public class BicepServiceTests ResourceGroupName = "test-rg", SubscriptionNameOrId = Guid.NewGuid().ToString() }; - var result = await bicepService.DeleteGroup(request, context); + var result = await deploymentService.DeleteGroup(request, context); Assert.True(result.Success); } @@ -143,7 +143,7 @@ public class BicepServiceTests It.IsAny(), It.IsAny(), It.IsAny())) - .ThrowsAsync(ex); + .ThrowsAsync(ex); } private void VerifyGroupDeployment(DeploymentGroupRequest request, string templatePath) { @@ -165,4 +165,4 @@ public class BicepServiceTests It.IsAny()), Times.Never); } -} \ No newline at end of file +} diff --git a/engine/BenchPress.TestEngine.Tests/Helpers/MockServerCallContext.cs b/engine/BenchPress.TestEngine.Tests/Helpers/MockServerCallContext.cs index eb106ff..ba7ed03 100644 --- a/engine/BenchPress.TestEngine.Tests/Helpers/MockServerCallContext.cs +++ b/engine/BenchPress.TestEngine.Tests/Helpers/MockServerCallContext.cs @@ -30,4 +30,4 @@ public class MockServerCallContext : ServerCallContext { throw new NotImplementedException(); } -} \ No newline at end of file +} diff --git a/engine/BenchPress.TestEngine.Tests/ResourceGroupServiceTests.cs b/engine/BenchPress.TestEngine.Tests/ResourceGroupServiceTests.cs index 369fa0d..498b6f7 100644 --- a/engine/BenchPress.TestEngine.Tests/ResourceGroupServiceTests.cs +++ b/engine/BenchPress.TestEngine.Tests/ResourceGroupServiceTests.cs @@ -23,4 +23,4 @@ public class ResourceGroupServiceTests var result = await resourceGroupService.GetResourceGroup(request, context); Assert.True(result.Existed); } -} \ No newline at end of file +} diff --git a/engine/BenchPress.TestEngine.Tests/SampleFiles/resourceGroup.bicep b/engine/BenchPress.TestEngine.Tests/SampleFiles/resourceGroup.bicep new file mode 100644 index 0000000..7ebd380 --- /dev/null +++ b/engine/BenchPress.TestEngine.Tests/SampleFiles/resourceGroup.bicep @@ -0,0 +1,17 @@ +targetScope = 'subscription' + +param resourceGroupName string +param location string +param environment string + +// https://docs.microsoft.com/en-us/azure/templates/microsoft.resources/resourcegroups?tabs=bicep +resource resourceGroup 'Microsoft.Resources/resourceGroups@2021-04-01' = { + name: resourceGroupName + location: location + tags: { + EnvironmentName: environment + } +} + +output name string = resourceGroup.name +output id string = resourceGroup.id diff --git a/engine/BenchPress.TestEngine.Tests/SampleFiles/storageAccount.bicep b/engine/BenchPress.TestEngine.Tests/SampleFiles/storageAccount.bicep new file mode 100644 index 0000000..78fd729 --- /dev/null +++ b/engine/BenchPress.TestEngine.Tests/SampleFiles/storageAccount.bicep @@ -0,0 +1,14 @@ +param name string +param location string + +resource sa 'Microsoft.Storage/storageAccounts@2019-06-01' = { + name: name + location: location + sku: { + name: 'Standard_LRS' + } + kind: 'StorageV2' + properties: { + accessTier: 'Hot' + } +} diff --git a/engine/BenchPress.TestEngine.Tests/Usings.cs b/engine/BenchPress.TestEngine.Tests/Usings.cs index 4141a67..72b1bd7 100644 --- a/engine/BenchPress.TestEngine.Tests/Usings.cs +++ b/engine/BenchPress.TestEngine.Tests/Usings.cs @@ -2,4 +2,4 @@ global using Xunit; global using Moq; global using Grpc.Core; global using BenchPress.TestEngine.Services; -global using Microsoft.Extensions.Logging; \ No newline at end of file +global using Microsoft.Extensions.Logging; diff --git a/engine/BenchPress.TestEngine/BenchPress.TestEngine.csproj b/engine/BenchPress.TestEngine/BenchPress.TestEngine.csproj index 3fe4e03..16aa34c 100644 --- a/engine/BenchPress.TestEngine/BenchPress.TestEngine.csproj +++ b/engine/BenchPress.TestEngine/BenchPress.TestEngine.csproj @@ -7,7 +7,7 @@ - + @@ -16,6 +16,10 @@ + + + + diff --git a/engine/BenchPress.TestEngine/Program.cs b/engine/BenchPress.TestEngine/Program.cs index 3fd710c..0ead46f 100644 --- a/engine/BenchPress.TestEngine/Program.cs +++ b/engine/BenchPress.TestEngine/Program.cs @@ -1,33 +1,35 @@ -using BenchPress.TestEngine.Services; -using Microsoft.AspNetCore.Server.Kestrel.Core; -using Microsoft.Extensions.Azure; -using Azure.Identity; - -var builder = WebApplication.CreateBuilder(args); - -// Additional configuration is required to successfully run gRPC on macOS. -// For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 -builder.WebHost.ConfigureKestrel(options => -{ - // Setup a HTTP/2 endpoint without TLS. - options.ListenLocalhost(5152, o => o.Protocols = - HttpProtocols.Http2); -}); - -// Add services to the container. -builder.Services.AddGrpc(); -builder.Services.AddAzureClients(builder => { - builder.AddClient(options => { - return new ArmClient(new DefaultAzureCredential()); - }); -}); -builder.Services.AddSingleton(); - -var app = builder.Build(); - -// Configure the HTTP request pipeline. -app.MapGrpcService(); -app.MapGrpcService(); -app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); - -app.Run(); +using BenchPress.TestEngine.Services; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.Extensions.Azure; +using Azure.Identity; + +var builder = WebApplication.CreateBuilder(args); + +// Additional configuration is required to successfully run gRPC on macOS. +// For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 +builder.WebHost.ConfigureKestrel(options => +{ + // Setup a HTTP/2 endpoint without TLS. + options.ListenLocalhost(5152, o => o.Protocols = + HttpProtocols.Http2); +}); + +// Add services to the container. +builder.Services.AddGrpc(); +builder.Services.AddAzureClients(builder => { + builder.AddClient(options => { + return new ArmClient(new DefaultAzureCredential()); + }); +}); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +app.MapGrpcService(); +app.MapGrpcService(); +app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909"); + +app.Run(); diff --git a/engine/BenchPress.TestEngine/Services/ArmDeploymentService.cs b/engine/BenchPress.TestEngine/Services/ArmDeploymentService.cs index abca0c2..da7f6c2 100644 --- a/engine/BenchPress.TestEngine/Services/ArmDeploymentService.cs +++ b/engine/BenchPress.TestEngine/Services/ArmDeploymentService.cs @@ -56,4 +56,4 @@ public class ArmDeploymentService : IArmDeploymentService { protected virtual async Task> CreateSubscriptionDeployment(SubscriptionResource sub, Azure.WaitUntil waitUntil, string deploymentName, ArmDeploymentContent deploymentContent) { return await sub.GetArmDeployments().CreateOrUpdateAsync(waitUntil, deploymentName, deploymentContent); } -} \ No newline at end of file +} diff --git a/engine/BenchPress.TestEngine/Services/BicepExecute.cs b/engine/BenchPress.TestEngine/Services/BicepExecute.cs new file mode 100644 index 0000000..80ab0df --- /dev/null +++ b/engine/BenchPress.TestEngine/Services/BicepExecute.cs @@ -0,0 +1,11 @@ +using Bicep.Cli; + +namespace BenchPress.TestEngine.Services; + +public class BicepExecute : IBicepExecute +{ + public Task ExecuteCommandAsync(string[] args) + { + return Bicep.Cli.Program.Main(args); + } +} diff --git a/engine/BenchPress.TestEngine/Services/BicepTranspileService.cs b/engine/BenchPress.TestEngine/Services/BicepTranspileService.cs new file mode 100644 index 0000000..fa8083b --- /dev/null +++ b/engine/BenchPress.TestEngine/Services/BicepTranspileService.cs @@ -0,0 +1,54 @@ +using System.IO; + +namespace BenchPress.TestEngine.Services; + +public class BicepTranspileService : IBicepTranspileService +{ + private IBicepExecute bicepExecute; + private readonly ILogger logger; + + public BicepTranspileService(IBicepExecute bicepExecute, ILogger logger) + { + this.bicepExecute = bicepExecute; + this.logger = logger; + } + + public async Task BuildAsync(string inputPath) + { + if (string.IsNullOrWhiteSpace(inputPath)) + { + throw new ArgumentNullException(nameof(inputPath)); + } + + if (Path.GetExtension(inputPath) != ".bicep") + { + throw new ArgumentException("Passed file is not a bicep file. File path: " + inputPath); + } + + inputPath = Path.GetFullPath(inputPath); + string outputPath = Path.ChangeExtension(inputPath, ".json"); + + logger.LogInformation("Invoking Bicep Submodule"); + try + { + var result = await bicepExecute.ExecuteCommandAsync(new string[]{ + "build", + inputPath, + "--outfile", + outputPath + }); + + if (result != 0) + { + throw new ApplicationException("Bicep transpilation failed"); + } + } + catch (Exception ex) + { + logger.LogError(ex.Message, ex); + throw; + } + + return outputPath; + } +} diff --git a/engine/BenchPress.TestEngine/Services/BicepService.cs b/engine/BenchPress.TestEngine/Services/DeploymentService.cs similarity index 76% rename from engine/BenchPress.TestEngine/Services/BicepService.cs rename to engine/BenchPress.TestEngine/Services/DeploymentService.cs index 3849d63..f676eaf 100644 --- a/engine/BenchPress.TestEngine/Services/BicepService.cs +++ b/engine/BenchPress.TestEngine/Services/DeploymentService.cs @@ -1,11 +1,12 @@ namespace BenchPress.TestEngine.Services; -public class BicepService : Bicep.BicepBase +public class DeploymentService : Deployment.DeploymentBase { - private readonly ILogger logger; + + private readonly ILogger logger; private readonly IArmDeploymentService armDeploymentService; - public BicepService(ILogger logger, IArmDeploymentService armDeploymentService) + public DeploymentService(ILogger logger, IArmDeploymentService armDeploymentService) { this.logger = logger; this.armDeploymentService = armDeploymentService; @@ -15,26 +16,32 @@ public class BicepService : Bicep.BicepBase { if (string.IsNullOrWhiteSpace(request.BicepFilePath) || string.IsNullOrWhiteSpace(request.ResourceGroupName) - || string.IsNullOrWhiteSpace(request.SubscriptionNameOrId)) + || string.IsNullOrWhiteSpace(request.SubscriptionNameOrId)) { - return new DeploymentResult { + return new DeploymentResult + { Success = false, ErrorMessage = $"One or more of the following required parameters was missing: {nameof(request.BicepFilePath)}, {nameof(request.ResourceGroupName)}, and {nameof(request.SubscriptionNameOrId)}" }; } - try { + try + { // TODO: pass in transpiled arm template instead var deployment = await armDeploymentService.DeployArmToResourceGroupAsync(request.SubscriptionNameOrId, request.ResourceGroupName, request.BicepFilePath, request.ParameterFilePath); var response = deployment.WaitForCompletionResponse(); - - return new DeploymentResult { + + return new DeploymentResult + { Success = !response.IsError, ErrorMessage = response.ReasonPhrase }; - } catch (Exception ex) { - return new DeploymentResult { + } + catch (Exception ex) + { + return new DeploymentResult + { Success = false, ErrorMessage = ex.Message }; @@ -45,4 +52,4 @@ public class BicepService : Bicep.BicepBase { throw new NotImplementedException(); } -} \ No newline at end of file +} diff --git a/engine/BenchPress.TestEngine/Services/IArmDeploymentService.cs b/engine/BenchPress.TestEngine/Services/IArmDeploymentService.cs index 505011d..349d768 100644 --- a/engine/BenchPress.TestEngine/Services/IArmDeploymentService.cs +++ b/engine/BenchPress.TestEngine/Services/IArmDeploymentService.cs @@ -3,4 +3,4 @@ namespace BenchPress.TestEngine.Services; public interface IArmDeploymentService { Task> DeployArmToResourceGroupAsync(string subscriptionNameOrId, string resourceGroupName, string armTemplatePath, string? parametersPath = null, Azure.WaitUntil waitUtil = Azure.WaitUntil.Completed); Task> DeployArmToSubscriptionAsync(string subscriptionNameOrId, string armTemplatePath, string? parametersPath = null, Azure.WaitUntil waitUtil = Azure.WaitUntil.Completed); -} \ No newline at end of file +} diff --git a/engine/BenchPress.TestEngine/Services/IBicepExecute.cs b/engine/BenchPress.TestEngine/Services/IBicepExecute.cs new file mode 100644 index 0000000..c86b947 --- /dev/null +++ b/engine/BenchPress.TestEngine/Services/IBicepExecute.cs @@ -0,0 +1,6 @@ +namespace BenchPress.TestEngine.Services; + +public interface IBicepExecute +{ + Task ExecuteCommandAsync(string[] args); +} diff --git a/engine/BenchPress.TestEngine/Services/IBicepTranspileService.cs b/engine/BenchPress.TestEngine/Services/IBicepTranspileService.cs new file mode 100644 index 0000000..fd220d2 --- /dev/null +++ b/engine/BenchPress.TestEngine/Services/IBicepTranspileService.cs @@ -0,0 +1,6 @@ +namespace BenchPress.TestEngine.Services; + +public interface IBicepTranspileService +{ + Task BuildAsync(string inputPath); +} diff --git a/framework/dotnet/BenchPress.TestFramework/BenchPress.TestFramework.csproj b/framework/dotnet/BenchPress.TestFramework/BenchPress.TestFramework.csproj index d7c2235..076340e 100644 --- a/framework/dotnet/BenchPress.TestFramework/BenchPress.TestFramework.csproj +++ b/framework/dotnet/BenchPress.TestFramework/BenchPress.TestFramework.csproj @@ -12,7 +12,7 @@ - + diff --git a/protos/bicep.proto b/protos/deployment.proto similarity index 97% rename from protos/bicep.proto rename to protos/deployment.proto index 31efa87..625e760 100644 --- a/protos/bicep.proto +++ b/protos/deployment.proto @@ -6,7 +6,7 @@ option csharp_namespace = "BenchPress.TestEngine"; // Currently only supports deployments with the target scope of resource group. // Other scopes: subscription, management group, and tenant. -service Bicep { +service Deployment { rpc DeploymentGroupCreate (DeploymentGroupRequest) returns (DeploymentResult); rpc DeleteGroup (DeleteGroupRequest) returns (DeploymentResult); } From 027e4f456b209ab9bd5504bf9e94135ea948e9ab Mon Sep 17 00:00:00 2001 From: jessica-ern <107070686+jessica-ern@users.noreply.github.com> Date: Wed, 9 Nov 2022 14:34:33 -0600 Subject: [PATCH 07/12] integrate BicepTranspileService (#23) --- .../DeploymentServiceTests.cs | 66 +++++++++++++------ .../Services/DeploymentService.cs | 9 +-- 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/engine/BenchPress.TestEngine.Tests/DeploymentServiceTests.cs b/engine/BenchPress.TestEngine.Tests/DeploymentServiceTests.cs index bfc8bab..3f289bd 100644 --- a/engine/BenchPress.TestEngine.Tests/DeploymentServiceTests.cs +++ b/engine/BenchPress.TestEngine.Tests/DeploymentServiceTests.cs @@ -7,21 +7,23 @@ public class DeploymentServiceTests { private readonly DeploymentService deploymentService; private readonly ServerCallContext context; + private readonly Mock bicepTranspileServiceMock; private readonly Mock armDeploymentMock; + private const string templatePath = "main.json"; public DeploymentServiceTests() { var logger = Mock.Of>(); + bicepTranspileServiceMock = new Mock(MockBehavior.Strict); armDeploymentMock = new Mock(MockBehavior.Strict); - deploymentService = new DeploymentService(logger, armDeploymentMock.Object); + deploymentService = new DeploymentService(logger, bicepTranspileServiceMock.Object, armDeploymentMock.Object); context = new MockServerCallContext(); } [Fact] public async Task DeploymentGroupCreate_DeploysResourceGroup_WithTranspiledFiles() { - // TODO: set up successful transpilation - var templatePath = validGroupRequest.BicepFilePath; + SetUpSuccessfulTranspilation(validGroupRequest.BicepFilePath, templatePath); SetUpSuccessfulGroupDeployment(validGroupRequest, templatePath); var result = await deploymentService.DeploymentGroupCreate(validGroupRequest, context); Assert.True(result.Success); @@ -35,20 +37,19 @@ public class DeploymentServiceTests public async Task DeploymentGroupCreate_FailsOnMissingParameters(string bicepFilePath, string resourceGroupName, string subscriptionNameOrId) { var request = SetUpGroupRequest(bicepFilePath, resourceGroupName, subscriptionNameOrId); - // TODO: set up successful transpilation - var templatePath = request.BicepFilePath; + SetUpSuccessfulTranspilation(validGroupRequest.BicepFilePath, templatePath); SetUpSuccessfulGroupDeployment(request, templatePath); var result = await deploymentService.DeploymentGroupCreate(request, context); Assert.False(result.Success); - // TODO: verify transpile wasn't called + VerifyNoTranspilation(); VerifyNoDeployments(); } - [Fact(Skip = "Not Fully Implemented")] + [Fact] public async Task DeploymentGroupCreate_ReturnsFailureOnTranspileException() { var expectedMessage = "the bicep file was malformed"; - // TODO: set up exception throwing transpilation + SetUpExceptionThrowingTranspilation(new Exception(expectedMessage)); SetUpSuccessfulGroupDeployment(validGroupRequest, "template.json"); var result = await deploymentService.DeploymentGroupCreate(validGroupRequest, context); Assert.False(result.Success); @@ -58,7 +59,7 @@ public class DeploymentServiceTests [Fact] public async Task DeploymentGroupCreate_ReturnsFailureOnFailedDeployment() { - // TODO: set up successful transpilation + SetUpSuccessfulTranspilation(validGroupRequest.BicepFilePath, templatePath); var expectedReason = "Failure occured during deployment"; SetUpFailedGroupDeployment(expectedReason); var result = await deploymentService.DeploymentGroupCreate(validGroupRequest, context); @@ -69,7 +70,7 @@ public class DeploymentServiceTests [Fact] public async Task DeploymentGroupCreate_ReturnsFailureOnDeploymentException() { - // TODO: set up successful transpilation + SetUpSuccessfulTranspilation(validGroupRequest.BicepFilePath, templatePath); var expectedMessage = "the template was malformed"; SetUpExceptionThrowingGroupDeployment(new Exception(expectedMessage)); var result = await deploymentService.DeploymentGroupCreate(validGroupRequest, context); @@ -96,7 +97,8 @@ public class DeploymentServiceTests SubscriptionNameOrId = Guid.NewGuid().ToString() }; - private DeploymentGroupRequest SetUpGroupRequest(string bicepFilePath, string resourceGroupName, string subscriptionNameOrId) { + private DeploymentGroupRequest SetUpGroupRequest(string bicepFilePath, string resourceGroupName, string subscriptionNameOrId) + { return new DeploymentGroupRequest { BicepFilePath = bicepFilePath, @@ -105,7 +107,28 @@ public class DeploymentServiceTests }; } - private ArmOperation SetupDeploymentOperation(bool success, string reason) { + private void SetUpSuccessfulTranspilation(string bicepFilePath, string armTemplatePath) + { + bicepTranspileServiceMock.Setup(x => x.BuildAsync(bicepFilePath)).ReturnsAsync(armTemplatePath); + } + + private void SetUpExceptionThrowingTranspilation(Exception ex) + { + bicepTranspileServiceMock.Setup(x => x.BuildAsync(It.IsAny())).ThrowsAsync(ex); + } + + private void VerifyTranspilation(string bicepFilePath) + { + bicepTranspileServiceMock.Verify(x => x.BuildAsync(bicepFilePath), Times.Once); + } + + private void VerifyNoTranspilation() + { + bicepTranspileServiceMock.Verify(x => x.BuildAsync(It.IsAny()), Times.Never); + } + + private ArmOperation SetupDeploymentOperation(bool success, string reason) + { var responseMock = new Mock(); responseMock.Setup(x => x.IsError).Returns(!success); responseMock.Setup(x => x.ReasonPhrase).Returns(reason); @@ -114,7 +137,8 @@ public class DeploymentServiceTests return operationMock.Object; } - private void SetUpSuccessfulGroupDeployment(DeploymentGroupRequest request, string templatePath) { + private void SetUpSuccessfulGroupDeployment(DeploymentGroupRequest request, string templatePath) + { var operation = SetupDeploymentOperation(true, "OK"); armDeploymentMock.Setup(x => x.DeployArmToResourceGroupAsync( request.SubscriptionNameOrId, @@ -125,7 +149,8 @@ public class DeploymentServiceTests .ReturnsAsync(operation); } - private void SetUpFailedGroupDeployment(string reason) { + private void SetUpFailedGroupDeployment(string reason) + { var operation = SetupDeploymentOperation(false, reason); armDeploymentMock.Setup(x => x.DeployArmToResourceGroupAsync( It.IsAny(), @@ -136,17 +161,19 @@ public class DeploymentServiceTests .ReturnsAsync(operation); } - private void SetUpExceptionThrowingGroupDeployment(Exception ex) { + private void SetUpExceptionThrowingGroupDeployment(Exception ex) + { armDeploymentMock.Setup(x => x.DeployArmToResourceGroupAsync( It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .ThrowsAsync(ex); + .ThrowsAsync(ex); } - private void VerifyGroupDeployment(DeploymentGroupRequest request, string templatePath) { + private void VerifyGroupDeployment(DeploymentGroupRequest request, string templatePath) + { armDeploymentMock.Verify(x => x.DeployArmToResourceGroupAsync( request.SubscriptionNameOrId, request.ResourceGroupName, @@ -156,7 +183,8 @@ public class DeploymentServiceTests Times.Once); } - private void VerifyNoDeployments() { + private void VerifyNoDeployments() + { armDeploymentMock.Verify(x => x.DeployArmToResourceGroupAsync( It.IsAny(), It.IsAny(), @@ -165,4 +193,4 @@ public class DeploymentServiceTests It.IsAny()), Times.Never); } -} +} \ No newline at end of file diff --git a/engine/BenchPress.TestEngine/Services/DeploymentService.cs b/engine/BenchPress.TestEngine/Services/DeploymentService.cs index f676eaf..7c745e2 100644 --- a/engine/BenchPress.TestEngine/Services/DeploymentService.cs +++ b/engine/BenchPress.TestEngine/Services/DeploymentService.cs @@ -4,11 +4,13 @@ public class DeploymentService : Deployment.DeploymentBase { private readonly ILogger logger; + private readonly IBicepTranspileService bicepTranspileService; private readonly IArmDeploymentService armDeploymentService; - public DeploymentService(ILogger logger, IArmDeploymentService armDeploymentService) + public DeploymentService(ILogger logger, IBicepTranspileService bicepTranspileService, IArmDeploymentService armDeploymentService) { this.logger = logger; + this.bicepTranspileService = bicepTranspileService; this.armDeploymentService = armDeploymentService; } @@ -27,8 +29,8 @@ public class DeploymentService : Deployment.DeploymentBase try { - // TODO: pass in transpiled arm template instead - var deployment = await armDeploymentService.DeployArmToResourceGroupAsync(request.SubscriptionNameOrId, request.ResourceGroupName, request.BicepFilePath, request.ParameterFilePath); + var armTemplatePath = await bicepTranspileService.BuildAsync(request.BicepFilePath); + var deployment = await armDeploymentService.DeployArmToResourceGroupAsync(request.SubscriptionNameOrId, request.ResourceGroupName, armTemplatePath, request.ParameterFilePath); var response = deployment.WaitForCompletionResponse(); return new DeploymentResult @@ -36,7 +38,6 @@ public class DeploymentService : Deployment.DeploymentBase Success = !response.IsError, ErrorMessage = response.ReasonPhrase }; - } catch (Exception ex) { From 59a2644e7681b22fb01089ba705855a226ea0330 Mon Sep 17 00:00:00 2001 From: jessica-ern <107070686+jessica-ern@users.noreply.github.com> Date: Thu, 10 Nov 2022 13:38:33 -0600 Subject: [PATCH 08/12] expose gRPC endpoint for deploying at the subscription scope (#24) * expose gRPC endpoint for deploying at the subscription scope * not the template name I wanted --- .../ArmDeploymentServiceTests.cs | 25 +++- .../DeploymentServiceTests.cs | 129 ++++++++++++++++++ .../Services/ArmDeploymentService.cs | 24 ++-- .../Services/DeploymentService.cs | 35 +++++ .../Services/IArmDeploymentService.cs | 2 +- protos/deployment.proto | 8 ++ 6 files changed, 207 insertions(+), 16 deletions(-) diff --git a/engine/BenchPress.TestEngine.Tests/ArmDeploymentServiceTests.cs b/engine/BenchPress.TestEngine.Tests/ArmDeploymentServiceTests.cs index 8ecb125..e0b1154 100644 --- a/engine/BenchPress.TestEngine.Tests/ArmDeploymentServiceTests.cs +++ b/engine/BenchPress.TestEngine.Tests/ArmDeploymentServiceTests.cs @@ -12,6 +12,7 @@ public class ArmDeploymentServiceTests { private readonly Mock subscriptionDeploymentsMock; private const string validSubId = "a3a01f37-665c-4ee8-9bc3-3adf7ebcec0d"; private const string validRgName = "test-rg"; + private const string validLocation = "eastus"; private const string smapleFiles = "../../../SampleFiles"; private const string standaloneTemplate = $"{smapleFiles}/storage-account.json"; private const string templateWithParams = $"{smapleFiles}/storage-account-needs-params.json"; @@ -92,19 +93,20 @@ public class ArmDeploymentServiceTests { { var subMock = SetUpSubscriptionMock(validSubId); SetUpDeploymentsMock(subscriptionDeploymentsMock); - await armDeploymentService.DeployArmToSubscriptionAsync(validSubId, templateWithParams, parameters); + await armDeploymentService.DeployArmToSubscriptionAsync(validSubId, validLocation, templateWithParams, parameters); VerifyDeploymentsMock(subscriptionDeploymentsMock); } [Theory] - [InlineData("", "a3a01f37-665c-4ee8-9bc3-3adf7ebcec0d")] - [InlineData("main.bicep", "")] - public async Task DeployArmToSubscriptionAsync_MissingParameter_ThrowsException(string templatePath, string subId) + [InlineData("main.bicep", "", "a3a01f37-665c-4ee8-9bc3-3adf7ebcec0d")] + [InlineData("", "eastus", "a3a01f37-665c-4ee8-9bc3-3adf7ebcec0d")] + [InlineData("main.bicep", "eastus", "")] + public async Task DeployArmToSubscriptionAsync_MissingParameter_ThrowsException(string templatePath, string location, string subId) { var subMock = SetUpSubscriptionMock(subId); SetUpDeploymentsMock(subscriptionDeploymentsMock); var ex = await Assert.ThrowsAsync( - async () => await armDeploymentService.DeployArmToSubscriptionAsync(subId, templatePath) + async () => await armDeploymentService.DeployArmToSubscriptionAsync(subId, location, templatePath) ); Assert.Equal("One or more parameters were missing or empty", ex.Message); } @@ -116,7 +118,7 @@ public class ArmDeploymentServiceTests { var excepectedMessage = "Deployment template validation failed"; SetUpDeploymentExceptionMock(subscriptionDeploymentsMock, new RequestFailedException(excepectedMessage)); var ex = await Assert.ThrowsAsync( - async () => await armDeploymentService.DeployArmToSubscriptionAsync(validSubId, templateWithParams) + async () => await armDeploymentService.DeployArmToSubscriptionAsync(validSubId, validLocation, templateWithParams) ); Assert.Equal(excepectedMessage, ex.Message); } @@ -127,13 +129,22 @@ public class ArmDeploymentServiceTests { var subMock = SetUpSubscriptionMock(validSubId); SetUpDeploymentsMock(subscriptionDeploymentsMock); var ex = await Assert.ThrowsAsync( - async () => await armDeploymentService.DeployArmToSubscriptionAsync("The Wrong Subscription", standaloneTemplate) + async () => await armDeploymentService.DeployArmToSubscriptionAsync("The Wrong Subscription", validLocation, standaloneTemplate) ); Assert.Equal("Subscription Not Found", ex.Message); } [Fact] public async Task CreateDeploymentContent_WithoutParameters() + { + var subMock = SetUpSubscriptionMock(validSubId); + SetUpDeploymentsMock(subscriptionDeploymentsMock); + await armDeploymentService.DeployArmToSubscriptionAsync(validSubId, validLocation, standaloneTemplate); + VerifyDeploymentsMock(subscriptionDeploymentsMock); + } + + [Fact] + public async Task CreateDeploymentContent_WithoutLocation() { var subMock = SetUpSubscriptionMock(validSubId); var rgMock = SetUpResourceGroupMock(subMock, validRgName); diff --git a/engine/BenchPress.TestEngine.Tests/DeploymentServiceTests.cs b/engine/BenchPress.TestEngine.Tests/DeploymentServiceTests.cs index 3f289bd..f428a12 100644 --- a/engine/BenchPress.TestEngine.Tests/DeploymentServiceTests.cs +++ b/engine/BenchPress.TestEngine.Tests/DeploymentServiceTests.cs @@ -78,6 +78,64 @@ public class DeploymentServiceTests Assert.Equal(expectedMessage, result.ErrorMessage); } + [Fact] + public async Task DeploymentSubCreate_DeploysToSub_WithTranspiledFiles() + { + SetUpSuccessfulTranspilation(validSubRequest.BicepFilePath, templatePath); + SetUpSuccessfulSubDeployment(validSubRequest, templatePath); + var result = await deploymentService.DeploymentSubCreate(validSubRequest, context); + Assert.True(result.Success); + VerifySubDeployment(validSubRequest, templatePath); + } + + [Theory] + [InlineData("main.bicep", "", "a3a01f37-665c-4ee8-9bc3-3adf7ebcec0d")] + [InlineData("", "eastus", "a3a01f37-665c-4ee8-9bc3-3adf7ebcec0d")] + [InlineData("main.bicep", "eastus", "")] + public async Task DeploymentSubCreate_FailsOnMissingParameters(string bicepFilePath, string location, string subscriptionNameOrId) + { + var request = SetUpSubRequest(bicepFilePath, location, subscriptionNameOrId); + SetUpSuccessfulTranspilation(validSubRequest.BicepFilePath, templatePath); + SetUpSuccessfulSubDeployment(request, templatePath); + var result = await deploymentService.DeploymentSubCreate(request, context); + Assert.False(result.Success); + VerifyNoTranspilation(); + VerifyNoDeployments(); + } + + [Fact] + public async Task DeploymentSubCreate_ReturnsFailureOnTranspileException() + { + var expectedMessage = "the bicep file was malformed"; + SetUpExceptionThrowingTranspilation(new Exception(expectedMessage)); + SetUpSuccessfulSubDeployment(validSubRequest, "template.json"); + var result = await deploymentService.DeploymentSubCreate(validSubRequest, context); + Assert.False(result.Success); + Assert.Equal(expectedMessage, result.ErrorMessage); + } + + [Fact] + public async Task DeploymentSubCreate_ReturnsFailureOnFailedDeployment() + { + SetUpSuccessfulTranspilation(validSubRequest.BicepFilePath, templatePath); + var expectedReason = "Failure occured during deployment"; + SetUpFailedSubDeployment(expectedReason); + var result = await deploymentService.DeploymentSubCreate(validSubRequest, context); + Assert.False(result.Success); + Assert.Equal(expectedReason, result.ErrorMessage); + } + + [Fact] + public async Task DeploymentSubpCreate_ReturnsFailureOnDeploymentException() + { + SetUpSuccessfulTranspilation(validSubRequest.BicepFilePath, templatePath); + var expectedMessage = "the template was malformed"; + SetUpExceptionThrowingSubDeployment(new Exception(expectedMessage)); + var result = await deploymentService.DeploymentSubCreate(validSubRequest, context); + Assert.False(result.Success); + Assert.Equal(expectedMessage, result.ErrorMessage); + } + [Fact(Skip = "Not Implemented")] public async Task DeleteGroup_DeletesAllResources() { @@ -97,6 +155,13 @@ public class DeploymentServiceTests SubscriptionNameOrId = Guid.NewGuid().ToString() }; + private readonly DeploymentSubRequest validSubRequest = new DeploymentSubRequest + { + BicepFilePath = "main.bicep", + Location = "eastus", + SubscriptionNameOrId = Guid.NewGuid().ToString() + }; + private DeploymentGroupRequest SetUpGroupRequest(string bicepFilePath, string resourceGroupName, string subscriptionNameOrId) { return new DeploymentGroupRequest @@ -107,6 +172,16 @@ public class DeploymentServiceTests }; } + private DeploymentSubRequest SetUpSubRequest(string bicepFilePath, string location, string subscriptionNameOrId) + { + return new DeploymentSubRequest + { + BicepFilePath = bicepFilePath, + Location = location, + SubscriptionNameOrId = subscriptionNameOrId + }; + } + private void SetUpSuccessfulTranspilation(string bicepFilePath, string armTemplatePath) { bicepTranspileServiceMock.Setup(x => x.BuildAsync(bicepFilePath)).ReturnsAsync(armTemplatePath); @@ -183,6 +258,52 @@ public class DeploymentServiceTests Times.Once); } + private void SetUpSuccessfulSubDeployment(DeploymentSubRequest request, string templatePath) + { + var operation = SetupDeploymentOperation(true, "OK"); + armDeploymentMock.Setup(x => x.DeployArmToSubscriptionAsync( + request.SubscriptionNameOrId, + request.Location, + templatePath, + request.ParameterFilePath, + It.IsAny())) + .ReturnsAsync(operation); + } + + private void SetUpFailedSubDeployment(string reason) + { + var operation = SetupDeploymentOperation(false, reason); + armDeploymentMock.Setup(x => x.DeployArmToSubscriptionAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(operation); + } + + private void SetUpExceptionThrowingSubDeployment(Exception ex) + { + armDeploymentMock.Setup(x => x.DeployArmToSubscriptionAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ThrowsAsync(ex); + } + + private void VerifySubDeployment(DeploymentSubRequest request, string templatePath) + { + armDeploymentMock.Verify(x => x.DeployArmToSubscriptionAsync( + request.SubscriptionNameOrId, + request.Location, + templatePath, + request.ParameterFilePath, + It.IsAny()), + Times.Once); + } + private void VerifyNoDeployments() { armDeploymentMock.Verify(x => x.DeployArmToResourceGroupAsync( @@ -192,5 +313,13 @@ public class DeploymentServiceTests It.IsAny(), It.IsAny()), Times.Never); + + armDeploymentMock.Verify(x => x.DeployArmToSubscriptionAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Never); } } \ No newline at end of file diff --git a/engine/BenchPress.TestEngine/Services/ArmDeploymentService.cs b/engine/BenchPress.TestEngine/Services/ArmDeploymentService.cs index da7f6c2..231eb90 100644 --- a/engine/BenchPress.TestEngine/Services/ArmDeploymentService.cs +++ b/engine/BenchPress.TestEngine/Services/ArmDeploymentService.cs @@ -20,11 +20,11 @@ public class ArmDeploymentService : IArmDeploymentService { return await CreateGroupDeployment(rg, waitUntil, NewDeploymentName, deploymentContent); } - public async Task> DeployArmToSubscriptionAsync(string subscriptionNameOrId, string armTemplatePath, string? parametersPath = null, WaitUntil waitUtil = WaitUntil.Completed) + public async Task> DeployArmToSubscriptionAsync(string subscriptionNameOrId, string location, string armTemplatePath, string? parametersPath = null, WaitUntil waitUtil = WaitUntil.Completed) { - ValidateParameters(subscriptionNameOrId, armTemplatePath); + ValidateParameters(subscriptionNameOrId, location, armTemplatePath); SubscriptionResource sub = await client.GetSubscriptions().GetAsync(subscriptionNameOrId); - var deploymentContent = await CreateDeploymentContent(armTemplatePath, parametersPath); + var deploymentContent = await CreateDeploymentContent(armTemplatePath, parametersPath, location); return await CreateSubscriptionDeployment(sub, waitUtil, NewDeploymentName, deploymentContent); } @@ -34,18 +34,26 @@ public class ArmDeploymentService : IArmDeploymentService { } } - private async Task CreateDeploymentContent(string armTemplatePath, string? parametersPath) { + private async Task CreateDeploymentContent(string armTemplatePath, string? parametersPath, string? location = null) { var templateContent = (await File.ReadAllTextAsync(armTemplatePath)).TrimEnd(); - var properties = new ArmDeploymentProperties(ArmDeploymentMode.Incremental) { + var properties = new ArmDeploymentProperties(ArmDeploymentMode.Incremental) + { Template = BinaryData.FromString(templateContent) }; - - if (!string.IsNullOrWhiteSpace(parametersPath)) { + + if (!string.IsNullOrWhiteSpace(parametersPath)) + { var paramteresContent = (await File.ReadAllTextAsync(parametersPath)).TrimEnd(); properties.Parameters = BinaryData.FromString(parametersPath); } - return new ArmDeploymentContent(properties); + var content = new ArmDeploymentContent(properties); + if (!string.IsNullOrWhiteSpace(location)) + { + content.Location = location; + } + + return content; } // These extension methods are wrapped to allow mocking in our tests diff --git a/engine/BenchPress.TestEngine/Services/DeploymentService.cs b/engine/BenchPress.TestEngine/Services/DeploymentService.cs index 7c745e2..48d9190 100644 --- a/engine/BenchPress.TestEngine/Services/DeploymentService.cs +++ b/engine/BenchPress.TestEngine/Services/DeploymentService.cs @@ -49,6 +49,41 @@ public class DeploymentService : Deployment.DeploymentBase } } + public override async Task DeploymentSubCreate(DeploymentSubRequest request, ServerCallContext context) + { + if (string.IsNullOrWhiteSpace(request.BicepFilePath) + || string.IsNullOrWhiteSpace(request.Location) + || string.IsNullOrWhiteSpace(request.SubscriptionNameOrId)) + { + return new DeploymentResult + { + Success = false, + ErrorMessage = $"One or more of the following required parameters was missing: {nameof(request.BicepFilePath)}, {nameof(request.Location)}, and {nameof(request.SubscriptionNameOrId)}" + }; + } + + try + { + var armTemplatePath = await bicepTranspileService.BuildAsync(request.BicepFilePath); + var deployment = await armDeploymentService.DeployArmToSubscriptionAsync(request.SubscriptionNameOrId, request.Location, armTemplatePath, request.ParameterFilePath); + var response = deployment.WaitForCompletionResponse(); + + return new DeploymentResult + { + Success = !response.IsError, + ErrorMessage = response.ReasonPhrase + }; + } + catch (Exception ex) + { + return new DeploymentResult + { + Success = false, + ErrorMessage = ex.Message + }; + } + } + public override async Task DeleteGroup(DeleteGroupRequest request, ServerCallContext context) { throw new NotImplementedException(); diff --git a/engine/BenchPress.TestEngine/Services/IArmDeploymentService.cs b/engine/BenchPress.TestEngine/Services/IArmDeploymentService.cs index 349d768..ae6be69 100644 --- a/engine/BenchPress.TestEngine/Services/IArmDeploymentService.cs +++ b/engine/BenchPress.TestEngine/Services/IArmDeploymentService.cs @@ -2,5 +2,5 @@ namespace BenchPress.TestEngine.Services; public interface IArmDeploymentService { Task> DeployArmToResourceGroupAsync(string subscriptionNameOrId, string resourceGroupName, string armTemplatePath, string? parametersPath = null, Azure.WaitUntil waitUtil = Azure.WaitUntil.Completed); - Task> DeployArmToSubscriptionAsync(string subscriptionNameOrId, string armTemplatePath, string? parametersPath = null, Azure.WaitUntil waitUtil = Azure.WaitUntil.Completed); + Task> DeployArmToSubscriptionAsync(string subscriptionNameOrId, string location, string armTemplatePath, string? parametersPath = null, Azure.WaitUntil waitUtil = Azure.WaitUntil.Completed); } diff --git a/protos/deployment.proto b/protos/deployment.proto index 625e760..f0a097f 100644 --- a/protos/deployment.proto +++ b/protos/deployment.proto @@ -8,6 +8,7 @@ option csharp_namespace = "BenchPress.TestEngine"; // Other scopes: subscription, management group, and tenant. service Deployment { rpc DeploymentGroupCreate (DeploymentGroupRequest) returns (DeploymentResult); + rpc DeploymentSubCreate (DeploymentSubRequest) returns (DeploymentResult); rpc DeleteGroup (DeleteGroupRequest) returns (DeploymentResult); } @@ -18,6 +19,13 @@ message DeploymentGroupRequest { string subscription_name_or_id = 4; } +message DeploymentSubRequest { + string bicep_file_path = 1; + string parameter_file_path = 2; + string location = 3; + string subscription_name_or_id = 4; +} + message DeleteGroupRequest { string resource_group_name = 1; string subscription_name_or_id = 2; From 6788879f35805c9221d310a409cd26ad7a6b4b07 Mon Sep 17 00:00:00 2001 From: jessica-ern <107070686+jessica-ern@users.noreply.github.com> Date: Thu, 10 Nov 2022 15:53:05 -0600 Subject: [PATCH 09/12] update the main README and add a Manually Testing the Test Engine doc (#27) * update the main README and add a Manually Testing the Test Engine doc * linting and spelling errors * Apply suggestions from code review Co-authored-by: Omeed Musavi * addressing PR comments * add sample code to samples Co-authored-by: Omeed Musavi --- README.md | 37 +++- config/megalinter/.cspell.json | 4 +- docs/images/architecture-diagram.png | Bin 0 -> 70438 bytes docs/manually_testing_the_test_engine.md | 199 ++++++++++++++++++ samples/manual-testers/dotnet/Program.cs | 17 ++ samples/manual-testers/dotnet/Tester.csproj | 23 ++ .../manual-testers/python/deployment_pb2.py | 32 +++ .../python/deployment_pb2_grpc.py | 105 +++++++++ samples/manual-testers/python/tester.py | 41 ++++ 9 files changed, 450 insertions(+), 8 deletions(-) create mode 100644 docs/images/architecture-diagram.png create mode 100644 docs/manually_testing_the_test_engine.md create mode 100644 samples/manual-testers/dotnet/Program.cs create mode 100644 samples/manual-testers/dotnet/Tester.csproj create mode 100644 samples/manual-testers/python/deployment_pb2.py create mode 100644 samples/manual-testers/python/deployment_pb2_grpc.py create mode 100644 samples/manual-testers/python/tester.py diff --git a/README.md b/README.md index 3776f3d..65e475a 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ # Bicep testing framework -This framework is intended to work as a testing framework for Azure deployment features by using [Bicep](https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/overview?tabs=bicep). +This is a testing framework for Azure deployments using [Bicep](https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/overview?tabs=bicep). -In order to see how you can work with this framework you can find one sample bicep file in the folder [samples](./samples/dotnet/samples/pwsh/resourceGroup.bicep) -that will be deployed by using one PowerShell script. +An example of how to use this framework can be found in the [Powershell Test Sample](docs/powershell_test_sample.md) guide. -Process is the following: +The process of the tests is the following: ```mermaid flowchart LR @@ -14,13 +13,37 @@ A[Creation] -->|Bicep| B[Verification] B --> C[Remove] ``` -**Creation**: New Features are gonna be deployed through Bicep files -**Verification**: Test is going to confirm the resource exists and also assert if it matches the expected value -**Remove**: Optionally resources can be removed after being tested +**Creation**: New Features are deployed using Bicep files +**Verification**: Tests confirm that the resource exists and that it matches the expected values +**Remove**: Optionally, resources can be removed after being tested + +## Benchpress Architecture + +BenchPress uses [gRPC](https://grpc.io/docs/what-is-grpc/introduction/) to create a multi-language testing framework. + +The BenchPress Test Engine is a C# gRPC Server located under `/engine/BenchPress.TestEngine`. The Test Engine is responsible +for the business logic of deploying Bicep files, obtaining information about deployed resources in Azure, and cleaning up the +deployment afterward. + +The BenchPress Test Frameworks (located under `/framework/`) are gRPC Clients of multiple languages. The Test Frameworks are +responsible interfacing between the user's tests (written in their chosen language) and the Test Engine. They are also +responsible for managing the life cycle of the Test Engine. + +gRPC uses [protocol buffers](https://developers.google.com/protocol-buffers/docs/overview). The `/protos/` folder contains BenchPress's .proto files. These define the API that the gRPC +Server and Clients use to communicate. + +![From left to right: 1). There is a Powershell Test Script and a Python Test Script. 2). The Powershell Test Script calls into a Powershell Test Framework. The Python Test Script calls into a Python Test Framework. 3) Both Test Frameworks call through a gRPC Boundary. 4) The gRPC Boundary wraps a language agnostic Test Engine. 5). The Test Engine calls into both the Bicep CLI and the Azure Resource Manager.](docs/images/architecture-diagram.png) ## Getting started + See [Getting Started](docs/getting_started.md) guide on how to start development on *Benchpress*. +## Deveplopment Tips + +See [Github Actions Lint Workflow](docs/github_actions_lint_workflow.md) for how to maintain the CI/CD pipeline. + +See [Manually Testing the Test Engine](docs/manually_testing_the_test_engine.md) for guidance on exploring the gRPC endpoints. + ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a diff --git a/config/megalinter/.cspell.json b/config/megalinter/.cspell.json index bed8f26..ff3f22c 100644 --- a/config/megalinter/.cspell.json +++ b/config/megalinter/.cspell.json @@ -30,7 +30,9 @@ "msrc", "Benchpress", "BenchPress", - "pwsh" + "pwsh", + "proto", + "protos" ], "version": "0.2", "patterns": [ diff --git a/docs/images/architecture-diagram.png b/docs/images/architecture-diagram.png new file mode 100644 index 0000000000000000000000000000000000000000..8e15b87ab65a7cc70ade189c88c6576fbb9f76e1 GIT binary patch literal 70438 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 + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + +``` + +... and the `.proto` files. Make sure they are marked as Client, not Server. + +```xml + + + + +``` + +In the `.cs` file that will be calling the Test Engine method, include the following `using` statements: + +```csharp +using Grpc.Net.Client; +using BenchPress.TestEngine; +``` + +Your editor's intellisense should now be able to pick up on the objects and interfaces defined in the .proto files. + +Start by creating the gRPC Channel using the port that the Test Engine is running on: + +```csharp +using var channel = GrpcChannel.ForAddress("http://localhost:5152"); +``` + +Then, create the client(s) that define the Test Engine method(s) you wish to call: + +```csharp +var deployClient = new Deployment.DeploymentClient(channel); +var rgClient = new ResourceGroup.ResourceGroupClient(channel); +``` + +Now you can build your Request objects... + +```csharp +var request = new DeploymentGroupRequest { + BicepFilePath = "main.bicep", + ParameterFilePath = "params.json", + ResourceGroupName = "rg-jsmith-benchpress-test", + SubscriptionNameOrId = "John-Smiths-Subscription" +}; +``` + +... and make calls to the Test Engine + +```csharp +var result = await deployClient.DeploymentGroupCreateAsync(request); +``` + +Altogether, you may have a simple console app `Program.cs` that looks like this: + +```csharp +using Grpc.Net.Client; +using BenchPress.TestEngine; + +using var channel = GrpcChannel.ForAddress("http://localhost:5152"); + var client = new Deployment.DeploymentClient(channel); + + var request = new DeploymentSubRequest { + BicepFilePath = "", + ParameterFilePath = "", + Location = "eastus", + SubscriptionNameOrId = "" + }; + + var result = await client.DeploymentSubCreateAsync(request); + + Console.WriteLine($"Success? {result.Success}"); + Console.WriteLine(result.ErrorMessage); +``` + +While the Test Engine is running in a separate terminal, run your Client code. Using this Client, you can manually test the Test Engine. + +### Python + +If needed, generate the `pb2` scripts for the `.proto` files you will be using. (They might already exist in under `/framework/python/src/benchpress`). + +```bash +python -m grpc_tools.protoc -I./protos --python_out=. --grpc_python_out=. ./protos/deployment.proto +``` + +The above example ran at the root of the project directory will create a `deployment_pb2.py` and `deployment_pb2_grpc.py` file. + +In the python script you will be using for your tests, include the following import statements (for whatever `pb2` files you are going to use): + +```python +import grpc +import deployment_pb2 +import deployment_pb2_grpc +import resource_group_pb2 +import resource_group_pb2_grpc +``` + +Start by creating the gRPC Channel using the port that the Test Engine is running on: + +```python +with grpc.insecure_channel('localhost:5152') as channel: +``` + +Then, create the client(s) that define the Test Engine method(s) you wish to call: + +```python +deployStub = deployment_pb2_grpc.DeploymentStub(channel) +rgStub = resource_group_pb2_grpc.ResourceGroupStub(channel) +``` + +Now you can build your Request objects... + +```python +req = deployment_pb2.DeploymentGroupRequest( + bicep_file_path = 'main.bicep', + parameter_file_path = 'params.json', + resource_group_name = 'rg-jsmith-benchpress-test', + subscription_name_or_id = 'John-Smiths-Subscription' + ) +``` + +... and make calls to the Test Engine + +```python +response = deployStub.DeploymentGroupCreate(req) +``` + +Altogether, you may have a python script like this: + +```python +from __future__ import print_function + +import logging + +import grpc +import deployment_pb2 +import deployment_pb2_grpc + + +def run(): + with grpc.insecure_channel('localhost:5152') as channel: + stub = deployment_pb2_grpc.DeploymentStub(channel) + req = deployment_pb2.DeploymentSubRequest( + bicep_file_path = '', + parameter_file_path = '', + location = 'eastus', + subscription_name_or_id = '' + ) + response = stub.DeploymentSubCreate(req) + print("Success? " + response.success) + print(response.error_message) + + +if __name__ == '__main__': + logging.basicConfig() + run() + +``` + +While the Test Engine is running in a separate terminal, run your Client script. Using this Client, you can manually test the Test Engine. \ No newline at end of file diff --git a/samples/manual-testers/dotnet/Program.cs b/samples/manual-testers/dotnet/Program.cs new file mode 100644 index 0000000..0d1fc34 --- /dev/null +++ b/samples/manual-testers/dotnet/Program.cs @@ -0,0 +1,17 @@ +using Grpc.Net.Client; +using BenchPress.TestEngine; + +using var channel = GrpcChannel.ForAddress("http://localhost:5152"); + var client = new Deployment.DeploymentClient(channel); + + var request = new DeploymentSubRequest { + BicepFilePath = "", + ParameterFilePath = "", + Location = "eastus", + SubscriptionNameOrId = "" + }; + + var result = await client.DeploymentSubCreateAsync(request); + + Console.WriteLine($"Success? {result.Success}"); + Console.WriteLine(result.ErrorMessage); \ No newline at end of file diff --git a/samples/manual-testers/dotnet/Tester.csproj b/samples/manual-testers/dotnet/Tester.csproj new file mode 100644 index 0000000..78c7ba7 --- /dev/null +++ b/samples/manual-testers/dotnet/Tester.csproj @@ -0,0 +1,23 @@ + + + + Exe + net6.0 + enable + enable + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + diff --git a/samples/manual-testers/python/deployment_pb2.py b/samples/manual-testers/python/deployment_pb2.py new file mode 100644 index 0000000..005ec6f --- /dev/null +++ b/samples/manual-testers/python/deployment_pb2.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: deployment.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10\x64\x65ployment.proto\x12\nbenchpress\"\x8c\x01\n\x16\x44\x65ploymentGroupRequest\x12\x17\n\x0f\x62icep_file_path\x18\x01 \x01(\t\x12\x1b\n\x13parameter_file_path\x18\x02 \x01(\t\x12\x1b\n\x13resource_group_name\x18\x03 \x01(\t\x12\x1f\n\x17subscription_name_or_id\x18\x04 \x01(\t\"R\n\x12\x44\x65leteGroupRequest\x12\x1b\n\x13resource_group_name\x18\x01 \x01(\t\x12\x1f\n\x17subscription_name_or_id\x18\x02 \x01(\t\":\n\x10\x44\x65ploymentResult\x12\x0f\n\x07success\x18\x01 \x01(\x08\x12\x15\n\rerror_message\x18\x02 \x01(\t2\xb4\x01\n\nDeployment\x12Y\n\x15\x44\x65ploymentGroupCreate\x12\".benchpress.DeploymentGroupRequest\x1a\x1c.benchpress.DeploymentResult\x12K\n\x0b\x44\x65leteGroup\x12\x1e.benchpress.DeleteGroupRequest\x1a\x1c.benchpress.DeploymentResultB\x18\xaa\x02\x15\x42\x65nchPress.TestEngineb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'deployment_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + DESCRIPTOR._serialized_options = b'\252\002\025BenchPress.TestEngine' + _DEPLOYMENTGROUPREQUEST._serialized_start=33 + _DEPLOYMENTGROUPREQUEST._serialized_end=173 + _DELETEGROUPREQUEST._serialized_start=175 + _DELETEGROUPREQUEST._serialized_end=257 + _DEPLOYMENTRESULT._serialized_start=259 + _DEPLOYMENTRESULT._serialized_end=317 + _DEPLOYMENT._serialized_start=320 + _DEPLOYMENT._serialized_end=500 +# @@protoc_insertion_point(module_scope) diff --git a/samples/manual-testers/python/deployment_pb2_grpc.py b/samples/manual-testers/python/deployment_pb2_grpc.py new file mode 100644 index 0000000..e655e41 --- /dev/null +++ b/samples/manual-testers/python/deployment_pb2_grpc.py @@ -0,0 +1,105 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +import deployment_pb2 as deployment__pb2 + + +class DeploymentStub(object): + """Currently only supports deployments with the target scope of resource group. + Other scopes: subscription, management group, and tenant. + """ + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.DeploymentGroupCreate = channel.unary_unary( + '/benchpress.Deployment/DeploymentGroupCreate', + request_serializer=deployment__pb2.DeploymentGroupRequest.SerializeToString, + response_deserializer=deployment__pb2.DeploymentResult.FromString, + ) + self.DeleteGroup = channel.unary_unary( + '/benchpress.Deployment/DeleteGroup', + request_serializer=deployment__pb2.DeleteGroupRequest.SerializeToString, + response_deserializer=deployment__pb2.DeploymentResult.FromString, + ) + + +class DeploymentServicer(object): + """Currently only supports deployments with the target scope of resource group. + Other scopes: subscription, management group, and tenant. + """ + + def DeploymentGroupCreate(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def DeleteGroup(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_DeploymentServicer_to_server(servicer, server): + rpc_method_handlers = { + 'DeploymentGroupCreate': grpc.unary_unary_rpc_method_handler( + servicer.DeploymentGroupCreate, + request_deserializer=deployment__pb2.DeploymentGroupRequest.FromString, + response_serializer=deployment__pb2.DeploymentResult.SerializeToString, + ), + 'DeleteGroup': grpc.unary_unary_rpc_method_handler( + servicer.DeleteGroup, + request_deserializer=deployment__pb2.DeleteGroupRequest.FromString, + response_serializer=deployment__pb2.DeploymentResult.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'benchpress.Deployment', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + + + # This class is part of an EXPERIMENTAL API. +class Deployment(object): + """Currently only supports deployments with the target scope of resource group. + Other scopes: subscription, management group, and tenant. + """ + + @staticmethod + def DeploymentGroupCreate(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/benchpress.Deployment/DeploymentGroupCreate', + deployment__pb2.DeploymentGroupRequest.SerializeToString, + deployment__pb2.DeploymentResult.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) + + @staticmethod + def DeleteGroup(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary(request, target, '/benchpress.Deployment/DeleteGroup', + deployment__pb2.DeleteGroupRequest.SerializeToString, + deployment__pb2.DeploymentResult.FromString, + options, channel_credentials, + insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/samples/manual-testers/python/tester.py b/samples/manual-testers/python/tester.py new file mode 100644 index 0000000..8e12bee --- /dev/null +++ b/samples/manual-testers/python/tester.py @@ -0,0 +1,41 @@ +# Copyright 2015 gRPC authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""The Python implementation of the GRPC helloworld.Greeter client.""" + +from __future__ import print_function + +import logging + +import grpc +import deployment_pb2 +import deployment_pb2_grpc + + +def run(): + print("Will try to greet world ...") + with grpc.insecure_channel('localhost:5152') as channel: + stub = deployment_pb2_grpc.DeploymentStub(channel) + req = deployment_pb2.DeploymentGroupRequest( + bicep_file_path = '/Users/jessicaern/Projects/benchpress-private/engine/BenchPress.TestEngine.Tests/SampleFiles/storageAccount.bicep', + resource_group_name = 'jern-benchpress-playground', + subscription_name_or_id = '519c3e33-0884-4604-bad7-6964e6ef55f8' + ) + response = stub.DeploymentGroupCreate(req) + print("Success? " + response.success) + print(response.error_message) + + +if __name__ == '__main__': + logging.basicConfig() + run() From 9b853fa996692510f7656f074e8181c4639580c6 Mon Sep 17 00:00:00 2001 From: Dilmurod Makhamadaliev <104784252+DilmurodMak@users.noreply.github.com> Date: Thu, 10 Nov 2022 18:19:40 -0500 Subject: [PATCH 10/12] FileSerice Wrapper - Integration with ArmDeploymentService and TranspileBicepSerivice (#26) * transpile bicep to arm template feature * bicep submodule restructured * rename the bicepSubmodule to BicepExecute * change method signature and make async * Renaming Bicep Service - main have existing BicepService.cs * additional tests added * corrections for feedbacks * actual manual deployment is tested * changed from method wrapping Bicep.Cli into direct calling Bicep.Cli.Programs.Main * fixing linting errors * additional mock test to throw exception and dotnet workflow update * linting fix * liniting fix * linting fix * linting fix * linting fix * linting fix * linting fix * linting fix * linting fix * linting fix * linting fix * linting fix * linting fix * come on now * come on now * come on now * adding submodule checkout * turn off dotnet build * file service interface * file service implementation * changes * file delete feature * file service implementation * file service implementation * tested locally * adding file service into transpile bicep * file service integration * integrate BicepTranspileService (#23) * fix build errors * fix linitn errors * corrections for feedbacks * parameter content * change async * FileNotFoundException test added * DeploymenServiceTests FileService setup * transpile bicep test mokc refactoring Co-authored-by: Jessica Ern Co-authored-by: jessica-ern <107070686+jessica-ern@users.noreply.github.com> --- .../ArmDeploymentServiceTests.cs | 21 ++++++---- .../BicepTranspileServiceTests.cs | 42 +++++++++++++------ engine/BenchPress.TestEngine/Program.cs | 1 + .../Services/ArmDeploymentService.cs | 28 ++++++++----- .../Services/BicepTranspileService.cs | 17 ++++++-- .../Services/FileService.cs | 26 ++++++++++++ .../Services/IFileService.cs | 10 +++++ .../BicepArmSamples}/params.json | 0 .../BicepArmSamples}/resourceGroup.bicep | 0 .../storage-account-needs-params.json | 0 .../BicepArmSamples}/storage-account.json | 0 .../BicepArmSamples}/storageAccount.bicep | 0 12 files changed, 109 insertions(+), 36 deletions(-) create mode 100644 engine/BenchPress.TestEngine/Services/FileService.cs create mode 100644 engine/BenchPress.TestEngine/Services/IFileService.cs rename {engine/BenchPress.TestEngine.Tests/SampleFiles => samples/BicepArmSamples}/params.json (100%) rename {engine/BenchPress.TestEngine.Tests/SampleFiles => samples/BicepArmSamples}/resourceGroup.bicep (100%) rename {engine/BenchPress.TestEngine.Tests/SampleFiles => samples/BicepArmSamples}/storage-account-needs-params.json (100%) rename {engine/BenchPress.TestEngine.Tests/SampleFiles => samples/BicepArmSamples}/storage-account.json (100%) rename {engine/BenchPress.TestEngine.Tests/SampleFiles => samples/BicepArmSamples}/storageAccount.bicep (100%) diff --git a/engine/BenchPress.TestEngine.Tests/ArmDeploymentServiceTests.cs b/engine/BenchPress.TestEngine.Tests/ArmDeploymentServiceTests.cs index e0b1154..69d257d 100644 --- a/engine/BenchPress.TestEngine.Tests/ArmDeploymentServiceTests.cs +++ b/engine/BenchPress.TestEngine.Tests/ArmDeploymentServiceTests.cs @@ -8,22 +8,25 @@ namespace BenchPress.TestEngine.Tests; public class ArmDeploymentServiceTests { private readonly ArmDeploymentService armDeploymentService; private readonly Mock armClientMock; + private readonly Mock fileServiceMock; private readonly Mock groupDeploymentsMock; private readonly Mock subscriptionDeploymentsMock; private const string validSubId = "a3a01f37-665c-4ee8-9bc3-3adf7ebcec0d"; private const string validRgName = "test-rg"; private const string validLocation = "eastus"; - private const string smapleFiles = "../../../SampleFiles"; - private const string standaloneTemplate = $"{smapleFiles}/storage-account.json"; - private const string templateWithParams = $"{smapleFiles}/storage-account-needs-params.json"; - private const string parameters = $"{smapleFiles}/params.json"; + private const string standaloneTemplate = "storage-account.json"; + private const string templateWithParams = "storage-account-needs-params.json"; + private const string parameters = "params.json"; public ArmDeploymentServiceTests() { armClientMock = new Mock(MockBehavior.Strict); + fileServiceMock = new Mock(MockBehavior.Strict); groupDeploymentsMock = new Mock(); subscriptionDeploymentsMock = new Mock(); - armDeploymentService = new TestArmDeploymentService(groupDeploymentsMock.Object, subscriptionDeploymentsMock.Object, armClientMock.Object); + armDeploymentService = new TestArmDeploymentService(groupDeploymentsMock.Object, subscriptionDeploymentsMock.Object, armClientMock.Object, fileServiceMock.Object); + fileServiceMock.Setup(fs => fs.ReadAllTextAsync(It.IsAny())).ThrowsAsync(new FileNotFoundException()); + fileServiceMock.Setup(fs => fs.ReadAllTextAsync(It.IsIn(new [] {standaloneTemplate, templateWithParams, parameters}))).ReturnsAsync(""); } [Fact] @@ -56,12 +59,12 @@ public class ArmDeploymentServiceTests { { var subMock = SetUpSubscriptionMock(validSubId); var rgMock = SetUpResourceGroupMock(subMock, validRgName); - var excepectedMessage = "Deployment template validation failed"; - SetUpDeploymentExceptionMock(groupDeploymentsMock, new RequestFailedException(excepectedMessage)); + var expectedMessage = "Deployment template validation failed"; + SetUpDeploymentExceptionMock(groupDeploymentsMock, new RequestFailedException(expectedMessage)); var ex = await Assert.ThrowsAsync( async () => await armDeploymentService.DeployArmToResourceGroupAsync(validSubId, validRgName, templateWithParams) ); - Assert.Equal(excepectedMessage, ex.Message); + Assert.Equal(expectedMessage, ex.Message); } [Fact] @@ -232,7 +235,7 @@ public class ArmDeploymentServiceTests { private readonly ArmDeploymentCollection rgDeploymentCollection; private readonly ArmDeploymentCollection subDeploymentCollection; - public TestArmDeploymentService(ArmDeploymentCollection rgDeploymentCollection, ArmDeploymentCollection subDeploymentCollection, ArmClient client) : base(client) + public TestArmDeploymentService(ArmDeploymentCollection rgDeploymentCollection, ArmDeploymentCollection subDeploymentCollection, ArmClient client, IFileService fileService) : base(client, fileService) { this.rgDeploymentCollection = rgDeploymentCollection; this.subDeploymentCollection = subDeploymentCollection; diff --git a/engine/BenchPress.TestEngine.Tests/BicepTranspileServiceTests.cs b/engine/BenchPress.TestEngine.Tests/BicepTranspileServiceTests.cs index 2654cce..e2d9add 100644 --- a/engine/BenchPress.TestEngine.Tests/BicepTranspileServiceTests.cs +++ b/engine/BenchPress.TestEngine.Tests/BicepTranspileServiceTests.cs @@ -3,13 +3,21 @@ namespace BenchPress.TestEngine.Tests; public class BicepTranspileServiceTests { private readonly Mock mockBicepSubmodule; + private readonly Mock mockFileService; private readonly BicepTranspileService bicepTranspileService; public BicepTranspileServiceTests() { mockBicepSubmodule = new Mock(); + mockFileService = new Mock(); var logger = Mock.Of>(); - bicepTranspileService = new BicepTranspileService(mockBicepSubmodule.Object, logger); + bicepTranspileService = new BicepTranspileService(mockBicepSubmodule.Object, logger, mockFileService.Object); + + mockBicepSubmodule.Setup(p => p.ExecuteCommandAsync(It.IsAny())).ReturnsAsync(0); + mockFileService.Setup(fs => fs.FileExists(It.IsAny())).Returns(true); + mockFileService.Setup(fs => fs.GetFileExtension(It.IsAny())).Returns(".bicep"); + mockFileService.Setup(fs => fs.ChangeFileExtension(It.IsAny(),It.IsAny())).Returns("a/b/c/test.json"); + } [Theory] @@ -18,7 +26,7 @@ public class BicepTranspileServiceTests [InlineData(" ")] public async Task Build_NullInputPath_Throws(string inputFile) { - mockBicepSubmodule.Setup(p => p.ExecuteCommandAsync(It.IsAny())).ReturnsAsync(0); + mockFileService.Setup(fs => fs.GetFileFullPath(It.IsAny())).Returns(inputFile); await Assert.ThrowsAsync(async () => await bicepTranspileService.BuildAsync(inputFile)); mockBicepSubmodule.Verify(p => p.ExecuteCommandAsync(It.IsAny()), Times.Never); @@ -28,13 +36,12 @@ public class BicepTranspileServiceTests [InlineData("a/b/c/test.bicep")] public async Task Build_GeneratedArmTemplateExist(string inputFile) { - mockBicepSubmodule.Setup(p => p.ExecuteCommandAsync(It.IsAny())).ReturnsAsync(0); - var inputBicepFilePath = Path.GetFullPath(inputFile); - var expectedPath = Path.GetFullPath(Path.ChangeExtension(inputBicepFilePath, ".json")); - var armPath = await bicepTranspileService.BuildAsync(inputBicepFilePath); - var args = new[] { "build", inputBicepFilePath, "--outfile", expectedPath }; + mockFileService.Setup(fs => fs.GetFileFullPath(It.IsAny())).Returns(inputFile); + var outFile = "a/b/c/test.json"; + var armPath = await bicepTranspileService.BuildAsync(inputFile); + var args = new[] { "build", inputFile, "--outfile", outFile }; - Assert.Equal(expectedPath, armPath); + Assert.Equal(outFile, armPath); mockBicepSubmodule.Verify(p => p.ExecuteCommandAsync(args), Times.Once); } @@ -42,10 +49,10 @@ public class BicepTranspileServiceTests [InlineData("a/b/c/test.txt")] public async Task Build_NonBicepFileInputPath_Throws(string inputFile) { - mockBicepSubmodule.Setup(p => p.ExecuteCommandAsync(It.IsAny())).ReturnsAsync(0); - var inputBicepFilePath = Path.GetFullPath(inputFile); + mockFileService.Setup(fs => fs.GetFileExtension(It.IsAny())).Returns(".txt"); + mockFileService.Setup(fs => fs.GetFileFullPath(It.IsAny())).Returns(inputFile); - await Assert.ThrowsAsync(async () => await bicepTranspileService.BuildAsync(inputBicepFilePath)); + await Assert.ThrowsAsync(async () => await bicepTranspileService.BuildAsync(inputFile)); } [Theory] @@ -53,8 +60,17 @@ public class BicepTranspileServiceTests public async Task Build_BicepModuleNotImplemented_Throws(string inputFile) { mockBicepSubmodule.Setup(p => p.ExecuteCommandAsync(It.IsAny())).ThrowsAsync(new ApplicationException("Bicep transpilation failed")); - var inputBicepFilePath = Path.GetFullPath(inputFile); + mockFileService.Setup(fs => fs.GetFileFullPath(It.IsAny())).Returns(inputFile); - await Assert.ThrowsAsync(async () => await bicepTranspileService.BuildAsync(inputBicepFilePath)); + await Assert.ThrowsAsync(async () => await bicepTranspileService.BuildAsync(inputFile)); + } + + [Theory] + [InlineData("a/b/c/test.bicep")] + public async Task Build_FileNotFoundException_Throws(string inputFile) + { + mockFileService.Setup(fs => fs.FileExists(It.IsAny())).Returns(false); + + await Assert.ThrowsAsync(async () => await bicepTranspileService.BuildAsync(inputFile)); } } diff --git a/engine/BenchPress.TestEngine/Program.cs b/engine/BenchPress.TestEngine/Program.cs index 0ead46f..1e34ebe 100644 --- a/engine/BenchPress.TestEngine/Program.cs +++ b/engine/BenchPress.TestEngine/Program.cs @@ -23,6 +23,7 @@ builder.Services.AddAzureClients(builder => { }); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); var app = builder.Build(); diff --git a/engine/BenchPress.TestEngine/Services/ArmDeploymentService.cs b/engine/BenchPress.TestEngine/Services/ArmDeploymentService.cs index 231eb90..338ed43 100644 --- a/engine/BenchPress.TestEngine/Services/ArmDeploymentService.cs +++ b/engine/BenchPress.TestEngine/Services/ArmDeploymentService.cs @@ -3,12 +3,16 @@ using Azure.ResourceManager.Resources.Models; namespace BenchPress.TestEngine.Services; -public class ArmDeploymentService : IArmDeploymentService { +public class ArmDeploymentService : IArmDeploymentService +{ private readonly ArmClient client; + private readonly IFileService fileService; private string NewDeploymentName { get { return $"benchpress-{Guid.NewGuid().ToString()}"; } } - public ArmDeploymentService(ArmClient client) { + public ArmDeploymentService(ArmClient client, IFileService fileService) + { this.client = client; + this.fileService = fileService; } public async Task> DeployArmToResourceGroupAsync(string subscriptionNameOrId, string resourceGroupName, string armTemplatePath, string? parametersPath = null, WaitUntil waitUntil = WaitUntil.Completed) @@ -28,23 +32,25 @@ public class ArmDeploymentService : IArmDeploymentService { return await CreateSubscriptionDeployment(sub, waitUtil, NewDeploymentName, deploymentContent); } - private void ValidateParameters(params string[] parameters) { - if(parameters.Any(s => string.IsNullOrWhiteSpace(s))) { + private void ValidateParameters(params string[] parameters) + { + if (parameters.Any(s => string.IsNullOrWhiteSpace(s))) + { throw new ArgumentException("One or more parameters were missing or empty"); } } private async Task CreateDeploymentContent(string armTemplatePath, string? parametersPath, string? location = null) { - var templateContent = (await File.ReadAllTextAsync(armTemplatePath)).TrimEnd(); + var templateContent = (await fileService.ReadAllTextAsync(armTemplatePath)).TrimEnd(); var properties = new ArmDeploymentProperties(ArmDeploymentMode.Incremental) { Template = BinaryData.FromString(templateContent) }; - + if (!string.IsNullOrWhiteSpace(parametersPath)) { - var paramteresContent = (await File.ReadAllTextAsync(parametersPath)).TrimEnd(); - properties.Parameters = BinaryData.FromString(parametersPath); + var parametersContent = (await fileService.ReadAllTextAsync(parametersPath)).TrimEnd(); + properties.Parameters = BinaryData.FromString(parametersContent); } var content = new ArmDeploymentContent(properties); @@ -57,11 +63,13 @@ public class ArmDeploymentService : IArmDeploymentService { } // These extension methods are wrapped to allow mocking in our tests - protected virtual async Task> CreateGroupDeployment(ResourceGroupResource rg, Azure.WaitUntil waitUntil, string deploymentName, ArmDeploymentContent deploymentContent) { + protected virtual async Task> CreateGroupDeployment(ResourceGroupResource rg, Azure.WaitUntil waitUntil, string deploymentName, ArmDeploymentContent deploymentContent) + { return await rg.GetArmDeployments().CreateOrUpdateAsync(waitUntil, deploymentName, deploymentContent); } - protected virtual async Task> CreateSubscriptionDeployment(SubscriptionResource sub, Azure.WaitUntil waitUntil, string deploymentName, ArmDeploymentContent deploymentContent) { + protected virtual async Task> CreateSubscriptionDeployment(SubscriptionResource sub, Azure.WaitUntil waitUntil, string deploymentName, ArmDeploymentContent deploymentContent) + { return await sub.GetArmDeployments().CreateOrUpdateAsync(waitUntil, deploymentName, deploymentContent); } } diff --git a/engine/BenchPress.TestEngine/Services/BicepTranspileService.cs b/engine/BenchPress.TestEngine/Services/BicepTranspileService.cs index fa8083b..30df14d 100644 --- a/engine/BenchPress.TestEngine/Services/BicepTranspileService.cs +++ b/engine/BenchPress.TestEngine/Services/BicepTranspileService.cs @@ -5,12 +5,14 @@ namespace BenchPress.TestEngine.Services; public class BicepTranspileService : IBicepTranspileService { private IBicepExecute bicepExecute; + private IFileService fileService; private readonly ILogger logger; - public BicepTranspileService(IBicepExecute bicepExecute, ILogger logger) + public BicepTranspileService(IBicepExecute bicepExecute, ILogger logger, IFileService fileService) { this.bicepExecute = bicepExecute; this.logger = logger; + this.fileService = fileService; } public async Task BuildAsync(string inputPath) @@ -20,13 +22,20 @@ public class BicepTranspileService : IBicepTranspileService throw new ArgumentNullException(nameof(inputPath)); } - if (Path.GetExtension(inputPath) != ".bicep") + if(!fileService.FileExists(inputPath)) + { + throw new FileNotFoundException(nameof(inputPath)); + } + + var extension = fileService.GetFileExtension(inputPath); + + if (extension != ".bicep") { throw new ArgumentException("Passed file is not a bicep file. File path: " + inputPath); } - inputPath = Path.GetFullPath(inputPath); - string outputPath = Path.ChangeExtension(inputPath, ".json"); + inputPath = fileService.GetFileFullPath(inputPath); + string outputPath = fileService.ChangeFileExtension(inputPath, ".json"); logger.LogInformation("Invoking Bicep Submodule"); try diff --git a/engine/BenchPress.TestEngine/Services/FileService.cs b/engine/BenchPress.TestEngine/Services/FileService.cs new file mode 100644 index 0000000..da01982 --- /dev/null +++ b/engine/BenchPress.TestEngine/Services/FileService.cs @@ -0,0 +1,26 @@ +namespace BenchPress.TestEngine.Services; + +public class FileService : IFileService +{ + public bool FileExists(string filePath) + { + FileInfo file = new FileInfo(filePath); + return file.Exists; + } + public string GetFileFullPath(string filePath) + { + return Path.GetFullPath(filePath); + } + public string GetFileExtension(string filePath) + { + return Path.GetExtension(filePath); + } + public string ChangeFileExtension(string filePath, string extension) + { + return Path.ChangeExtension(filePath, extension); + } + public async Task ReadAllTextAsync(string filePath) + { + return await File.ReadAllTextAsync(filePath); + } +} diff --git a/engine/BenchPress.TestEngine/Services/IFileService.cs b/engine/BenchPress.TestEngine/Services/IFileService.cs new file mode 100644 index 0000000..78509fb --- /dev/null +++ b/engine/BenchPress.TestEngine/Services/IFileService.cs @@ -0,0 +1,10 @@ +namespace BenchPress.TestEngine.Services; + +public interface IFileService +{ + public bool FileExists(string filePath); + string GetFileFullPath(string filePath); + string ChangeFileExtension(string filePath, string extension); + string GetFileExtension(string filePath); + Task ReadAllTextAsync(string filePath); +} diff --git a/engine/BenchPress.TestEngine.Tests/SampleFiles/params.json b/samples/BicepArmSamples/params.json similarity index 100% rename from engine/BenchPress.TestEngine.Tests/SampleFiles/params.json rename to samples/BicepArmSamples/params.json diff --git a/engine/BenchPress.TestEngine.Tests/SampleFiles/resourceGroup.bicep b/samples/BicepArmSamples/resourceGroup.bicep similarity index 100% rename from engine/BenchPress.TestEngine.Tests/SampleFiles/resourceGroup.bicep rename to samples/BicepArmSamples/resourceGroup.bicep diff --git a/engine/BenchPress.TestEngine.Tests/SampleFiles/storage-account-needs-params.json b/samples/BicepArmSamples/storage-account-needs-params.json similarity index 100% rename from engine/BenchPress.TestEngine.Tests/SampleFiles/storage-account-needs-params.json rename to samples/BicepArmSamples/storage-account-needs-params.json diff --git a/engine/BenchPress.TestEngine.Tests/SampleFiles/storage-account.json b/samples/BicepArmSamples/storage-account.json similarity index 100% rename from engine/BenchPress.TestEngine.Tests/SampleFiles/storage-account.json rename to samples/BicepArmSamples/storage-account.json diff --git a/engine/BenchPress.TestEngine.Tests/SampleFiles/storageAccount.bicep b/samples/BicepArmSamples/storageAccount.bicep similarity index 100% rename from engine/BenchPress.TestEngine.Tests/SampleFiles/storageAccount.bicep rename to samples/BicepArmSamples/storageAccount.bicep From 479f542ea67684cc852929c782a402e418718517 Mon Sep 17 00:00:00 2001 From: Dilmurod Makhamadaliev <104784252+DilmurodMak@users.noreply.github.com> Date: Fri, 11 Nov 2022 10:59:27 -0500 Subject: [PATCH 11/12] PowerShell Test / GitHub Actions Workflow Docs Update (#29) * integrate BicepTranspileService (#23) * doc update * small changes * FileNotFoundException test added * transpile bicep test mokc refactoring Co-authored-by: jessica-ern <107070686+jessica-ern@users.noreply.github.com> --- docs/github_actions_lint_workflow.md | 77 +++++++++---------- docs/powershell_test_sample.md | 32 ++++++-- .../DeploymentServiceTests.cs | 13 ++-- 3 files changed, 67 insertions(+), 55 deletions(-) diff --git a/docs/github_actions_lint_workflow.md b/docs/github_actions_lint_workflow.md index cc02db3..a544d75 100755 --- a/docs/github_actions_lint_workflow.md +++ b/docs/github_actions_lint_workflow.md @@ -1,6 +1,6 @@ # Github Actions Lint Workflow -Current repo has three linting workflows in the `.github/workflows`. Each workflow uses specific Megalinter flavor to lint specific folders when new code gets pushed. All configuration files are located under `config/megalinter` directory +Current repo has three linting workflows in the `.github/workflows` directory. Each workflow uses specific Megalinter flavor to lint specific folders when new code gets pushed. All configuration files are located under `config/megalinter` directory - pr_dotnet - uses `oxsecurity/megalinter/flavors/dotnet@v6.12.0` to lint dotnet code in the `framework/` and `samples/` directories - pr_python - uses `oxsecurity/megalinter/flavors/python@v6.12.0` to lint python code in the `framework/` and `samples/` directories @@ -29,25 +29,44 @@ Rather than having to commit/push every time you want to test out the changes yo This is an example of running `.github/workflows/pr_dotnet.yml` file to lint dotnet specific folders locally using `act` command +Run the workflow locally using `act` command + +```sh +act pull_request --workflows .\.github\workflows\pr_docs.yaml +``` + ```yaml name: pr_dotnet on: pull_request: - paths-ignore: + paths: - "framework/dotnet/**" - "samples/dotnet/**" + - "engine/**" + - "protos/**" + - ".github/workflows/pr_dotnet.yaml" branches: - main + +env: + DOTNET_VERSION: '6.0.x' + jobs: lint: - runs-on: ubuntu-latest + name: lint-${{matrix.os}} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ ubuntu-latest ] steps: - - name: Checkout code + - name: Checkout repository and submodules uses: actions/checkout@v3 with: token: ${{ secrets.PAT || secrets.GITHUB_TOKEN }} + fetch-depth: 0 + submodules: recursive - name: MegaLinter dotnet flavor uses: oxsecurity/megalinter/flavors/dotnet@v6.12.0 @@ -57,42 +76,18 @@ jobs: PRINT_ALL_FILES: true DISABLE: SPELL,COPYPASTE,YAML DISABLE_LINTERS: REPOSITORY_CHECKOV,REPOSITORY_TRIVY - FILTER_REGEX_EXCLUDE: '(BenchPress/|engine/|examples/|/docs|\.github/workflows|\.devcontainer|\.editorconfig|\.gitmodules|\.sln|\.md|LICENSE|/framework/python|samples/python)' - FILTER_REGEX_INCLUDE: "(framework/dotnet|samples/dotnet)" + FILTER_REGEX_INCLUDE: '(framework/dotnet|samples/dotnet|engine/)' + FILTER_REGEX_EXCLUDE: '(examples/|/docs|\.devcontainer|\.editorconfig|\.gitmodules|\.sln|\.md|LICENSE|/framework/python|samples/python)' REPORT_OUTPUT_FOLDER: ${GITHUB_WORKSPACE}/megalinter-reports -``` - -Run workflow using `act` command - -```sh -act pull_request --workflows .\.github\workflows\pr_docs.yaml -``` - -#### Linting overview - -When pull_request is created, following directories gets skipped from linting to allow the merge - -```yaml -pull_request: - paths-ignore: - - "framework/dotnet/**" - - "samples/dotnet/**" - branches: - - main -``` - -In this pr_dotnet workflow, we are using Megalinter flavor for dotnet - -```yaml -- name: MegaLinter dotnet flavor - uses: oxsecurity/megalinter/flavors/dotnet@v6.12.0 -``` - -Finally, using Megalinter env variables to exclude and include directories for linting, and disable linters for this pr_dotnet - -```yaml -env: - DISABLE_LINTERS: REPOSITORY_CHECKOV,REPOSITORY_TRIVY - FILTER_REGEX_EXCLUDE: '(BenchPress/|engine/|examples/|/docs|\.github/workflows|\.devcontainer|\.editorconfig|\.gitmodules|\.sln|\.md|LICENSE|/framework/python|samples/python)' - FILTER_REGEX_INCLUDE: "(framework/dotnet|samples/dotnet)" + + - name: Setup dotnet + uses: actions/setup-dotnet@v3 + with: + dotnet-version: ${{ env.DOTNET_VERSION }} + + - name: Install dependencies + run: dotnet restore + + - name: Build + run: dotnet build --configuration Release --no-restore ``` diff --git a/docs/powershell_test_sample.md b/docs/powershell_test_sample.md index c15c5ba..80b84b9 100644 --- a/docs/powershell_test_sample.md +++ b/docs/powershell_test_sample.md @@ -1,19 +1,38 @@ -# How to test PowerShell Test Sample Code +# PowerShell How To Test Example -Following instruction shows how to run StorageAccount.Tests.ps1 file +Following instructions shows how to run StorageAccount.Tests.ps1 test. -File is located under following directory: `/samples/dotnet/samples/pwsh/Test` +`StorageAccount.Test.ps1`: -Login to Azure Subscription `Connect-AzAccount` -if you are using docker container `Connect-AzAccount -UseDeviceAuthentication` and follow additional login instruction on the terminal +- Deploys Azure resources to the Azure Cloud +- tests deployed resource properties +- Finally deletes deployed resources. -Run the test `.\StorageAccount.Tests.ps1` +## Prerequisites + +- Azure Subscription +- Azure CLI +- Pester [Pester Install Guide](https://pester.dev/docs/introduction/installation) +- Optional: Docker + +Test files are located under the following directory: `/samples/dotnet/samples/pwsh/Test` + +## Step by Step Guide + +### Auth to Azure + +Login to Azure Subscription `Connect-AzAccount` from Azure CLI +if you are using docker container `Connect-AzAccount -UseDeviceAuthentication` and follow additional login instructions prompted by terminal + +### Running the Test Storage Account deployment uses two bicep files: - `storageAccountDeploy.bicep` - to deploy resource group and storage account module. This is an actual deployment file - `storageAccount.bicep` - storage account bicep file which is called by `storageAccountDeploy.bicep` as a module file +To run the test simply navigate to the `StorageAccount.Tests.ps1` in CLI and execute the PowerShell test file `.\StorageAccount.Tests.ps1` + `StorageAccount.Tests.ps1` - Content. ```powershell @@ -79,5 +98,4 @@ Describe 'Spin up , Tear down Storage Account' { Remove-BicepFeature $resourceGroupName } } -#EOF ``` diff --git a/engine/BenchPress.TestEngine.Tests/DeploymentServiceTests.cs b/engine/BenchPress.TestEngine.Tests/DeploymentServiceTests.cs index f428a12..4826264 100644 --- a/engine/BenchPress.TestEngine.Tests/DeploymentServiceTests.cs +++ b/engine/BenchPress.TestEngine.Tests/DeploymentServiceTests.cs @@ -224,7 +224,7 @@ public class DeploymentServiceTests .ReturnsAsync(operation); } - private void SetUpFailedGroupDeployment(string reason) + private void SetUpFailedGroupDeployment(string reason) { var operation = SetupDeploymentOperation(false, reason); armDeploymentMock.Setup(x => x.DeployArmToResourceGroupAsync( @@ -244,11 +244,10 @@ public class DeploymentServiceTests It.IsAny(), It.IsAny(), It.IsAny())) - .ThrowsAsync(ex); + .ThrowsAsync(ex); } - private void VerifyGroupDeployment(DeploymentGroupRequest request, string templatePath) - { + private void VerifyGroupDeployment(DeploymentGroupRequest request, string templatePath) { armDeploymentMock.Verify(x => x.DeployArmToResourceGroupAsync( request.SubscriptionNameOrId, request.ResourceGroupName, @@ -270,7 +269,7 @@ public class DeploymentServiceTests .ReturnsAsync(operation); } - private void SetUpFailedSubDeployment(string reason) + private void SetUpFailedSubDeployment(string reason) { var operation = SetupDeploymentOperation(false, reason); armDeploymentMock.Setup(x => x.DeployArmToSubscriptionAsync( @@ -290,7 +289,7 @@ public class DeploymentServiceTests It.IsAny(), It.IsAny(), It.IsAny())) - .ThrowsAsync(ex); + .ThrowsAsync(ex); } private void VerifySubDeployment(DeploymentSubRequest request, string templatePath) @@ -322,4 +321,4 @@ public class DeploymentServiceTests It.IsAny()), Times.Never); } -} \ No newline at end of file +} From e6e2ca23bff4787e89c9603a30bc40c89d637132 Mon Sep 17 00:00:00 2001 From: Robert David Hernandez Date: Fri, 11 Nov 2022 15:00:11 -0600 Subject: [PATCH 12/12] spike/engine lifecycle (#28) * add Mermaid formatted sequence diagram * add draft engine interfaces and sequence diagram * A post-create script that gets executed when the container first launches * Example of using Aspect-oriented programming to start test engine * wired up engine lifecycle manager * reformat to C# bracket style * add additional lifecycle hooks for more granualarity * Create StateMachineExample.cs * WIP state machine * Lifecycle diagram * API, example test, state diagram. * close to done on example code diagram and doc * More examples and text updates * Update engine_lifecycle_spike.md * minor refactoring * minor refactorings * minor refactorings * minor refactorings, add awaits * update markdown doc and minor refactoring to example code * remove unused code * update doc * Make sure code compiles. Minor updates to wording * refactoring and aligning doc to code * delete duplicate code in /sequence_diagram.md * delete unused code * update casing to match convention * comment strings added to design class * remove unused code, rename main * move Attributes folder to designs Co-authored-by: Uffaz Co-authored-by: Uffaz Nathaniel --- .devcontainer/Dockerfile | 3 +- designs/Attributes/BenchpressTestAttribute.cs | 4 + designs/Attributes/OnDoneAttribute.cs | 4 + .../OnEngineStartFailureAttribute.cs | 4 + .../OnEngineStartSuccessAttribute.cs | 4 + .../Attributes/OnInitializationAttribute.cs | 5 + designs/Attributes/OnShutdownAttribute.cs | 4 + designs/Attributes/OnTestExecuteAttribute.cs | 4 + designs/BenchPress.cs | 238 ++++++++++++++++++ designs/IAllocateResourceGroup.cs | 3 + designs/ICreateResource.cs | 3 + designs/ILifecycleManager.cs | 6 + designs/IValidateResource.cs | 3 + designs/README.md | 188 ++++++++++++++ 14 files changed, 471 insertions(+), 2 deletions(-) create mode 100644 designs/Attributes/BenchpressTestAttribute.cs create mode 100644 designs/Attributes/OnDoneAttribute.cs create mode 100644 designs/Attributes/OnEngineStartFailureAttribute.cs create mode 100644 designs/Attributes/OnEngineStartSuccessAttribute.cs create mode 100644 designs/Attributes/OnInitializationAttribute.cs create mode 100644 designs/Attributes/OnShutdownAttribute.cs create mode 100644 designs/Attributes/OnTestExecuteAttribute.cs create mode 100644 designs/BenchPress.cs create mode 100644 designs/IAllocateResourceGroup.cs create mode 100644 designs/ICreateResource.cs create mode 100644 designs/ILifecycleManager.cs create mode 100644 designs/IValidateResource.cs create mode 100644 designs/README.md diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 4007ef6..4fb73f5 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -3,5 +3,4 @@ ARG VARIANT="6.0" FROM mcr.microsoft.com/vscode/devcontainers/dotnet:${VARIANT} COPY ./scripts/post-create.sh /benchpress/ -RUN chmod +x /benchpress/post-create.sh - +RUN chmod +x /benchpress/post-create.sh \ No newline at end of file diff --git a/designs/Attributes/BenchpressTestAttribute.cs b/designs/Attributes/BenchpressTestAttribute.cs new file mode 100644 index 0000000..fa05632 --- /dev/null +++ b/designs/Attributes/BenchpressTestAttribute.cs @@ -0,0 +1,4 @@ +public class BenchpressTestAttribute : Attribute +{ + public BenchpressTestAttribute() { } +} \ No newline at end of file diff --git a/designs/Attributes/OnDoneAttribute.cs b/designs/Attributes/OnDoneAttribute.cs new file mode 100644 index 0000000..934fbe1 --- /dev/null +++ b/designs/Attributes/OnDoneAttribute.cs @@ -0,0 +1,4 @@ +public class OnDoneAttribute : Attribute +{ + public OnDoneAttribute() { } +} \ No newline at end of file diff --git a/designs/Attributes/OnEngineStartFailureAttribute.cs b/designs/Attributes/OnEngineStartFailureAttribute.cs new file mode 100644 index 0000000..30b355e --- /dev/null +++ b/designs/Attributes/OnEngineStartFailureAttribute.cs @@ -0,0 +1,4 @@ +public class OnEngineStartFailureAttribute : Attribute +{ + public OnEngineStartFailureAttribute() { } +} \ No newline at end of file diff --git a/designs/Attributes/OnEngineStartSuccessAttribute.cs b/designs/Attributes/OnEngineStartSuccessAttribute.cs new file mode 100644 index 0000000..5ed99f4 --- /dev/null +++ b/designs/Attributes/OnEngineStartSuccessAttribute.cs @@ -0,0 +1,4 @@ +public class OnEngineStartSuccessAttribute : Attribute +{ + public OnEngineStartSuccessAttribute() { } +} \ No newline at end of file diff --git a/designs/Attributes/OnInitializationAttribute.cs b/designs/Attributes/OnInitializationAttribute.cs new file mode 100644 index 0000000..efeda61 --- /dev/null +++ b/designs/Attributes/OnInitializationAttribute.cs @@ -0,0 +1,5 @@ + +public class OnInitizationAttribute : Attribute +{ + public OnInitizationAttribute() { } +} \ No newline at end of file diff --git a/designs/Attributes/OnShutdownAttribute.cs b/designs/Attributes/OnShutdownAttribute.cs new file mode 100644 index 0000000..364b19f --- /dev/null +++ b/designs/Attributes/OnShutdownAttribute.cs @@ -0,0 +1,4 @@ +public class OnShutdownAttribute : Attribute +{ + public OnShutdownAttribute() { } +} \ No newline at end of file diff --git a/designs/Attributes/OnTestExecuteAttribute.cs b/designs/Attributes/OnTestExecuteAttribute.cs new file mode 100644 index 0000000..aeb1d6c --- /dev/null +++ b/designs/Attributes/OnTestExecuteAttribute.cs @@ -0,0 +1,4 @@ +public class OnTestExecuteAttribute : Attribute +{ + public OnTestExecuteAttribute() { } +} \ No newline at end of file diff --git a/designs/BenchPress.cs b/designs/BenchPress.cs new file mode 100644 index 0000000..c0f76bc --- /dev/null +++ b/designs/BenchPress.cs @@ -0,0 +1,238 @@ +using System.Reflection; + +public enum State +{ + PreInitialization, + Initialization, + EngineStarting, + EngineStartSuccess, + EngineStartFailure, + TestExecute, + Shutdown, + EngineShutdownSuccess, + Done +} + +// The choice of name of the class is not significant and is subject +// to change by the desginer. +// +// At time of writing, this class is not intended to be in a +// compilable or executable state. +// +// The objective of this class design artifact is serve as a guide +// for a developer to programatically manage the lifecycle of the +// BenchPress Engine and the State Machine it will implement. +public class BenchPress : ILifecycleManager +{ + private static readonly object lockObj = new object(); + private static BenchPress? instance = null; + + private const int MaxRestart = 2; + + public int EnginePID { get; private set; } = -1; + + public State CurrentState { get; private set; } = State.PreInitialization; + + public static BenchPress Instance + { + get + { + if (instance == null) + { + lock (lockObj) + { + instance = new BenchPress(); + } + } + return instance; + } + } + + private BenchPress() + { + TransitionToNextState(State.Initialization); + } + + private void Process() + { + switch (CurrentState) + { + case State.Initialization: Init(); break; + case State.EngineStarting: + PreEngineStart(); + StartEngine(); + break; + case State.EngineStartSuccess: OnEngineStartSuccess(); break; + case State.EngineStartFailure: OnEngineStartFailure(); break; + case State.TestExecute: OnTestExecute(); break; + case State.Shutdown: StopEngine(); break; + case State.EngineShutdownSuccess: Teardown(); break; + case State.Done: Done(); break; + } + } + + private void TransitionToNextState(State nextState) + { + if (nextState != CurrentState) + { + CurrentState = nextState; + Process(); + } + } + + public void Init() + { + TransitionToNextState(State.EngineStarting); + } + + public void PreEngineStart(int retryCount=3, int httpTimeout=60000, bool keepAlive=true) { } + + public async void StartEngine() + { + var isStarted = false; + try + { + Monitor.Enter(lockObj); + try + { + var restartCount = 0; + + while (!isStarted && restartCount < MAX_RESTART) + { + restartCount++; + + int enginePID = await MockStartProcess(); + if (enginePID > 0) + { + isStarted = true; + } + } + } + finally + { + Monitor.Exit(lockObj); + } + } + catch (SynchronizationLockException SyncEx) + { + Console.WriteLine("A SynchronizationLockException occurred. Message: "); + Console.WriteLine(SyncEx.Message); + } + + if (isStarted) + { + TransitionToNextState(State.EngineStartSuccess); + } + else + { + TransitionToNextState(State.EngineStartFailure); + } + } + + private void OnEngineStartSuccess() + { + TransitionToNextState(State.TestExecute); + } + + private void OnEngineStartFailure() + { + TransitionToNextState(State.Shutdown); + } + + private void PreTestExecute() { } + + private void OnTestExecute() + { + var testMethods = GetMethodsWithAttribute(typeof(BenchpressTestAttribute)); + + // Invoke the test + testMethods.ToList().ForEach(method => + { + InvokeTest(method); + }); + + TransitionToNextState(State.Shutdown); + } + + public async void StopEngine() + { + try + { + Monitor.Enter(lockObj); + try + { + int exitCode = await MockStopProcess(EnginePID); + } + finally + { + Monitor.Exit(lockObj); + } + } + catch (SynchronizationLockException SyncEx) + { + Console.WriteLine("A SynchronizationLockException occurred. Message: "); + Console.WriteLine(SyncEx.Message); + } + TransitionToNextState(State.EngineShutdownSuccess); + } + + private void Teardown() + { + InvokeMethodsMarkedWithAttribute(typeof(OnShutdownAttribute)); + TransitionToNextState(State.Done); + } + + private void Done() { } + + private void InvokeTest(MethodInfo method) + { + // pre + //InvokeMethodsMarkedWithAttribute(BenchPress.Attributes.PreTestExecute); + + // execute + //InvokeMethodsMarkedWithAttribute(BenchPress.Attributes.Test); + + // post + InvokeMethodsMarkedWithAttribute(typeof(OnTestExecuteAttribute)); + } + + private MethodInfo[] GetMethodsWithAttribute(params Type[] attributes) + { + return new MethodInfo[] { }; + } + + private void InvokeMethodsMarkedWithAttribute(params Type[] attributes) + { + /* logic */ + /*foreach (Attribute attr in attributes) + { + foreach (MethodInfo method in attr) + { + if (HasAttribute(method, attr)) + { + method.Invoke(); + } + } + }*/ + } + + private async Task MockStartProcess() + { + int PID = -1; + await Task.Run(() => + { + PID = 1; + }); + return PID; + } + + private async Task MockStopProcess(int PID) + { + int EXIT_CODE = -1; + await Task.Run(() => + { + EXIT_CODE = 0; + }); + return EXIT_CODE; + } +} diff --git a/designs/IAllocateResourceGroup.cs b/designs/IAllocateResourceGroup.cs new file mode 100644 index 0000000..c4c92ad --- /dev/null +++ b/designs/IAllocateResourceGroup.cs @@ -0,0 +1,3 @@ +interface IAllocateResourceGroup { + bool CreateResourceGroup(String AzureSubscritionId); +} \ No newline at end of file diff --git a/designs/ICreateResource.cs b/designs/ICreateResource.cs new file mode 100644 index 0000000..b8c31be --- /dev/null +++ b/designs/ICreateResource.cs @@ -0,0 +1,3 @@ +interface ICreateResource { + bool CreateResources(String resourceGroupName, List resources); +} \ No newline at end of file diff --git a/designs/ILifecycleManager.cs b/designs/ILifecycleManager.cs new file mode 100644 index 0000000..cea2460 --- /dev/null +++ b/designs/ILifecycleManager.cs @@ -0,0 +1,6 @@ +interface ILifecycleManager { + void Init(); + void PreEngineStart(int retryCount=3, int httpTimeout=60000, bool keepAlive=true); + void StartEngine(); + void StopEngine(); +} diff --git a/designs/IValidateResource.cs b/designs/IValidateResource.cs new file mode 100644 index 0000000..599aff4 --- /dev/null +++ b/designs/IValidateResource.cs @@ -0,0 +1,3 @@ +interface IValidateResource { + bool ValidateResoure(String validStateJson); +} \ No newline at end of file diff --git a/designs/README.md b/designs/README.md new file mode 100644 index 0000000..31929b8 --- /dev/null +++ b/designs/README.md @@ -0,0 +1,188 @@ +# Benchpress Testing Framework Design + +## Overview +The core Benchpress engine proposes an *Inversion of Control* (IoC) design paradigm. In this design, we transfer the control of objects or portions of a program to a container or framework for orchestrating and execution. + +Few advantages of IoC design is that it enables test writers to use any language-specific framework of their choice. For example, in C# *NUnit*, *XUnit*, and *MSTest* are some of the most popular choices. + +The primary goal of the Benchpress testing framework is to start and stop the *Core testing Engine* in a thread-safe manner, while also supporting injecting optional runtime configuration into the engine. + +Some examples of runtime configuration that the framework may provide to the engine include: + - Number of automatic restarts due to exception conditions the engine should undergo before reporting a fatal error to the framework. + - Number of automatic retries should the engine make when interacting over the network before reporting a fatal error to the framework. + - How long of a time period should the engine wait during network interactions before entering a timeout condition and reporting a fatal error to the framework. + +Our API allows test authors to include declarative decorators/annotations/attributes on their test methods and/or test classes that will intercept the flow-of-control at runtime to execute the various pre/post lifecycle management steps - including ensuring an engine process instance is available - allowing for massively concurrent test execution. + +## State Diagram + +Internally the testing framework has a state machine that allows it to track and execute events and test methods. + +```mermaid +flowchart TD + PreInitialization --> Initialization + Initialization --> EngineStarting + EngineStarting -->|Success| EngineStartSuccess + EngineStarting -->|Failure| EngineStartFailure + EngineStartSuccess --> TestExecute + TestExecute -->|1...n| TestExecute + EngineStartFailure --> ShutDown + TestExecute --> ShutDown + ShutDown --> EngineShutdownSuccess + EngineShutdownSuccess --> Done +``` + +## Public API + +The public API **only** needs to be called in frameworks/languages where IoC is not possible, e.g., Powershell/Pester. In such cases, the expectation is that users will call `Start()` to configure the testing enviornment and on test teardown call `Stop()`. + +- `Init()` - Init singleton instance +- `PreEngineStart` - Optional Configuration +- `StartEngine()` - Start the engine +- `StopEngine()` - Stop the engine + +## Example Test + +### Method-level +```c# +class Test1 +{ + // Annotate at method level + [BenchpressTest] + public void TestRG() + { + var result = Benchpress.DoesResourceGroupExist("my-rg"); + Debug.Assert(result, true); + } + + // Annotate at method level + [BenchpressTest] + public void Test_ResourceGroupExists() + { + var result = Benchpress.DoesResourceGroupExist("my-rg"); + Debug.Assert(result, true); + } +} +``` + +### Class-level +```c# +// Annotate at class level +[BenchpressTest] +class Test2 +{ + public void TestRG() + { + var result = Benchpress.DoesResourceGroupExist("my-rg"); + Debug.Assert(result, true); + } + + public void Test_ResourceGroupExists() + { + var result = Benchpress.DoesResourceGroupExist("my-rg"); + Debug.Assert(result, true); + } +} +``` + +### Subscribe to life-cycle events +```c# +// Listen to life-cycle events +[BenchpressTest] +class Test3 +{ + [Initization] + public void OnInitization() { /* ... */ } + + [EngineStartSuccess] + public void OnEngineStartSuccess() { /* ... */ } + + [EngineStartFailure] + public void OnEngineStartFailure() { /* ... */ } + + [TestExecute] + public void OnTestExecute(MethodInfo method) { /* ... */ } + + [Shutdown] + public void OnShutdown() { /* ... */ } + + [Done] + public void OnDone() { /* ... */ } + + [Fact] + public void TestResourceGroupExists() + { + var result = Benchpress.DoesStorageAccountExists("mystorage"); + Debug.Assert(result == true, "Test failed"); + } + + [Fact] + public void TestStorageAccountPolicy() + { + var result = Benchpress.DoesStorageAccountExists("mystorage--222"); + Debug.Assert(result == true, "Test failed"); + } +} +``` + +### Lifecycle Event Descriptions +``` +Init() // Called once at the very start of test execution before the engine is started. This method can be used to perform or pre-execution steps. + +PreEngineStart(RetryCount = 3, HttpTimeout = 60000, KeepAlive = true, ...more configuration) – Called once to start the engine if not already started + +StartEngine() // Performs resource locking and starts the engine + +OnEngineStartSuccess() // Called once if the engine is started successfully + +OnEngineStartFailure() // Called once the engine is started successfully. + +PreTestExecute(method) // Called once before each test executes. + +OnTestExecute(method) // Called once after the test executes. + +PreTestExecute(method) // Called once before each test executes. + +StopEngine() // Performs resource locking and stops the engine + +TearDown() // Called once after the engine is stopped and before Done() is called + +Done() // Called at the end once of framework +``` + +### Proposed Sequence Diagram + +```mermaid +sequenceDiagram +Runtime/OS->>+Engine: main([...args]) +Engine->Engine: Init +Engine->Engine: If RestartCount>2 then Destroy +Engine->Engine: Setup +par Engine Ready State. ASync Comms allowed + Runtime/OS->>+Engine: Shutdown Signal + Engine->Engine: Destroy + Engine-->>-Runtime/OS: $EXIT_CODE +and + Engine->Engine: Exceptional Condition + Engine->Engine: Teardown +and + Framework->>+Engine: Create Resource Group given $AZURE_SUBSCRIPTION_ID + Engine->>+Azure: Create Resource Group + Azure-->>-Engine: + Engine->>-Framework: Resource Group Creation Status +and + Framework->>+Engine: Create Resource(s) given $RESOURCE_GROUP_NAME + Engine->>+Azure: Create Resource(s) + Azure-->>-Engine: + Engine-->>-Framework: +and + Framework->>+Engine: Validate Resource(s) given $VALID_STAE + Engine->>+Azure: Get Resource(s) State(s) + Azure-->>-Engine: + Engine-->>-Framework: +end +Engine->Engine: Teardown +Engine->Engine: If AutoRestart==True then Setup +Engine->Engine: Destroy +Engine-->>-Runtime/OS: $EXIT_CODE +```