From c6a3e4994e2b358c0ed11dbb13023705ffecf3e2 Mon Sep 17 00:00:00 2001 From: Sorra Date: Wed, 18 May 2022 15:23:12 -0700 Subject: [PATCH 001/226] Hello World created from GitHub --- .github/workflows/helloWorld.yml | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/helloWorld.yml diff --git a/.github/workflows/helloWorld.yml b/.github/workflows/helloWorld.yml new file mode 100644 index 00000000..6e737461 --- /dev/null +++ b/.github/workflows/helloWorld.yml @@ -0,0 +1,36 @@ +# This is a basic workflow to help you get started with Actions + +name: CI + +# Controls when the workflow will run +on: + # Triggers the workflow on push or pull request events but only for the main branch + push: + branches: [ main ] + pull_request: + branches: [ main ] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: ubuntu-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v3 + + # Runs a single command using the runners shell + - name: Run a one-line script + run: echo Hello, world! + + # Runs a set of commands using the runners shell + - name: Run a multi-line script + run: | + echo Add other actions to build, + echo test, and deploy your project. From 2bc6d9479e5d00281b135d2d281edf72bde04140 Mon Sep 17 00:00:00 2001 From: Ross Gardler Date: Wed, 18 May 2022 15:20:42 -0700 Subject: [PATCH 002/226] maybe if we put it in the right directory... --- .github/{ => workflows}/github-actions-demo.yml | 0 .github/workflows/github-actions-demo.yml~ | 7 +++++++ 2 files changed, 7 insertions(+) rename .github/{ => workflows}/github-actions-demo.yml (100%) create mode 100644 .github/workflows/github-actions-demo.yml~ diff --git a/.github/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml similarity index 100% rename from .github/github-actions-demo.yml rename to .github/workflows/github-actions-demo.yml diff --git a/.github/workflows/github-actions-demo.yml~ b/.github/workflows/github-actions-demo.yml~ new file mode 100644 index 00000000..48305a73 --- /dev/null +++ b/.github/workflows/github-actions-demo.yml~ @@ -0,0 +1,7 @@ +name: GitHub Actions Demo +on: [push] +jobs: + Explore-Gitub-Actions: + runs-on: ubuntu-latest + steps: + - run: echo "Hello World" \ No newline at end of file From 93c67e00e9b0302db204d8a041cb33e30f118901 Mon Sep 17 00:00:00 2001 From: jasonmesser7 <91570951+jasonmesser7@users.noreply.github.com> Date: Wed, 5 Oct 2022 16:07:13 -0700 Subject: [PATCH 003/226] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5cd7cecf..f5c82b93 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Project +# Innovation Engine > This repo has been populated by an initial template to help get you started. Please > make sure to update the content to build a great experience for community-building. From 4324a25d7ac3caa704b33aeb1473f43d7cb196bc Mon Sep 17 00:00:00 2001 From: jasonmesser7 <91570951+jasonmesser7@users.noreply.github.com> Date: Wed, 5 Oct 2022 17:04:13 -0700 Subject: [PATCH 004/226] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f5c82b93..5cd7cecf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Innovation Engine +# Project > This repo has been populated by an initial template to help get you started. Please > make sure to update the content to build a great experience for community-building. From 3000dae3fbd463ff63f68e02d7770df9343aece6 Mon Sep 17 00:00:00 2001 From: jasonmesser7 <91570951+jasonmesser7@users.noreply.github.com> Date: Tue, 20 Dec 2022 12:01:27 -0800 Subject: [PATCH 005/226] Parser and executor code (#1) * Created main.py and am doing a test commit * test commit * test commit * Parses a markdown file and pulls out headings, code blocks, and paragraphs * took away a few magic values & cleaned up code a bit * added the initial executor code complete with repl behavior, by no means complete and it will have bugs but it would be good to get some eyes on it before I go too much further * added a few more comments and deleted a few unused functions * Added testing directions of using python3 main.py to test * Added automated testing functionality with fuzzy matching * cleaned up code a bit and added test functionality * testing * Added github actions for automated testing * Fixing github actions * bug fix for actions * Added push to main as a trigger * bug fix * testing * changed file type to yml * testing * fixed bug with fuzzywuzzy * changing sensitivity so that actions should fail as there is a warning * Changing Sensitivity and adjust actions test * testing * test should pass now * Trying to enable manual workflow trigger * testing github actions * Changing default test * added the ability to set command line variables from both comments and .ini files which are key value pairs * updated README.md and added new test scripts * fixed test file and changed github action to point to test file not readme * Hardedned program to deal with broken markdown files, there is still no good way to deal with interactive bash commands though * added some exception handling so the program doesn't crash on a timeout * Changed Parser file class to MarkdownParser to avoid name clash with std parser, added test scripts folder for testing, and added some additional error handling for commands * removing exit(1) from test section as it terminates early if multiple files are being tested * removed exit(1) from main when file does not parse correctly * removed exit 1 to stop early failures with multiple files * adding random string to end of resource group in testing section so that there are not name conflicts. Catch is the document itself has to use MY_RESOURCE_GROUP_NAME as the variable for testing * cleaned up file structure and fixed a few bugs in Readme.md * added verion locks to requirements.txt, deleted hardcoded vm create document, added .vscode to .gitignore, and modified test script to work based on new location --- .github/workflows/00-testing.yml | 25 ++ .gitignore | 6 +- MarkdownParser.py | 236 +++++++++++++++ README.md | 172 +++++++++-- demoScripts/createVMCommentVars.md | 150 ++++++++++ demoScripts/createVMEnvVars.ini | 5 + demoScripts/createVMEnvVars.md | 139 +++++++++ executor.py | 299 ++++++++++++++++++++ main.py | 37 +++ requirements.txt | 5 + testScripts/CommentTest.md | 22 ++ testScripts/brokenMarkdown.md | 7 + testScripts/createRG.md | 29 ++ testScripts/e2eAzureTestCommentVariables.md | 159 +++++++++++ testScripts/fuzzyMatchTest.md | 51 ++++ testScripts/nonCLI.md | 114 ++++++++ testScripts/test.md | 172 +++++++++++ testScripts/variableHierarchy.ini | 2 + testScripts/variableHierarchy.md | 26 ++ tutorial.md | 124 ++++++++ 20 files changed, 1761 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/00-testing.yml create mode 100644 MarkdownParser.py create mode 100644 demoScripts/createVMCommentVars.md create mode 100644 demoScripts/createVMEnvVars.ini create mode 100644 demoScripts/createVMEnvVars.md create mode 100644 executor.py create mode 100644 main.py create mode 100644 requirements.txt create mode 100644 testScripts/CommentTest.md create mode 100644 testScripts/brokenMarkdown.md create mode 100644 testScripts/createRG.md create mode 100644 testScripts/e2eAzureTestCommentVariables.md create mode 100644 testScripts/fuzzyMatchTest.md create mode 100644 testScripts/nonCLI.md create mode 100644 testScripts/test.md create mode 100644 testScripts/variableHierarchy.ini create mode 100644 testScripts/variableHierarchy.md create mode 100644 tutorial.md diff --git a/.github/workflows/00-testing.yml b/.github/workflows/00-testing.yml new file mode 100644 index 00000000..6e7c3145 --- /dev/null +++ b/.github/workflows/00-testing.yml @@ -0,0 +1,25 @@ +name: 00-testing + +on: + push: + branches: + - ParserAndExecutor + + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + - name: Deploy + env: + AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} + GITHUB_SHA: ${{ github.sha }} + run: | + cd $GITHUB_WORKSPACE/ + pip3 install -r requirements.txt + python3 main.py test test.md \ No newline at end of file diff --git a/.gitignore b/.gitignore index f383d4e8..0e516d73 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -*/*~ \ No newline at end of file +# Python +__pycache__ + +#VS Code +.vscode \ No newline at end of file diff --git a/MarkdownParser.py b/MarkdownParser.py new file mode 100644 index 00000000..d7b109de --- /dev/null +++ b/MarkdownParser.py @@ -0,0 +1,236 @@ +#Given a markdown file, this should break out the headings, paragraphs, executable commands etc. +import re + +class MarkdownParser: + + + def __init__(self, markdownFilepath): + self.markdownFilepath = markdownFilepath + self.markdownElements = [] + self.codeBlockType = '```' + self.headingType = '#' + self.paragraphType = 'p' + self.commentType = '' + endOfComment = True + continue + elif self.markdownFile.read(1) == '`': + if self.checkForCodeBlock(char): + subtype, command = self.processCodeSample(char) + self.createAndAppendElement(self.commentType, subtype, command) + comment += command + else: + self.markdownFile.seek(currentPosition) + comment += char + + char = self.markdownFile.read(1) + + if "expected_similarity" in comment: + results,subtype = self.processResultsBlock() + similarity = re.findall(r"\d*\.?\d+", comment)[0] + # outputblock = "```output\n" + results + "\n```" + # self.createAndAppendElement(self.paragraphType, 'paragraph', outputblock.strip()) + # Loops through elements starting at the end looking for the most recent + # Codeblock markdown item and adds the results + for i, markdownElement in reversed(list(enumerate(self.markdownElements))): + if markdownElement[0] == self.codeBlockType: + self.markdownElements[i][1].results = results + self.markdownElements[i][1].similarity = similarity + + break + self.createAndAppendElement(self.codeBlockType, subtype, results) + else: + self.createAndAppendElement(self.commentType, None, comment) + + + + + def createAndAppendElement(self, type, subtype, value): + element = MarkdownElement(subtype, value) + self.markdownElements.append((type, element)) + + # Helper function ran when we hit a backtick. It checks the next two characters to see + # if they are also backticks. If so, we enter a code block and return true + def checkForCodeBlock(self, char): + currentPosition = self.markdownFile.tell() + if self.markdownFile.read(1) == '`' and self.markdownFile.read(1) == '`': + self.markdownFile.seek(currentPosition) + return True + else: + self.markdownFile.seek(currentPosition) + return False + + def checkForComment(self): + currentPosition = self.markdownFile.tell() + if self.markdownFile.read(1) == '!' and self.markdownFile.read(1) == '-' and self.markdownFile.read(1) == '-': + self.markdownFile.seek(currentPosition) + return True + else: + self.markdownFile.seek(currentPosition) + return False + + def processResultsBlock(self): + results = "" + subtype = "" + endOfCodeBlock = False + + char = self.markdownFile.read(1) + + while char != '`': + char = self.markdownFile.read(1) + + self.markdownFile.read(2) + while char != '\n': + char = self.markdownFile.read(1) + subtype += char + + while not endOfCodeBlock: + if (char == '`'): + if self.checkForCodeBlock(char): + endOfCodeBlock = True + # Read the remaining bash ticks + self.markdownFile.read(2) + + else: + results += char + # Read all 3 back ticks + char = self.markdownFile.read(1) + + return results.strip(), subtype.strip() + # If we want to add process for bold and italicized text + def processBoldText(self,char): + pass + + # If want to process dashes for hidden titles etc. + def processDash(self, char): + pass + +class MarkdownElement: + + def __init__(self, subtype, value): + self.subtype = subtype + self.value = value + self.results = None + self.similarity = 1.0 + diff --git a/README.md b/README.md index 5cd7cecf..b7c46498 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,164 @@ -# Project +# Overview -> This repo has been populated by an initial template to help get you started. Please -> make sure to update the content to build a great experience for community-building. +Innovation Engine is a tool for rapid innovation and simplification. -As the maintainer of this project, please make a few updates: +# Executable Documentation +Executable documentation takes standard markdown language and amplifies it by allowing it to be executed step by step in an educational manner, and tested via automated CI/CD pipelines. -- Improving this README.MD file to provide a great experience -- Updating SUPPORT.MD with content about this project's support experience -- Understanding the security reporting process in SECURITY.MD -- Remove this section from the README +# Try Out Executable Documentation +Azure Cloud Shell provides an environment with all of the prerequisites installed to run Executable Documentation. This is the recommended method for new users to try and develop tutorials for Innovation Engine. + +Open [Azure Cloud Shell](https://ms.portal.azure.com/#cloudshell/) and select Bash as the environment. + +>**Note** This snippet clones the Innovation Engine repo, installs necessary dependencies, and runs the interactive Innovation Engine tutorial script. + +```bash +git clone https://github.com/Azure/InnovationEngine + +cd innovationEngine + +pip3 install -r requirements.txt + +python3 main.py test tutorial.md +``` + +The general format to run an executable document is: +`python3 main.py ` + +### Modes of Operation +Today, executable documentation can be run in 3 modes of operation: + +Interactive: Displays the descriptive text of the tutorial and pauses at code blocks and headings to allow user interaction `python3 main.py interactive tutorial.md` + +Test: Runs the commands and then verifies that the output is sufficiently similar to the expected results (recorded in the markdown file) to be considered correct. `python3 main.py test tutorial.md` + +Execute: Reads the document and executes all of the code blocks not pausing for input or testing output. Essentially executes a markdown file as a script. `python3 main.py execute tutorial.md` +## Use Executable documentation for Automated Testing +One of the core benefits of executable documentation is the ability to run automated testing on markdown file. This can be used to ensure freshness of content. + +In order to do this one will need to combine innovation engine executable documentation syntax with GitHub actions. + +In order to test if a command or action ran correctly executable documentation needs something to compare the results against. This requirement is met with result blocks. + +### Result Blocks +Result blocks are distinguished in Executable documentation by a custom expected_similarity comment tag followed by a code block. For example + + + +```text +Hello world +``` +This example purposely breaks the comment syntax so that it shows up in markdown. Otherwise, the tag of expected_similarity is completely invisible. + +The expected similarity value is a floating point number between 0 and 1 which specifies how closely the output needs to match the results block. 0 being no similarity, 1 being an exact match. + +>**Note** It may take a little bit of trial and error to find the exact value for expected_similarity. + +### Environment Variables + +Another barrier to automated testing is setting default values for test cases to use in running. This problem can be solved with command line variables in Executable documentation Syntax. + +Default environment variables can be set for executable documentation in a few different ways. + +1. A matching .ini file to the markdown + - Upon running any document executable documentation will look for a corresponding .ini file. For example if my markdown file is named tutorial.md the corresponding ini file would be tutorial.ini. + - This file is a simple key value match for environment variable and value. For example: + ```ini + MY_RESOURCE_GROUP_NAME = myResourceGroup + MY_LOCATION = eastus + MY_VM_NAME = myVM + MY_VM_IMAGE = debian + MY_ADMIN_USERNAME = azureuser + ``` +2. A comment at the beginning of the document containing a code blog with the tag 'variables'. This will be invisible to users unless they look at the raw markdown. For example: + >**Note** The below example intentionally has broken comment syntax w/ two !'s. + + + +Variables set in comments will override variables set in a .ini file. Consequently, locally declared variables in code samples will override variables set in comments. + +### Setting Up GitHub Actions to use Innovation Engine + +After documentation is set up to take advantage of automated testing a github action will need to be created to run testing on a recurring basis. The action will simply create a basic Linux container, install Innovation Engine Executable Documentation and run Executable documentation in the Test mode on whatever markdown files are specified. + +It is important to note that if you require any specific access or cli tools not included in standard bash that will need to be installed in the container. The following example is how this may be done for a document which runs Azure commands. + +```yml +name: 00-testing + +on: + push: + branches: + - main + + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + - name: Deploy + env: + AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} + GITHUB_SHA: ${{ github.sha }} + run: | + cd $GITHUB_WORKSPACE/ + git clone https://github.com/Azure/InnovationEngine/tree/ParserAndExecutor + cd innovationEngine + pip3 install -r requirements.txt + cp ../../articles/quick-create-cli.md README.md + python3 main.py test README.md +``` + + +## Use Executable Documentation for Interactive Documentation + +Innovation Engine can also be used for interactive tutorials via a local or remote shell environment. After cloning the project and running `pip3 install -r requirements.txt`, Innovation Engine can be used for interactive tutorials by simply using the interactive flag when executing the program. For example, `python3 main.py interactive tutorial.md` + +As it is written the code will pause and wait for input on any header or code block. Any document written in standard markdown can be run as an interactive document. ## Contributing -This project welcomes contributions and suggestions. Most contributions require you to agree to a -Contributor License Agreement (CLA) declaring that you have the right to, and actually do, grant us -the rights to use your contribution. For details, visit https://cla.opensource.microsoft.com. +This is an open source project. Don't keep your code improvements, +features and cool ideas to yourself. Please issue pull requests +against our [GitHub repo](https://github.com/Azure/innovationengine). + +Be sure to use our Git pre-commit script to test your contributions +before committing, simply run the following command: `python3 main.py test test` + +This project welcomes contributions and suggestions. Most +contributions require you to agree to a Contributor License Agreement +(CLA) declaring that you have the right to, and actually do, grant us +the rights to use your contribution. For details, visit +https://cla.microsoft.com. + +When you submit a pull request, a CLA-bot will automatically determine +whether you need to provide a CLA and decorate the PR appropriately +(e.g., label, comment). Simply follow the instructions provided by the +bot. You will only need to do this once across all repos using our +CLA. -When you submit a pull request, a CLA bot will automatically determine whether you need to provide -a CLA and decorate the PR appropriately (e.g., status check, comment). Simply follow the instructions -provided by the bot. You will only need to do this once across all repos using our CLA. +This project has adopted +the +[Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see +the +[Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or +contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with +any additional questions or comments. -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. ## Trademarks @@ -30,4 +166,4 @@ This project may contain trademarks or logos for projects, products, or services trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. -Any use of third-party trademarks or logos are subject to those third-party's policies. +Any use of third-party trademarks or logos are subject to those third-party's policies. \ No newline at end of file diff --git a/demoScripts/createVMCommentVars.md b/demoScripts/createVMCommentVars.md new file mode 100644 index 00000000..016edbfa --- /dev/null +++ b/demoScripts/createVMCommentVars.md @@ -0,0 +1,150 @@ + + + +The following example uses a comment block to set environment variables which are used throughout the file as one of the acceptable patterns for an executable document. + + +# Quickstart: Create a Linux virtual machine with the Azure CLI + +**Applies to:** :heavy_check_mark: Linux VMs + +This quickstart shows you how to use the Azure CLI to deploy a Linux virtual machine (VM) in Azure. The Azure CLI is used to create and manage Azure resources via either the command line or scripts. + +In this tutorial, we will be installing the latest Debian image. To show the VM in action, you'll connect to it using SSH and install the NGINX web server. + +If you don't have an Azure subscription, create a [free account](https://azure.microsoft.com/free/?WT.mc_id=A261C142F) before you begin. + +## Launch Azure Cloud Shell + +The Azure Cloud Shell is a free interactive shell that you can use to run the steps in this article. It has common Azure tools preinstalled and configured to use with your account. + +To open the Cloud Shell, just select **Try it** from the upper right corner of a code block. You can also open Cloud Shell in a separate browser tab by going to [https://shell.azure.com/bash](https://shell.azure.com/bash). Select **Copy** to copy the blocks of code, paste it into the Cloud Shell, and select **Enter** to run it. + +If you prefer to install and use the CLI locally, this quickstart requires Azure CLI version 2.0.30 or later. Run `az --version` to find the version. If you need to install or upgrade, see [Install Azure CLI]( /cli/azure/install-azure-cli). + + + +## Create a resource group + +Create a resource group with the [az group create](/cli/azure/group) command. An Azure resource group is a logical container into which Azure resources are deployed and managed. The following example creates a resource group named *myResourceGroup* in the *eastus* location: + +```bash +az group create --name $MY_RESOURCE_GROUP_NAME --location $MY_LOCATION +``` + +## Create virtual machine + +Create a VM with the [az vm create](/cli/azure/vm) command. + +The following example creates a VM named *myVM* and adds a user account named *azureuser*. The `--generate-ssh-keys` parameter is used to automatically generate an SSH key, and put it in the default key location (*~/.ssh*). To use a specific set of keys instead, use the `--ssh-key-values` option. + +```bash +az vm create \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --name $MY_VM_NAME \ + --image $MY_VM_IMAGE \ + --admin-username $MY_ADMIN_USERNAME \ + --generate-ssh-keys +``` + +It takes a few minutes to create the VM and supporting resources. The following example output shows the VM create operation was successful. + +```Output +{ + "fqdns": "", + "id": "/subscriptions//resourceGroups/myResourceGroup/providers/Microsoft.Compute/virtualMachines/myVM", + "location": "eastus", + "macAddress": "00-0D-3A-23-9A-49", + "powerState": "VM running", + "privateIpAddress": "10.0.0.4", + "publicIpAddress": "40.68.254.142", + "resourceGroup": "myResourceGroup" +} +``` + +Make a note of the `publicIpAddress` to use later. + +## Install web server + +To see your VM in action, install the NGINX web server. Update your package sources and then install the latest NGINX package. + +```bash +az vm run-command invoke \ + -g $MY_RESOURCE_GROUP_NAME \ + -n $MY_VM_NAME \ + --command-id RunShellScript \ + --scripts "sudo apt-get update && sudo apt-get install -y nginx" +``` + +## Open port 80 for web traffic + +By default, only SSH connections are opened when you create a Linux VM in Azure. Use [az vm open-port](/cli/azure/vm) to open TCP port 80 for use with the NGINX web server: + +```bash +az vm open-port --port 80 --resource-group $MY_RESOURCE_GROUP_NAME --name $MY_VM_NAME +``` + +## View the web server in action + +Use a web browser of your choice to view the default NGINX welcome page. Use the public IP address of your VM as the web address. The following example shows the default NGINX web site: + +![Screenshot showing the N G I N X default web page.](./media/quick-create-cli/nginix-welcome-page-debian.png) + +Or Run the following command to see the NGINX welcome page in terminal + +```bash + curl $(az vm show -d -g $MY_RESOURCE_GROUP_NAME -n $MY_VM_NAME --query "publicIps" -o tsv) +``` + + +```HTML + + + +Welcome to nginx! + + + +

Welcome to nginx!

+

If you see this page, the nginx web server is successfully installed and +working. Further configuration is required.

+ +

For online documentation and support please refer to +nginx.org.
+Commercial support is available at +nginx.com.

+ +

Thank you for using nginx.

+ + +``` + +## Clean up resources + +When no longer needed, you can use the [az group delete](/cli/azure/group) command to remove the resource group, VM, and all related resources. + +```bash +az group delete --name $MY_RESOURCE_GROUP_NAME --no-wait --yes --verbose +``` + +## Next steps + +In this quickstart, you deployed a simple virtual machine, opened a network port for web traffic, and installed a basic web server. To learn more about Azure virtual machines, continue to the tutorial for Linux VMs. + + +> [!div class="nextstepaction"] +> [Azure Linux virtual machine tutorials](./tutorial-manage-vm.md) diff --git a/demoScripts/createVMEnvVars.ini b/demoScripts/createVMEnvVars.ini new file mode 100644 index 00000000..ad6cd15a --- /dev/null +++ b/demoScripts/createVMEnvVars.ini @@ -0,0 +1,5 @@ +MY_RESOURCE_GROUP_NAME = myResourceGroup +MY_LOCATION = eastus +MY_VM_NAME = myVM +MY_VM_IMAGE = debian +MY_ADMIN_USERNAME = azureuser \ No newline at end of file diff --git a/demoScripts/createVMEnvVars.md b/demoScripts/createVMEnvVars.md new file mode 100644 index 00000000..2f5ce350 --- /dev/null +++ b/demoScripts/createVMEnvVars.md @@ -0,0 +1,139 @@ + +The following example uses a .ini file which is named azureVmCreateEnvVariables.ini to set environment variables which are used throughout the file as one of the acceptable patterns for an executable document. + +# Quickstart: Create a Linux virtual machine with the Azure CLI + +**Applies to:** :heavy_check_mark: Linux VMs + +This quickstart shows you how to use the Azure CLI to deploy a Linux virtual machine (VM) in Azure. The Azure CLI is used to create and manage Azure resources via either the command line or scripts. + +In this tutorial, we will be installing the latest Debian image. To show the VM in action, you'll connect to it using SSH and install the NGINX web server. + +If you don't have an Azure subscription, create a [free account](https://azure.microsoft.com/free/?WT.mc_id=A261C142F) before you begin. + +## Launch Azure Cloud Shell + +The Azure Cloud Shell is a free interactive shell that you can use to run the steps in this article. It has common Azure tools preinstalled and configured to use with your account. + +To open the Cloud Shell, just select **Try it** from the upper right corner of a code block. You can also open Cloud Shell in a separate browser tab by going to [https://shell.azure.com/bash](https://shell.azure.com/bash). Select **Copy** to copy the blocks of code, paste it into the Cloud Shell, and select **Enter** to run it. + +If you prefer to install and use the CLI locally, this quickstart requires Azure CLI version 2.0.30 or later. Run `az --version` to find the version. If you need to install or upgrade, see [Install Azure CLI]( /cli/azure/install-azure-cli). + + + +## Create a resource group + +Create a resource group with the [az group create](/cli/azure/group) command. An Azure resource group is a logical container into which Azure resources are deployed and managed. The following example creates a resource group named *myResourceGroup* in the *eastus* location: + +```bash +az group create --name $MY_RESOURCE_GROUP_NAME --location $MY_LOCATION +``` + +## Create virtual machine + +Create a VM with the [az vm create](/cli/azure/vm) command. + +The following example creates a VM named *myVM* and adds a user account named *azureuser*. The `--generate-ssh-keys` parameter is used to automatically generate an SSH key, and put it in the default key location (*~/.ssh*). To use a specific set of keys instead, use the `--ssh-key-values` option. + +```bash +az vm create \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --name $MY_VM_NAME \ + --image $MY_VM_IMAGE \ + --admin-username $MY_ADMIN_USERNAME \ + --generate-ssh-keys +``` + +It takes a few minutes to create the VM and supporting resources. The following example output shows the VM create operation was successful. + +```Output +{ + "fqdns": "", + "id": "/subscriptions//resourceGroups/myResourceGroup/providers/Microsoft.Compute/virtualMachines/myVM", + "location": "eastus", + "macAddress": "00-0D-3A-23-9A-49", + "powerState": "VM running", + "privateIpAddress": "10.0.0.4", + "publicIpAddress": "40.68.254.142", + "resourceGroup": "myResourceGroup" +} +``` + +Make a note of the `publicIpAddress` to use later. + +## Install web server + +To see your VM in action, install the NGINX web server. Update your package sources and then install the latest NGINX package. + +```bash +az vm run-command invoke \ + -g $MY_RESOURCE_GROUP_NAME \ + -n $MY_VM_NAME \ + --command-id RunShellScript \ + --scripts "sudo apt-get update && sudo apt-get install -y nginx" +``` + +## Open port 80 for web traffic + +By default, only SSH connections are opened when you create a Linux VM in Azure. Use [az vm open-port](/cli/azure/vm) to open TCP port 80 for use with the NGINX web server: + +```bash +az vm open-port --port 80 --resource-group $MY_RESOURCE_GROUP_NAME --name $MY_VM_NAME +``` + +## View the web server in action + +Use a web browser of your choice to view the default NGINX welcome page. Use the public IP address of your VM as the web address. The following example shows the default NGINX web site: + +![Screenshot showing the N G I N X default web page.](./media/quick-create-cli/nginix-welcome-page-debian.png) + +Or Run the following command to see the NGINX welcome page in terminal + +```bash + curl $(az vm show -d -g myResourceGroup -n myVM --query "publicIps" -o tsv) +``` + + +```HTML + + + +Welcome to nginx! + + + +

Welcome to nginx!

+

If you see this page, the nginx web server is successfully installed and +working. Further configuration is required.

+ +

For online documentation and support please refer to +nginx.org.
+Commercial support is available at +nginx.com.

+ +

Thank you for using nginx.

+ + +``` + +## Clean up resources + +When no longer needed, you can use the [az group delete](/cli/azure/group) command to remove the resource group, VM, and all related resources. + +```bash +az group delete --name $MY_RESOURCE_GROUP_NAME --no-wait --yes --verbose +``` + +## Next steps + +In this quickstart, you deployed a simple virtual machine, opened a network port for web traffic, and installed a basic web server. To learn more about Azure virtual machines, continue to the tutorial for Linux VMs. + + +> [!div class="nextstepaction"] +> [Azure Linux virtual machine tutorials](./tutorial-manage-vm.md) diff --git a/executor.py b/executor.py new file mode 100644 index 00000000..19ee4485 --- /dev/null +++ b/executor.py @@ -0,0 +1,299 @@ + +# Class which will run the main loop of the program + +from unittest import result +import pexpect +import pexpect.replwrap +import time +from fuzzywuzzy import fuzz +from fuzzywuzzy import process +import re +import random +from os.path import exists + +PEXPECT_PROMPT = u'[PEXPECT_PROMPT>' +PEXPECT_CONTINUATION_PROMPT = u'[PEXPECT_PROMPT+' + +class Executor: + shell = None + markdownData = None + executableCodeList = None + + def __init__(self, markdownData, modeOfOperation, fileName): + self.markdownData = markdownData + self.fileName = fileName + self.executableCodeList = {"bash", "terraform", 'azurecli-interactive' , 'azurecli'} + self.modeOfOperation = modeOfOperation + self.numberOfTestsPassed = 0 + self.totalNumberOfTests = 0 + self.failedTests = [] + self.randomIdentifierSet = False + self.randomIdentifier = random.randint(100,10000) + self.shell = self.get_shell() + self.readEnvVariables() + + + # Fairly straight forward main loop. While markdownData is not empty + # Checks type for heading, code block, or paragrpah. + # If Heading it outputs the heading, pops the item and prompts input from user + # If paragraph it outputs paragraph and pops item from list and continues with no pause + # If Code block, it calls ExecuteCode helper function to print and execute the code block + + def runMainLoop(self): + if self.modeOfOperation == "interactive": + self.runMainLoopInteractive() + elif self.modeOfOperation == "test": + self.runMainLoopTest() + elif self.modeOfOperation == 'execute': + self.runMainLoopExecute() + else: + self.runMainLoopInteractive() + + + # This function loops through the markdown elements in an interactive manner. It pauses and + # requests input from the user to continue at every heading and code block + def runMainLoopInteractive(self): + beginningHeading = True + fromCodeBlock = False + + for markdownItem in self.markdownData: + if markdownItem[0] == '#': + if beginningHeading or fromCodeBlock: + print(markdownItem[1].value) + beginningHeading = False + fromCodeBlock = False + else: + beginningHeading = True + self.askForInput("Press any key to continue...") + + elif markdownItem[0] == 'p' and markdownItem[1].subtype == 'prerequisites': + print(markdownItem[1].value) + self.askForInput("Press any key to proceed and execute the prerequisites...") + self.executePrerequisites(markdownItem) + beginningHeading = True + + elif markdownItem[0] == 'p': + print(markdownItem[1].value) + beginningHeading = True + + elif markdownItem[0] == '```': + print('\n```' + markdownItem[1].subtype + '\n' + markdownItem[1].value + '\n```') + self.executeCode(markdownItem) + fromCodeBlock = True + + # This function runs through and only looks at code blocks. It executes them and then + # Looks at the output. It will automatically return exit code 1 if a test fails. + # Used in GitHub Actions and automated testing scenarios + def runMainLoopTest(self): + for markdownItem in self.markdownData: + if markdownItem[0] == '```': + print('\n```' + markdownItem[1].subtype + '\n' + markdownItem[1].value + '\n```') + if markdownItem[1].subtype in self.executableCodeList: + if not self.randomIdentifierSet and self.modeOfOperation == "test": + self.randomIdentifierSet = True + setRandomIdentifierCommand = 'export MY_RESOURCE_GROUP_NAME=testResourceGroup' + str(self.randomIdentifier) + self.shell.run_command(setRandomIdentifierCommand, 1200).strip() + + self.runCommand(markdownItem) + + elif markdownItem[0] == 'p' and markdownItem[1].subtype == 'prerequisites': + self.executePrerequisites(markdownItem) + + elif markdownItem[0] == 'p' and markdownItem[1].subtype == 'next steps': + self.executeNextSteps(markdownItem) + + + print("\n{} of {} Tests Passed!".format(str(self.numberOfTestsPassed), str(self.totalNumberOfTests))) + if self.numberOfTestsPassed < self.totalNumberOfTests: + print("---------FAILED CODE BLOCKS-------------- \n\n") + for failedTest in self.failedTests: + print(failedTest[0][1].value + '\n') + print(failedTest[1]) + #Taking out the exit 1 command as this will end the testing if we are testing multiple files + #exit(1) + + # This function runs through and executes not pausing for any input or failing. + # The primary intention of this is for executing pre requisites + def runMainLoopExecute(self): + for markdownItem in self.markdownData: + print(markdownItem[1].value) + if markdownItem[0] == '```': + print('\n```' + markdownItem[1].subtype + '\n' + markdownItem[1].value + '\n```') + if markdownItem[1].subtype in self.executableCodeList: + self.runCommand(markdownItem) + + # Checks to see if code block is executable i.e, bash, terraform, azurecli-interactive, azurecli + # If it is it will wait for input and call run command which passes the command to the repl + def executeCode(self, markdownItem): + if markdownItem[1].subtype in self.executableCodeList: + self.askForInput("Press any key to execute the above code block...") + print("Executing Code...") + self.runCommand(markdownItem) + + else: + self.askForInput("Press any key to continue...") + + # Function takes a command and uses the shell which was instantiated at run time using the + # Local shell information to execute that command. If the user is logged into az cli on + # The authentication will carry over to this environment as well + def runCommand(self, markdownItem): + command = markdownItem[1].value + expectedResult = markdownItem[1].results + expectedSimilarity = markdownItem[1].similarity + + #print("debug", "Execute command: '" + command + "'\n") + startTime = time.time() + try: + # Setting a 20 minute timeout...Need a better way to discover broken commands + response = self.shell.run_command(command, 1200).strip() + except ValueError as ve: + print("Continuation prompt required for command " + command) + print(ex) + response = command + " failed to run" + except Exception as ex: + print("command timed out") + print(ex) + response = command + " failed to run" + + + timeToExecute = time.time() - startTime + print("\n" + response + "\n" + "Time to Execute - " + str(timeToExecute)) + + if expectedResult is not None: + print("Expected Results - " + expectedResult) + self.testResponse(response, expectedResult, expectedSimilarity, markdownItem) + + + def testResponse(self, response, expectedResult, expectedSimilarity, markdownItem): + # Todo... try to implement more than just fuzzy matching. Can we look and see if the command returned + # A warning or an error? Problem I am having is calls can return every type of response... I could + # Hard code something for Azure responses, but it wouldn't be extendible + #print("\n```output\n" + expectedResult + "\n```") + + if self.modeOfOperation == "interactive": + actualSimilarity = fuzz.ratio(response, expectedResult) / 100 + + if actualSimilarity < float(expectedSimilarity): + print("The output is NOT correct. The remainder of the document may not function properly") + print("The Actual similarity was {} \n The expected similarity was {}".format(str(actualSimilarity), expectedSimilarity)) + + + self.askForInput("Press any key to continue...") + + elif self.modeOfOperation == "test": + self.totalNumberOfTests += 1 + actualSimilarity = fuzz.ratio(response, expectedResult) / 100 + + if actualSimilarity < float(expectedSimilarity): + errorOutput = "Test Failed \n\nThe expected result was - \n" + expectedResult + errorOutput += "\nthe actual result - \n" + response + errorOutput += "\nThe Actual similarity was {} \nThe expected similarity was {} \n".format(str(actualSimilarity), expectedSimilarity) + print(errorOutput) + + self.failedTests.append((markdownItem, errorOutput)) + else: + self.numberOfTestsPassed += 1 + + + # todo: Create testing mode for execute which simply lets the user know if a test fails which one it is + + + + + def executePrerequisites(self, markdownItem): + results = re.findall(r'\]\(([^)]+)\)', markdownItem[1].value) + + for markdownFilepath in results: + if exists(markdownFilepath): + command = 'python3 main.py execute ' + markdownFilepath + print("this is the command to execute \n" + command) + response = self.shell.run_command(command).strip() + print(response) + else: + print("Could not find file named " + markdownFilepath + " Please locate and run this prerequisite manually") + + def executeNextSteps(self, markdownItem): + print("Found Next Steps....") + pass + + def askForInput(self, inputPrompt): + print("\n\n" + inputPrompt + " Press b to exit the program \n \n") + keyPressed = self.getInstructionKey() + if keyPressed == 'b': + print("Exiting program on b key press") + exit() + + def getInstructionKey(self): + """Waits for a single keypress on stdin. + This is a silly function to call if you need to do it a lot because it has + to store stdin's current setup, setup stdin for reading single keystrokes + then read the single keystroke then revert stdin back after reading the + keystroke. + Returns the character of the key that was pressed (zero on + KeyboardInterrupt which can happen when a signal gets handled) + This method is licensed under cc by-sa 3.0 + Thanks to mheyman http://stackoverflow.com/questions/983354/how-do-i-make-python-to-wait-for-a-pressed-key\ + """ + import termios, fcntl, sys, os + fd = sys.stdin.fileno() + # save old state + flags_save = fcntl.fcntl(fd, fcntl.F_GETFL) + attrs_save = termios.tcgetattr(fd) + # make raw - the way to do this comes from the termios(3) man page. + attrs = list(attrs_save) # copy the stored version to update + # iflag + attrs[0] &= ~(termios.IGNBRK | termios.BRKINT | termios.PARMRK + | termios.ISTRIP | termios.INLCR | termios. IGNCR + | termios.ICRNL | termios.IXON ) + # oflag + attrs[1] &= ~termios.OPOST + # cflag + attrs[2] &= ~(termios.CSIZE | termios. PARENB) + attrs[2] |= termios.CS8 + # lflag + attrs[3] &= ~(termios.ECHONL | termios.ECHO | termios.ICANON + | termios.ISIG | termios.IEXTEN) + termios.tcsetattr(fd, termios.TCSANOW, attrs) + # turn off non-blocking + fcntl.fcntl(fd, fcntl.F_SETFL, flags_save & ~os.O_NONBLOCK) + # read a single keystroke + try: + ret = sys.stdin.read(1) # returns a single character + except KeyboardInterrupt: + ret = 0 + finally: + # restore old state + termios.tcsetattr(fd, termios.TCSAFLUSH, attrs_save) + fcntl.fcntl(fd, fcntl.F_SETFL, flags_save) + return ret + + # Function looks for file named + def readEnvVariables(self): + + if exists(self.fileName[:-3] + '.ini'): + envFile = open(self.fileName[:-3] + '.ini') + lines = envFile.readlines() + + for line in lines: + variableName = line.split()[0] + value = line.split()[2] + command = variableName + '=' + value + self.shell.run_command(command).strip() + # Comment block variables goes after the .ini declaration and thus overrides + for markdownItem in self.markdownData: + if markdownItem[0] == " + + + + + +# Testing multi Line code block + +```azurecli-interactive +echo "Hello \ +world" +``` + +# This is what the output should be + +```text +hello world +``` \ No newline at end of file diff --git a/testScripts/brokenMarkdown.md b/testScripts/brokenMarkdown.md new file mode 100644 index 00000000..4e2e5197 --- /dev/null +++ b/testScripts/brokenMarkdown.md @@ -0,0 +1,7 @@ +This is a markdown file which does not pass the requirements... It has a code block which never ends. + +Innovation Engine should be able to exit the program automatically instead of hanging + +```bash +echo "hello World" +`` \ No newline at end of file diff --git a/testScripts/createRG.md b/testScripts/createRG.md new file mode 100644 index 00000000..f53b7693 --- /dev/null +++ b/testScripts/createRG.md @@ -0,0 +1,29 @@ + + + +## Create a resource group + +Create a resource group with the [az group create](/cli/azure/group) command. An Azure resource group is a logical container into which Azure resources are deployed and managed. The following example creates a resource group named *myResourceGroup* in the *eastus* location: + +```bash +az group create --name $MY_RESOURCE_GROUP_NAME --location $MY_LOCATION +``` + + +```Output +{ + "fqdns": "", + "id": "/subscriptions//resourceGroups/myResourceGroup/providers/Microsoft.Compute/virtualMachines/myVM", + "location": "eastus", + "macAddress": "00-0D-3A-23-9A-49", + "powerState": "VM running", + "privateIpAddress": "10.0.0.4", + "publicIpAddress": "40.68.254.142", + "resourceGroup": "myResourceGroup" +} +``` \ No newline at end of file diff --git a/testScripts/e2eAzureTestCommentVariables.md b/testScripts/e2eAzureTestCommentVariables.md new file mode 100644 index 00000000..2106218f --- /dev/null +++ b/testScripts/e2eAzureTestCommentVariables.md @@ -0,0 +1,159 @@ +--- +title: 'Quickstart: Use the Azure CLI to create a Linux VM' +description: In this quickstart, you learn how to use the Azure CLI to create a Linux virtual machine +author: cynthn +ms.service: virtual-machines +ms.collection: linux +ms.topic: quickstart +ms.workload: infrastructure +ms.date: 06/01/2022 +ms.author: cynthn +ms.custom: mvc, seo-javascript-september2019, seo-javascript-october2019, seo-python-october2019, devx-track-azurecli, mode-api +--- + + + +# Quickstart: Create a Linux virtual machine with the Azure CLI + +**Applies to:** :heavy_check_mark: Linux VMs + +This quickstart shows you how to use the Azure CLI to deploy a Linux virtual machine (VM) in Azure. The Azure CLI is used to create and manage Azure resources via either the command line or scripts. + +In this tutorial, we will be installing the latest Debian image. To show the VM in action, you'll connect to it using SSH and install the NGINX web server. + +If you don't have an Azure subscription, create a [free account](https://azure.microsoft.com/free/?WT.mc_id=A261C142F) before you begin. + +## Launch Azure Cloud Shell + +The Azure Cloud Shell is a free interactive shell that you can use to run the steps in this article. It has common Azure tools preinstalled and configured to use with your account. + +To open the Cloud Shell, just select **Try it** from the upper right corner of a code block. You can also open Cloud Shell in a separate browser tab by going to [https://shell.azure.com/bash](https://shell.azure.com/bash). Select **Copy** to copy the blocks of code, paste it into the Cloud Shell, and select **Enter** to run it. + +If you prefer to install and use the CLI locally, this quickstart requires Azure CLI version 2.0.30 or later. Run `az --version` to find the version. If you need to install or upgrade, see [Install Azure CLI]( /cli/azure/install-azure-cli). + + + +## Create a resource group + +Create a resource group with the [az group create](/cli/azure/group) command. An Azure resource group is a logical container into which Azure resources are deployed and managed. The following example creates a resource group named *myResourceGroup* in the *eastus* location: + +```bash +az group create --name $MY_RESOURCE_GROUP_NAME --location $MY_LOCATION +``` + +## Create virtual machine + +Create a VM with the [az vm create](/cli/azure/vm) command. + +The following example creates a VM named *myVM* and adds a user account named *azureuser*. The `--generate-ssh-keys` parameter is used to automatically generate an SSH key, and put it in the default key location (*~/.ssh*). To use a specific set of keys instead, use the `--ssh-key-values` option. + +```bash +az vm create \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --name $MY_VM_NAME \ + --image $MY_VM_IMAGE \ + --admin-username $MY_ADMIN_USERNAME \ + --generate-ssh-keys +``` + +It takes a few minutes to create the VM and supporting resources. The following example output shows the VM create operation was successful. + +```Output +{ + "fqdns": "", + "id": "/subscriptions//resourceGroups/myResourceGroup/providers/Microsoft.Compute/virtualMachines/myVM", + "location": "eastus", + "macAddress": "00-0D-3A-23-9A-49", + "powerState": "VM running", + "privateIpAddress": "10.0.0.4", + "publicIpAddress": "40.68.254.142", + "resourceGroup": "myResourceGroup" +} +``` + +Make a note of the `publicIpAddress` to use later. + +## Install web server + +To see your VM in action, install the NGINX web server. Update your package sources and then install the latest NGINX package. + +```bash +az vm run-command invoke \ + -g $MY_RESOURCE_GROUP_NAME \ + -n $MY_VM_NAME \ + --command-id RunShellScript \ + --scripts "sudo apt-get update && sudo apt-get install -y nginx" +``` + +## Open port 80 for web traffic + +By default, only SSH connections are opened when you create a Linux VM in Azure. Use [az vm open-port](/cli/azure/vm) to open TCP port 80 for use with the NGINX web server: + +```bash +az vm open-port --port 80 --resource-group $MY_RESOURCE_GROUP_NAME --name $MY_VM_NAME +``` + +## View the web server in action + +Use a web browser of your choice to view the default NGINX welcome page. Use the public IP address of your VM as the web address. The following example shows the default NGINX web site: + +![Screenshot showing the N G I N X default web page.](./media/quick-create-cli/nginix-welcome-page-debian.png) + +Or Run the following command to see the NGINX welcome page in terminal + +```bash + curl $(az vm show -d -g $MY_RESOURCE_GROUP_NAME -n $MY_VM_NAME --query "publicIps" -o tsv) +``` + + +```HTML + + + +Welcome to nginx! + + + +

Welcome to nginx!

+

If you see this page, the nginx web server is successfully installed and +working. Further configuration is required.

+ +

For online documentation and support please refer to +nginx.org.
+Commercial support is available at +nginx.com.

+ +

Thank you for using nginx.

+ + +``` + +## Clean up resources + +When no longer needed, you can use the [az group delete](/cli/azure/group) command to remove the resource group, VM, and all related resources. + +```bash +az group delete --name $MY_RESOURCE_GROUP_NAME --no-wait --yes --verbose +``` + +## Next steps + +In this quickstart, you deployed a simple virtual machine, opened a network port for web traffic, and installed a basic web server. To learn more about Azure virtual machines, continue to the tutorial for Linux VMs. + + +> [!div class="nextstepaction"] +> [Azure Linux virtual machine tutorials](./tutorial-manage-vm.md) diff --git a/testScripts/fuzzyMatchTest.md b/testScripts/fuzzyMatchTest.md new file mode 100644 index 00000000..f58e37c3 --- /dev/null +++ b/testScripts/fuzzyMatchTest.md @@ -0,0 +1,51 @@ +# Testing multi Line code block + +```azurecli-interactive +echo "Hello World" +``` +This is what the expected output should be + +```text +Hello world +``` + + +# Testing multi Line code block + +```azurecli-interactive +echo "Hello \ +world" +``` + +# Output Should Fail + +```text +world Hello +``` + +# Code block + +```azurecli-interactive +echo "Hello \ +world" +``` + +# Output Should Pass + +```text +Hello world +``` + +# Code block + +```azurecli-interactive +echo "Hello \ +world" +``` + +# Bad similarity - should fail + +```text +Hello world +``` + diff --git a/testScripts/nonCLI.md b/testScripts/nonCLI.md new file mode 100644 index 00000000..9d16b6fb --- /dev/null +++ b/testScripts/nonCLI.md @@ -0,0 +1,114 @@ +--- +title: Quickstart - Create a Linux VM in the Azure portal +description: In this quickstart, you learn how to use the Azure portal to create a Linux virtual machine. +author: cynthn +ms.service: virtual-machines +ms.collection: linux +ms.topic: quickstart +ms.workload: infrastructure +ms.date: 08/01/2022 +ms.author: cynthn +ms.custom: mvc, mode-ui +--- +This document will not be a CLI document. I am curious what innovation engine will do in this case. + +# Quickstart: Create a Linux virtual machine in the Azure portal + +**Applies to:** :heavy_check_mark: Linux VMs + +Azure virtual machines (VMs) can be created through the Azure portal. The Azure portal is a browser-based user interface to create Azure resources. This quickstart shows you how to use the Azure portal to deploy a Linux virtual machine (VM) running Ubuntu 18.04 LTS. To see your VM in action, you also SSH to the VM and install the NGINX web server. + +If you don't have an Azure subscription, create a [free account](https://azure.microsoft.com/free/?WT.mc_id=A261C142F) before you begin. + +## Sign in to Azure + +Sign in to the [Azure portal](https://portal.azure.com). + +## Create virtual machine + +1. Enter *virtual machines* in the search. +1. Under **Services**, select **Virtual machines**. +1. In the **Virtual machines** page, select **Create** and then **Virtual machine**. The **Create a virtual machine** page opens. + +1. In the **Basics** tab, under **Project details**, make sure the correct subscription is selected and then choose to **Create new** resource group. Enter *myResourceGroup* for the name.*. + + ![Screenshot of the Project details section showing where you select the Azure subscription and the resource group for the virtual machine](./media/quick-create-portal/project-details.png) + +1. Under **Instance details**, enter *myVM* for the **Virtual machine name**, and choose *Ubuntu 18.04 LTS - Gen2* for your **Image**. Leave the other defaults. The default size and pricing is only shown as an example. Size availability and pricing are dependent on your region and subscription. + + :::image type="content" source="media/quick-create-portal/instance-details.png" alt-text="Screenshot of the Instance details section where you provide a name for the virtual machine and select its region, image, and size."::: + + > [!NOTE] + > Some users will now see the option to create VMs in multiple zones. To learn more about this new capability, see [Create virtual machines in an availability zone](../create-portal-availability-zone.md). + > :::image type="content" source="../media/create-portal-availability-zone/preview.png" alt-text="Screenshot showing that you have the option to create virtual machines in multiple availability zones."::: + + +1. Under **Administrator account**, select **SSH public key**. + +1. In **Username** enter *azureuser*. + +1. For **SSH public key source**, leave the default of **Generate new key pair**, and then enter *myKey* for the **Key pair name**. + + ![Screenshot of the Administrator account section where you select an authentication type and provide the administrator credentials](./media/quick-create-portal/administrator-account.png) + +1. Under **Inbound port rules** > **Public inbound ports**, choose **Allow selected ports** and then select **SSH (22)** and **HTTP (80)** from the drop-down. + + ![Screenshot of the inbound port rules section where you select what ports inbound connections are allowed on](./media/quick-create-portal/inbound-port-rules.png) + +1. Leave the remaining defaults and then select the **Review + create** button at the bottom of the page. + +1. On the **Create a virtual machine** page, you can see the details about the VM you are about to create. When you are ready, select **Create**. + +1. When the **Generate new key pair** window opens, select **Download private key and create resource**. Your key file will be download as **myKey.pem**. Make sure you know where the `.pem` file was downloaded; you will need the path to it in the next step. + +1. When the deployment is finished, select **Go to resource**. + +1. On the page for your new VM, select the public IP address and copy it to your clipboard. + + + ![Screenshot showing how to copy the IP address for the virtual machine](./media/quick-create-portal/ip-address.png) + + +## Connect to virtual machine + +Create an SSH connection with the VM. + +1. If you are on a Mac or Linux machine, open a Bash prompt and set read-only permission on the .pem file using `chmod 400 ~/Downloads/myKey.pem`. If you are on a Windows machine, open a PowerShell prompt. + +1. At your prompt, open an SSH connection to your virtual machine. Replace the IP address with the one from your VM, and replace the path to the `.pem` with the path to where the key file was downloaded. + +```console +ssh -i ~/Downloads/myKey.pem azureuser@10.111.12.123 +``` + +> [!TIP] +> The SSH key you created can be used the next time your create a VM in Azure. Just select the **Use a key stored in Azure** for **SSH public key source** the next time you create a VM. You already have the private key on your computer, so you won't need to download anything. + +## Install web server + +To see your VM in action, install the NGINX web server. From your SSH session, update your package sources and then install the latest NGINX package. + +```bash +sudo apt-get -y update +sudo apt-get -y install nginx +``` + +When done, type `exit` to leave the SSH session. + + +## View the web server in action + +Use a web browser of your choice to view the default NGINX welcome page. Type the public IP address of the VM as the web address. The public IP address can be found on the VM overview page or as part of the SSH connection string you used earlier. + +![Screenshot showing the NGINX default site in a browser](./media/quick-create-portal/nginx.png) + +## Clean up resources + +When no longer needed, you can delete the resource group, virtual machine, and all related resources. To do so, select the resource group for the virtual machine, select **Delete**, then confirm the name of the resource group to delete. + +## Next steps + +In this quickstart, you deployed a simple virtual machine, created a Network Security Group and rule, and installed a basic web server. To learn more about Azure virtual machines, continue to the tutorial for Linux VMs. + +> [!div class="nextstepaction"] +> [Azure Linux virtual machine tutorials](./tutorial-manage-vm.md) diff --git a/testScripts/test.md b/testScripts/test.md new file mode 100644 index 00000000..164f31b3 --- /dev/null +++ b/testScripts/test.md @@ -0,0 +1,172 @@ +--- +title: 'Quickstart: Use the Azure CLI to create a Linux VM' +--- + +# Prerequisites + +Innovation Engine can process prerequisites for documents. This code section tests that the pre requisites functionality works in Innovation Engine. +It will run the following real prerequisites along with a look for and fail to run a fake prerequisite. + +You must have completed [Fuzzy Matching Test](testScripts/fuzzyMatchTest.md) and you must have completed [Comment Test](testScripts/CommentTest.md) + +You also need to have completed [This is a fake file](testScripts/fakefile.md) + +And there are going to be additional \ and ( to throw off the algorithm... + +# Running simple bash commands + +Innovation engine can execute bash commands. For example + + +```bash +echo "Hello World" +``` + +# Test Code block with expected output + +```azurecli-interactive +echo "Hello \ +world" +``` + +It also can test the output to make sure everything ran as planned. + +``` +Hello world +``` + +# Test non-executable code blocks +If a code block does not have an executable tag it will simply render the codeblock as text + +For example: + +```YAML +apiVersion: apps/v1 +kind: Deployment +metadata: + name: azure-vote-back +spec: + replicas: 1 + selector: + matchLabels: + app: azure-vote-back + template: + metadata: + labels: + app: azure-vote-back + spec: + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: azure-vote-back + image: mcr.microsoft.com/oss/bitnami/redis:6.0.8 + env: + - name: ALLOW_EMPTY_PASSWORD + value: "yes" + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 250m + memory: 256Mi + ports: + - containerPort: 6379 + name: redis +--- +apiVersion: v1 +kind: Service +metadata: + name: azure-vote-back +spec: + ports: + - port: 6379 + selector: + app: azure-vote-back +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: azure-vote-front +spec: + replicas: 1 + selector: + matchLabels: + app: azure-vote-front + template: + metadata: + labels: + app: azure-vote-front + spec: + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: azure-vote-front + image: mcr.microsoft.com/azuredocs/azure-vote-front:v1 + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 250m + memory: 256Mi + ports: + - containerPort: 80 + env: + - name: REDIS + value: "azure-vote-back" +--- +apiVersion: v1 +kind: Service +metadata: + name: azure-vote-front +spec: + type: LoadBalancer + ports: + - port: 80 + selector: + app: azure-vote-front + +``` + +# Testing regular comments + +Innovation engine is able to handle comments and actual do fancy things with special comments. + +There are comments you can't see here. + + + + + +# Testing Declaring Environment Variables from Comments +Innovation Engine can declare environment variables via hidden inline comments. This feature is useful for running documents E2E as part of CI/CD + + + + +```azurecli-interactive +echo $MY_VARIABLE +``` + + +# Test Running an Azure Command +```azurecli-interactive +az group exists --name MyResourceGroup +``` + +# Next Steps + +These are the next steps... at some point we need to do something here \ No newline at end of file diff --git a/testScripts/variableHierarchy.ini b/testScripts/variableHierarchy.ini new file mode 100644 index 00000000..84c5cad7 --- /dev/null +++ b/testScripts/variableHierarchy.ini @@ -0,0 +1,2 @@ +MY_RESOURCE_GROUP = SetInINI +MY_VARIABLE_NAME = VariableFromIni \ No newline at end of file diff --git a/testScripts/variableHierarchy.md b/testScripts/variableHierarchy.md new file mode 100644 index 00000000..003d0e34 --- /dev/null +++ b/testScripts/variableHierarchy.md @@ -0,0 +1,26 @@ +This document is to show the hierarchy of environment variables + + + + +```bash +echo $MY_RESOURCE_GROUP +echo $MY_VARIABLE_NAME +``` + +# The following will now declare variables locally which will overwrite comment variables + +```bash +export MY_RESOURCE_GROUP=RGSetLocally +export MY_VARIABLE_NAME=LocallySetVariable +``` + +```bash +echo $MY_RESOURCE_GROUP +echo $MY_VARIABLE_NAME +``` diff --git a/tutorial.md b/tutorial.md new file mode 100644 index 00000000..30772fd4 --- /dev/null +++ b/tutorial.md @@ -0,0 +1,124 @@ +# Welcome to the innovation Engine Tutorial +## *TODO ADD MORE DETAIL TO IMPROVE TUTORIAL* + +# Running simple bash commands + +Innovation engine can execute bash commands. For example + +```bash +echo "Hello World" +``` + +# Test Code block with expected output + +```azurecli-interactive +echo "Hello \ +world" +``` + +It also can test the output to make sure everything ran as planned. + +``` +Hello world +``` + +# Executable vs non-executable code blocks +Innovation engine supports code blocks which are both executable and non-executable. A code block is executable if the label/tag after the bash scripts is one of the supported executable tags. Those tags are: bash, terraform, azurecli-interactive, and azurecli. + +If a code block has a non supported tag like YAML or HTML it will simply render the code block as text and continue parsing the document. + +For example: + +```YAML +apiVersion: apps/v1 +kind: Deployment +metadata: + name: azure-vote-back +spec: + replicas: 1 + selector: + matchLabels: + app: azure-vote-back + template: + metadata: + labels: + app: azure-vote-back + spec: + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: azure-vote-back + image: mcr.microsoft.com/oss/bitnami/redis:6.0.8 + env: + - name: ALLOW_EMPTY_PASSWORD + value: "yes" + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 250m + memory: 256Mi + ports: + - containerPort: 6379 + name: redis +--- +apiVersion: v1 +kind: Service +metadata: + name: azure-vote-back +spec: + ports: + - port: 6379 + selector: + app: azure-vote-back +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: azure-vote-front +spec: + replicas: 1 + selector: + matchLabels: + app: azure-vote-front + template: + metadata: + labels: + app: azure-vote-front + spec: + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: azure-vote-front + image: mcr.microsoft.com/azuredocs/azure-vote-front:v1 + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 250m + memory: 256Mi + ports: + - containerPort: 80 + env: + - name: REDIS + value: "azure-vote-back" +--- +apiVersion: v1 +kind: Service +metadata: + name: azure-vote-front +spec: + type: LoadBalancer + ports: + - port: 80 + selector: + app: azure-vote-front + +``` + + +# Next Steps + +These are the next steps... at some point we need to do something here \ No newline at end of file From 748ac7f31d835af8e15b441f7e0be657363a007e Mon Sep 17 00:00:00 2001 From: rgardler-msft Date: Fri, 10 Feb 2023 14:24:51 -0800 Subject: [PATCH 006/226] Improve getting started docs (#11) * Improve getting started docs 1. fix capitlization error 2. use virtual environment 3. separate installation from running * Switch to interactive mode Co-authored-by: jasonmesser7 <91570951+jasonmesser7@users.noreply.github.com> --------- Co-authored-by: jasonmesser7 <91570951+jasonmesser7@users.noreply.github.com> --- README.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index b7c46498..2fb14f95 100644 --- a/README.md +++ b/README.md @@ -8,18 +8,20 @@ Executable documentation takes standard markdown language and amplifies it by al # Try Out Executable Documentation Azure Cloud Shell provides an environment with all of the prerequisites installed to run Executable Documentation. This is the recommended method for new users to try and develop tutorials for Innovation Engine. -Open [Azure Cloud Shell](https://ms.portal.azure.com/#cloudshell/) and select Bash as the environment. - ->**Note** This snippet clones the Innovation Engine repo, installs necessary dependencies, and runs the interactive Innovation Engine tutorial script. +Open [Azure Cloud Shell](https://ms.portal.azure.com/#cloudshell/) and select Bash as the environment. Paste the following commands into the shell, this will clone the Innovation Engine repo, create a virtual environment and install the necessary dependencies. ```bash -git clone https://github.com/Azure/InnovationEngine - -cd innovationEngine +git clone https://github.com/Azure/InnovationEngine; +cd InnovationEngine; +virtualenv innovationEngine; +source innovationEngine/bin/activate +pip3 install -r requirements.txt; +``` -pip3 install -r requirements.txt +Now you can run the interactive Innovation Engine tutorial with the following command: -python3 main.py test tutorial.md +```bash +python3 main.py interactive tutorial.md ``` The general format to run an executable document is: @@ -166,4 +168,4 @@ This project may contain trademarks or logos for projects, products, or services trademarks or logos is subject to and must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. -Any use of third-party trademarks or logos are subject to those third-party's policies. \ No newline at end of file +Any use of third-party trademarks or logos are subject to those third-party's policies. From 55e1315d1a239f7840be998694de1e37249c295b Mon Sep 17 00:00:00 2001 From: jasonmesser7 <91570951+jasonmesser7@users.noreply.github.com> Date: Fri, 10 Feb 2023 15:31:50 -0800 Subject: [PATCH 007/226] bug fix which caused testing to fail on newer versions of bash (#13) --- executor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/executor.py b/executor.py index 19ee4485..c3200338 100644 --- a/executor.py +++ b/executor.py @@ -292,6 +292,7 @@ def get_shell(self): """ if self.shell == None: child = pexpect.spawnu('/bin/bash', echo=False, timeout=None) + child.sendline("bind 'set enable-bracketed-paste off'") ps1 = PEXPECT_PROMPT[:5] + u'\[\]' + PEXPECT_PROMPT[5:] ps2 = PEXPECT_CONTINUATION_PROMPT[:5] + u'\[\]' + PEXPECT_CONTINUATION_PROMPT[5:] prompt_change = u"PS1='{0}' PS2='{1}' PROMPT_COMMAND=''".format(ps1, ps2) From 880a2fde2974f669dd34ad121cfaded7b7000f38 Mon Sep 17 00:00:00 2001 From: jasonmesser7 <91570951+jasonmesser7@users.noreply.github.com> Date: Sat, 11 Feb 2023 17:20:11 -0800 Subject: [PATCH 008/226] Adding tutorial for AKS quickstart with event grid (#10) * first draft of AKS quick start with event grid notfications * Added tutorial with AKS create + Subscribing to event grid notifications * Add tests for all steps, plus a few docs improvements. --------- Co-authored-by: Ross Gardler --- demoScripts/aksQuickstartEventGrid.md | 556 ++++++++++++++++++++++++++ 1 file changed, 556 insertions(+) create mode 100644 demoScripts/aksQuickstartEventGrid.md diff --git a/demoScripts/aksQuickstartEventGrid.md b/demoScripts/aksQuickstartEventGrid.md new file mode 100644 index 00000000..3a111b93 --- /dev/null +++ b/demoScripts/aksQuickstartEventGrid.md @@ -0,0 +1,556 @@ +--- +title: Subscribe to Azure Kubernetes Service events with Azure Event Grid +description: Use Azure Event Grid to subscribe to Azure Kubernetes Service events +services: container-service +author: zr-msft +ms.topic: article +ms.date: 07/12/2021 +ms.author: zarhoads +--- + +# Quickstart: Subscribe to Azure Kubernetes Service (AKS) events with Azure Event Grid + +Azure Event Grid is a fully managed event routing service that provides uniform event consumption using a publish-subscribe model. + +In this quickstart, you'll create an AKS cluster and subscribe to AKS events. + +## Prerequisites + +* An Azure subscription. If you don't have an Azure subscription, you can create a [free account](https://azure.microsoft.com/free). +* [Azure CLI][azure-cli-install] or [Azure PowerShell][azure-powershell-install] installed. + +## Create an AKS cluster + +### [Azure CLI](#tab/azure-cli) + +## Define Environment Variables + +This document uses environment variables for all parameters to facilitate reuse. The default values provided here should work in most test environments. For production work you will obviously need to modify these values. + +```azurecli-interactive +export RESOURCE_GROUP_NAME=aksQuickstartResourceGroup +export RESOURCE_LOCATION=eastus +export AKS_CLUSTER_NAME=aksQuickstartCluster +export NAMESPACE_NAME="aksQuickstartNamespace$(printf "%08d" $((RANDOM%100000000)))" +export EVENT_GRID_HUB_NAME=aksQuickstartEventGridHub +export EVENT_GRID_SUBSCRIPTION_NAME=aksQuickstartEventGridSubscription +``` + +## Create an AKS Cluster + +Create an AKS cluster using the [az aks create][az-aks-create] command. The following example creates a resource group and a cluster with one node. They will be named according to the environment variables set above: + +```azurecli-interactive +az group create --name $RESOURCE_GROUP_NAME --location $RESOURCE_LOCATION +``` + + +```output +{ + "id": "/subscriptions/325e7c34-99fb-4190-aa87-1df746c67705/resourceGroups/aksQuickstartResourceGroup", + "location": "eastus", + "managedBy": null, + "name": "aksQuickstartResourceGroup", + "properties": { + "provisioningState": "Succeeded" + }, + "tags": null, + "type": "Microsoft.Resources/resourceGroups" +} +``` + +Now we can create an AKS cluster within that resource group. + +```azurecli-interactive +az aks create --resource-group $RESOURCE_GROUP_NAME --name $AKS_CLUSTER_NAME --location $RESOURCE_LOCATION --node-count 1 --generate-ssh-keys +``` + +This will take a little while to run, when it completes you should see an output that looks something like this: + + +```output +{ + "aadProfile": null, + "addonProfiles": null, + "agentPoolProfiles": [ + { + "availabilityZones": null, + "count": 1, + "creationData": null, + "currentOrchestratorVersion": "1.24.9", + "enableAutoScaling": false, + "enableEncryptionAtHost": false, + "enableFips": false, + "enableNodePublicIp": false, + "enableUltraSsd": false, + "gpuInstanceProfile": null, + "hostGroupId": null, + "kubeletConfig": null, + "kubeletDiskType": "OS", + "linuxOsConfig": null, + "maxCount": null, + "maxPods": 110, + "minCount": null, + "mode": "System", + "name": "nodepool1", + "nodeImageVersion": "AKSUbuntu-1804gen2containerd-2023.01.20", + "nodeLabels": null, + "nodePublicIpPrefixId": null, + "nodeTaints": null, + "orchestratorVersion": "1.24.9", + "osDiskSizeGb": 128, + "osDiskType": "Managed", + "osSku": "Ubuntu", + "osType": "Linux", + "podSubnetId": null, + "powerState": { + "code": "Running" + }, + "provisioningState": "Succeeded", + "proximityPlacementGroupId": null, + "scaleDownMode": null, + "scaleSetEvictionPolicy": null, + "scaleSetPriority": null, + "spotMaxPrice": null, + "tags": null, + "type": "VirtualMachineScaleSets", + "upgradeSettings": { + "maxSurge": null + }, + "vmSize": "Standard_DS2_v2", + "vnetSubnetId": null, + "workloadRuntime": null + } + ], + "apiServerAccessProfile": null, + "autoScalerProfile": null, + "autoUpgradeProfile": null, + "azurePortalFqdn": "aksquickst-aksquickstartres-325e7c-784c55cf.portal.hcp.eastus.azmk8s.io", + "currentKubernetesVersion": "1.24.9", + "disableLocalAccounts": false, + "diskEncryptionSetId": null, + "dnsPrefix": "aksQuickst-aksQuickstartRes-325e7c", + "enablePodSecurityPolicy": null, + "enableRbac": true, + "extendedLocation": null, + "fqdn": "aksquickst-aksquickstartres-325e7c-784c55cf.hcp.eastus.azmk8s.io", + "fqdnSubdomain": null, + "httpProxyConfig": null, + "id": "/subscriptions/325e7c34-99fb-4190-aa87-1df746c67705/resourcegroups/aksQuickstartResourceGroup/providers/Microsoft.ContainerService/managedClusters/aksQuickstartCluster", + "identity": { + "principalId": "REDACTED", + "tenantId": "REDACTED", + "type": "SystemAssigned", + "userAssignedIdentities": null + }, + "identityProfile": { + "kubeletidentity": { + "clientId": "REDACTED", + "objectId": "REDACTED", + "resourceId": "/subscriptions/REDACTED/resourcegroups/MC_aksQuickstartResourceGroup_aksQuickstartCluster_eastus/providers/Microsoft.ManagedIdentity/userAssignedIdentities/aksQuickstartCluster-agentpool" + } + }, + "kubernetesVersion": "1.24.9", + "linuxProfile": { + "adminUsername": "azureuser", + "ssh": { + "publicKeys": [ + { + "keyData": "ssh-rsa REDACTED" + } + ] + } + }, + "location": "eastus", + "maxAgentPools": 100, + "name": "aksQuickstartCluster", + "networkProfile": { + "dnsServiceIp": "10.0.0.10", + "dockerBridgeCidr": "172.17.0.1/16", + "ipFamilies": [ + "IPv4" + ], + "loadBalancerProfile": { + "allocatedOutboundPorts": null, + "effectiveOutboundIPs": [ + { + "id": "/subscriptions/REDACTED/resourceGroups/MC_aksQuickstartResourceGroup_aksQuickstartCluster_eastus/providers/Microsoft.Network/publicIPAddresses/e19ddc6c-0842-45d5-814d-702cc95945ce", + "resourceGroup": "MC_aksQuickstartResourceGroup_aksQuickstartCluster_eastus" + } + ], + "enableMultipleStandardLoadBalancers": null, + "idleTimeoutInMinutes": null, + "managedOutboundIPs": { + "count": 1, + "countIpv6": null + }, + "outboundIPs": null, + "outboundIpPrefixes": null + }, + "loadBalancerSku": "Standard", + "natGatewayProfile": null, + "networkMode": null, + "networkPlugin": "kubenet", + "networkPolicy": null, + "outboundType": "loadBalancer", + "podCidr": "10.244.0.0/16", + "podCidrs": [ + "10.244.0.0/16" + ], + "serviceCidr": "10.0.0.0/16", + "serviceCidrs": [ + "10.0.0.0/16" + ] + }, + "nodeResourceGroup": "MC_aksQuickstartResourceGroup_aksQuickstartCluster_eastus", + "oidcIssuerProfile": { + "enabled": false, + "issuerUrl": null + }, + "podIdentityProfile": null, + "powerState": { + "code": "Running" + }, + "privateFqdn": null, + "privateLinkResources": null, + "provisioningState": "Succeeded", + "publicNetworkAccess": null, + "resourceGroup": "aksQuickstartResourceGroup", + "securityProfile": { + "azureKeyVaultKms": null, + "defender": null + }, + "servicePrincipalProfile": { + "clientId": "msi", + "secret": null + }, + "sku": { + "name": "Basic", + "tier": "Free" + }, + "storageProfile": { + "blobCsiDriver": null, + "diskCsiDriver": { + "enabled": true + }, + "fileCsiDriver": { + "enabled": true + }, + "snapshotController": { + "enabled": true + } + }, + "systemData": null, + "tags": null, + "type": "Microsoft.ContainerService/ManagedClusters", + "windowsProfile": null +} +``` + +### [Azure PowerShell](#tab/azure-powershell) + +Create an AKS cluster using the [New-AzAksCluster][new-azakscluster] command. The following example creates a resource group *MyResourceGroup* and a cluster named *MyAKS* with one node in the *MyResourceGroup* resource group: + +```azurepowershell-interactive +New-AzResourceGroup -Name MyResourceGroup -Location eastus +New-AzAksCluster -ResourceGroupName MyResourceGroup -Name MyAKS -Location eastus -NodeCount 1 -GenerateSshKey +``` + +--- + +## Subscribe to AKS events + +### [Azure CLI](#tab/azure-cli) + +Create a namespace and event hub using [az eventhubs namespace create][az-eventhubs-namespace-create] and [az eventhubs eventhub create][az-eventhubs-eventhub-create]. The following example creates a namespace *MyNamespace* and an event hub *MyEventGridHub* in *MyNamespace*, both in the *MyResourceGroup* resource group. + +```azurecli-interactive +az eventhubs namespace create --location $RESOURCE_LOCATION --name $NAMESPACE_NAME --resource-group $RESOURCE_GROUP_NAME +``` + + +```output +{ + "alternateName": null, + "clusterArmId": null, + "createdAt": "2023-02-11T00:27:48.977000+00:00", + "disableLocalAuth": false, + "encryption": null, + "id": "/subscriptions/325e7c34-99fb-4190-aa87-1df746c67705/resourceGroups/aksQuickstartResourceGroup/providers/Microsoft.EventHub/namespaces/aksQuickstartNamespace00021677", + "identity": null, + "isAutoInflateEnabled": false, + "kafkaEnabled": true, + "location": "East US", + "maximumThroughputUnits": 0, + "metricId": "325e7c34-99fb-4190-aa87-1df746c67705:aksquickstartnamespace00021677", + "minimumTlsVersion": "1.2", + "name": "aksQuickstartNamespace00021677", + "privateEndpointConnections": null, + "provisioningState": "Succeeded", + "publicNetworkAccess": "Enabled", + "resourceGroup": "aksQuickstartResourceGroup", + "serviceBusEndpoint": "https://aksQuickstartNamespace00021677.servicebus.windows.net:443/", + "sku": { + "capacity": 1, + "name": "Standard", + "tier": "Standard" + }, + "status": "Active", + "systemData": null, + "tags": {}, + "type": "Microsoft.EventHub/Namespaces", + "updatedAt": "2023-02-11T00:28:40.050000+00:00", + "zoneRedundant": false +} +``` + +```azurecli-interactive +az eventhubs eventhub create --name $EVENT_GRID_HUB_NAME --namespace-name $NAMESPACE_NAME --resource-group $RESOURCE_GROUP_NAME +``` + + +```output +{ + "alternateName": null, + "clusterArmId": null, + "createdAt": "2023-02-11T00:27:48.977000+00:00", + "disableLocalAuth": false, + "encryption": null, + "id": "/subscriptions/325e7c34-99fb-4190-aa87-1df746c67705/resourceGroups/aksQuickstartResourceGroup/providers/Microsoft.EventHub/namespaces/aksQuickstartNamespace00021677", + "identity": null, + "isAutoInflateEnabled": false, + "kafkaEnabled": true, + "location": "East US", + "maximumThroughputUnits": 0, + "metricId": "325e7c34-99fb-4190-aa87-1df746c67705:aksquickstartnamespace00021677", + "minimumTlsVersion": "1.2", + "name": "aksQuickstartNamespace00021677", + "privateEndpointConnections": null, + "provisioningState": "Succeeded", + "publicNetworkAccess": "Enabled", + "resourceGroup": "aksQuickstartResourceGroup", + "serviceBusEndpoint": "https://aksQuickstartNamespace00021677.servicebus.windows.net:443/", + "sku": { + "capacity": 1, + "name": "Standard", + "tier": "Standard" + }, + "status": "Active", + "systemData": null, + "tags": {}, + "type": "Microsoft.EventHub/Namespaces", + "updatedAt": "2023-02-11T00:29:54.450000+00:00", + "zoneRedundant": false +} +``` + +> [!NOTE] +> The *name* of your namespace must be unique. In the defaults above we set a random postfix to try to ensure it is unique, but this is not guaranteed. + +Subscribe to the AKS events using [az eventgrid event-subscription create][az-eventgrid-event-subscription-create]: + +First we need the resource ID and endpoint, which we will store in an environment variables for later use: + +```azurecli-interactive +SOURCE_RESOURCE_ID=$(az aks show -g $RESOURCE_GROUP_NAME -n $AKS_CLUSTER_NAME --query id --output tsv) +ENDPOINT=$(az eventhubs eventhub show -g $RESOURCE_GROUP_NAME -n $EVENT_GRID_HUB_NAME --namespace-name $NAMESPACE_NAME --query id --output tsv) +``` + +Now we can actually subscribe to the events: + +```azurecli-interactive +az eventgrid event-subscription create --name $EVENT_GRID_SUBSCRIPTION_NAME \ + --source-resource-id $SOURCE_RESOURCE_ID \ + --endpoint-type eventhub \ + --endpoint $ENDPOINT +``` + + +```output +{ + "deadLetterDestination": null, + "deadLetterWithResourceIdentity": null, + "deliveryWithResourceIdentity": null, + "destination": { + "deliveryAttributeMappings": null, + "endpointType": "EventHub", + "resourceId": "/subscriptions/REDACTED/resourceGroups/aksQuickstartResourceGroup/providers/Microsoft.EventHub/namespaces/aksQuickstartNamespace00006800/eventhubs/aksQuickstartEventGridHub" + }, + "eventDeliverySchema": "EventGridSchema", + "expirationTimeUtc": null, + "filter": { + "advancedFilters": null, + "enableAdvancedFilteringOnArrays": null, + "includedEventTypes": [ + "Microsoft.ContainerService.NewKubernetesVersionAvailable" + ], + "isSubjectCaseSensitive": null, + "subjectBeginsWith": "", + "subjectEndsWith": "" + }, + "id": "/subscriptions/REDACTED/resourceGroups/aksQuickstartResourceGroup/providers/Microsoft.ContainerService/managedClusters/aksQuickstartCluster/providers/Microsoft.EventGrid/eventSubscriptions/aksQuickstartEventGridSubscription", + "labels": null, + "name": "aksQuickstartEventGridSubscription", + "provisioningState": "Succeeded", + "resourceGroup": "aksQuickstartResourceGroup", + "retryPolicy": { + "eventTimeToLiveInMinutes": 1440, + "maxDeliveryAttempts": 30 + }, + "systemData": null, + "topic": "/subscriptions/REDACTED/resourceGroups/aksquickstartresourcegroup/providers/microsoft.containerservice/managedclusters/aksquickstartcluster", + "type": "Microsoft.EventGrid/eventSubscriptions" +} +``` + +Verify your subscription to AKS events using `az eventgrid event-subscription list`: + +```azurecli-interactive +az eventgrid event-subscription list --source-resource-id $SOURCE_RESOURCE_ID +``` + +The following example output shows you're subscribed to events from the *MyAKS* cluster and those events are delivered to the *MyEventGridHub* event hub: + +```output +[ + { + "deadLetterDestination": null, + "deadLetterWithResourceIdentity": null, + "deliveryWithResourceIdentity": null, + "destination": { + "deliveryAttributeMappings": null, + "endpointType": "EventHub", + "resourceId": "/subscriptions/SUBSCRIPTION_ID/resourceGroups/MyResourceGroup/providers/Microsoft.EventHub/namespaces/MyNamespace/eventhubs/MyEventGridHub" + }, + "eventDeliverySchema": "EventGridSchema", + "expirationTimeUtc": null, + "filter": { + "advancedFilters": null, + "enableAdvancedFilteringOnArrays": null, + "includedEventTypes": [ + "Microsoft.ContainerService.NewKubernetesVersionAvailable" + ], + "isSubjectCaseSensitive": null, + "subjectBeginsWith": "", + "subjectEndsWith": "" + }, + "id": "/subscriptions/SUBSCRIPTION_ID/resourceGroups/MyResourceGroup/providers/Microsoft.ContainerService/managedClusters/MyAKS/providers/Microsoft.EventGrid/eventSubscriptions/MyEventGridSubscription", + "labels": null, + "name": "MyEventGridSubscription", + "provisioningState": "Succeeded", + "resourceGroup": "MyResourceGroup", + "retryPolicy": { + "eventTimeToLiveInMinutes": 1440, + "maxDeliveryAttempts": 30 + }, + "systemData": null, + "topic": "/subscriptions/SUBSCRIPTION_ID/resourceGroups/MyResourceGroup/providers/microsoft.containerservice/managedclusters/MyAKS", + "type": "Microsoft.EventGrid/eventSubscriptions" + } +] +``` + +### [Azure PowerShell](#tab/azure-powershell) + +Create a namespace and event hub using [New-AzEventHubNamespace][new-azeventhubnamespace] and [New-AzEventHub][new-azeventhub]. The following example creates a namespace *MyNamespace* and an event hub *MyEventGridHub* in *MyNamespace*, both in the *MyResourceGroup* resource group. + +```azurepowershell-interactive +New-AzEventHubNamespace -Location eastus -Name MyNamespace -ResourceGroupName $RESOURCE_GROUP_NAME +New-AzEventHub -Name MyEventGridHub -Namespace MyNamespace -ResourceGroupName $RESOURCE_GROUP_NAME +``` + +> [!NOTE] +> The *name* of your namespace must be unique. + +Subscribe to the AKS events using [New-AzEventGridSubscription][new-azeventgridsubscription]: + +```azurepowershell-interactive +$SOURCE_RESOURCE_ID = (Get-AzAksCluster -ResourceGroupName MyResourceGroup -Name MyAKS).Id +$ENDPOINT = (Get-AzEventHub -ResourceGroupName MyResourceGroup -EventHubName MyEventGridHub -Namespace MyNamespace).Id +$params = @{ + EventSubscriptionName = 'MyEventGridSubscription' + ResourceId = $SOURCE_RESOURCE_ID + EndpointType = 'eventhub' + Endpoint = $ENDPOINT +} +New-AzEventGridSubscription @params +``` + +Verify your subscription to AKS events using `Get-AzEventGridSubscription`: + +```azurepowershell-interactive +Get-AzEventGridSubscription -ResourceId $SOURCE_RESOURCE_ID | Select-Object -ExpandProperty PSEventSubscriptionsList +``` + +The following example output shows you're subscribed to events from the *MyAKS* cluster and those events are delivered to the *MyEventGridHub* event hub: + +```Output +EventSubscriptionName : MyEventGridSubscription +Id : /subscriptions/SUBSCRIPTION_ID/resourceGroups/MyResourceGroup/providers/Microsoft.ContainerService/managedClusters/MyAKS/providers/Microsoft.EventGrid/eventSubscriptions/MyEventGridSubscription +Type : Microsoft.EventGrid/eventSubscriptions +Topic : /subscriptions/SUBSCRIPTION_ID/resourceGroups/myresourcegroup/providers/microsoft.containerservice/managedclusters/myaks +Filter : Microsoft.Azure.Management.EventGrid.Models.EventSubscriptionFilter +Destination : Microsoft.Azure.Management.EventGrid.Models.EventHubEventSubscriptionDestination +ProvisioningState : Succeeded +Labels : +EventTtl : 1440 +MaxDeliveryAttempt : 30 +EventDeliverySchema : EventGridSchema +ExpirationDate : +DeadLetterEndpoint : +Endpoint : /subscriptions/SUBSCRIPTION_ID/resourceGroups/MyResourceGroup/providers/Microsoft.EventHub/namespaces/MyNamespace/eventhubs/MyEventGridHub +``` + +--- + +When AKS events occur, you'll see those events appear in your event hub. For example, when the list of available Kubernetes versions for your clusters changes, you'll see a `Microsoft.ContainerService.NewKubernetesVersionAvailable` event. For more information on the events AKS emits, see [Azure Kubernetes Service (AKS) as an Event Grid source][aks-events]. + +## Delete the cluster and subscriptions + +### [Azure CLI](#tab/azure-cli) + +Use the [az group delete][az-group-delete] command to remove the resource group, the AKS cluster, namespace, and event hub, and all related resources. + +```azurecli-interactive +az group delete --name $RESOURCE_GROUP_NAME --yes --no-wait +``` + +### [Azure PowerShell](#tab/azure-powershell) + +Use the [Remove-AzResourceGroup][remove-azresourcegroup] cmdlet to remove the resource group, the AKS cluster, namespace, and event hub, and all related resources. + +```azurepowershell-interactive +Remove-AzResourceGroup -Name MyResourceGroup +``` + +--- + +> [!NOTE] +> When you delete the cluster, the Azure Active Directory service principal used by the AKS cluster is not removed. For steps on how to remove the service principal, see [AKS service principal considerations and deletion][sp-delete]. +> +> If you used a managed identity, the identity is managed by the platform and does not require removal. + +## Next steps + +In this quickstart, you deployed a Kubernetes cluster and then subscribed to AKS events in Azure Event Hubs. + +To learn more about AKS, and walk through a complete code to deployment example, continue to the Kubernetes cluster tutorial. + +> [!div class="nextstepaction"] +> [AKS tutorial][aks-tutorial] + +[azure-cli-install]: /cli/azure/install-azure-cli +[azure-powershell-install]: /powershell/azure/install-az-ps +[aks-events]: ../event-grid/event-schema-aks.md +[aks-tutorial]: ./tutorial-kubernetes-prepare-app.md +[az-aks-create]: /cli/azure/aks#az_aks_create +[new-azakscluster]: /powershell/module/az.aks/new-azakscluster +[az-eventhubs-namespace-create]: /cli/azure/eventhubs/namespace#az-eventhubs-namespace-create +[new-azeventhubnamespace]: /powershell/module/az.eventhub/new-azeventhubnamespace +[az-eventhubs-eventhub-create]: /cli/azure/eventhubs/eventhub#az-eventhubs-eventhub-create +[new-azeventhub]: /powershell/module/az.eventhub/new-azeventhub +[az-eventgrid-event-subscription-create]: /cli/azure/eventgrid/event-subscription#az-eventgrid-event-subscription-create +[new-azeventgridsubscription]: /powershell/module/az.eventgrid/new-azeventgridsubscription +[az-group-delete]: /cli/azure/group#az_group_delete +[sp-delete]: kubernetes-service-principal.md#other-considerations +[remove-azresourcegroup]: /powershell/module/az.resources/remove-azresourcegroup \ No newline at end of file From 9be4e305f61739246ae9d5ffdd83d3c49cd68ed7 Mon Sep 17 00:00:00 2001 From: jasonmesser7 <91570951+jasonmesser7@users.noreply.github.com> Date: Wed, 22 Feb 2023 09:44:27 -0800 Subject: [PATCH 009/226] Adding exit 1 back into testing. This was taken our originally for multi tenancy, however we need this signal for an action to fail and block a PR (#15) --- executor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/executor.py b/executor.py index c3200338..33d53850 100644 --- a/executor.py +++ b/executor.py @@ -109,8 +109,8 @@ def runMainLoopTest(self): for failedTest in self.failedTests: print(failedTest[0][1].value + '\n') print(failedTest[1]) - #Taking out the exit 1 command as this will end the testing if we are testing multiple files - #exit(1) + + exit(1) # This function runs through and executes not pausing for any input or failing. # The primary intention of this is for executing pre requisites From bc5e14c16ee1cf2fa019ee86d6f3a4552fef997b Mon Sep 17 00:00:00 2001 From: vmarcella Date: Wed, 19 Apr 2023 18:20:05 -0700 Subject: [PATCH 010/226] [add] initial golang setup. --- cmd/ie/commands/root.go | 20 ++++++++++++++++++++ cmd/ie/ie.go | 7 +++++++ go.mod | 9 +++++++++ go.sum | 10 ++++++++++ 4 files changed, 46 insertions(+) create mode 100644 cmd/ie/commands/root.go create mode 100644 cmd/ie/ie.go create mode 100644 go.mod create mode 100644 go.sum diff --git a/cmd/ie/commands/root.go b/cmd/ie/commands/root.go new file mode 100644 index 00000000..f809e5f5 --- /dev/null +++ b/cmd/ie/commands/root.go @@ -0,0 +1,20 @@ +package commands + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +var rootCommand = &cobra.Command{ + Use: "ie", + Short: "The innovation engine.", +} + +func ExecuteCLI() { + if err := rootCommand.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/cmd/ie/ie.go b/cmd/ie/ie.go new file mode 100644 index 00000000..ea115e84 --- /dev/null +++ b/cmd/ie/ie.go @@ -0,0 +1,7 @@ +package main + +import "github.com/Azure/InnovationEngine/cmd/ie/commands" + +func main() { + commands.ExecuteCLI() +} diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..529453ba --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/Azure/InnovationEngine + +go 1.20 + +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/cobra v1.7.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..f3366a91 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From a2d77b6042b836d83a12b857c0aa5077c6dad9d1 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Wed, 19 Apr 2023 18:51:39 -0700 Subject: [PATCH 011/226] [add] other subcommands & start laying out the engine. --- cmd/ie/commands/execute.go | 15 +++++++++++++++ cmd/ie/commands/interactive.go | 15 +++++++++++++++ cmd/ie/commands/root.go | 1 + cmd/ie/commands/test.go | 15 +++++++++++++++ internal/engine/engine.go | 1 + 5 files changed, 47 insertions(+) create mode 100644 cmd/ie/commands/execute.go create mode 100644 cmd/ie/commands/interactive.go create mode 100644 cmd/ie/commands/test.go create mode 100644 internal/engine/engine.go diff --git a/cmd/ie/commands/execute.go b/cmd/ie/commands/execute.go new file mode 100644 index 00000000..bbb620f3 --- /dev/null +++ b/cmd/ie/commands/execute.go @@ -0,0 +1,15 @@ +package commands + +import ( + "github.com/spf13/cobra" +) + +var executeCommand = &cobra.Command{ + Use: "execute", + Short: "Execute a document.", +} + +// / Register the command with our command runner. +func init() { + rootCommand.AddCommand(executeCommand) +} diff --git a/cmd/ie/commands/interactive.go b/cmd/ie/commands/interactive.go new file mode 100644 index 00000000..064a3581 --- /dev/null +++ b/cmd/ie/commands/interactive.go @@ -0,0 +1,15 @@ +package commands + +import ( + "github.com/spf13/cobra" +) + +var interactiveCommand = &cobra.Command{ + Use: "interactive", + Short: "Execute a document in interactive mode.", +} + +// / Register the command with our command runner. +func init() { + rootCommand.AddCommand(interactiveCommand) +} diff --git a/cmd/ie/commands/root.go b/cmd/ie/commands/root.go index f809e5f5..fff85cce 100644 --- a/cmd/ie/commands/root.go +++ b/cmd/ie/commands/root.go @@ -12,6 +12,7 @@ var rootCommand = &cobra.Command{ Short: "The innovation engine.", } +// Entrypoint into the Innovation Engine CLI. func ExecuteCLI() { if err := rootCommand.Execute(); err != nil { fmt.Println(err) diff --git a/cmd/ie/commands/test.go b/cmd/ie/commands/test.go new file mode 100644 index 00000000..4128e16d --- /dev/null +++ b/cmd/ie/commands/test.go @@ -0,0 +1,15 @@ +package commands + +import ( + "github.com/spf13/cobra" +) + +var testCommand = &cobra.Command{ + Use: "test", + Short: "Test document commands against it's expected outputs.", +} + +// / Register the command with our command runner. +func init() { + rootCommand.AddCommand(testCommand) +} diff --git a/internal/engine/engine.go b/internal/engine/engine.go new file mode 100644 index 00000000..00a22ef6 --- /dev/null +++ b/internal/engine/engine.go @@ -0,0 +1 @@ +package engine From cb6d5aa3cad1e732cfa7ed2ecd939b634639ecc6 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Wed, 19 Apr 2023 19:22:12 -0700 Subject: [PATCH 012/226] [add] makefile with build + run commands. --- .gitignore | 5 ++++- Makefile | 27 +++++++++++++++++++++++++++ cmd/api/main.go | 7 +++++++ go.mod | 2 +- internal/engine/engine.go | 3 +++ 5 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 Makefile create mode 100644 cmd/api/main.go diff --git a/.gitignore b/.gitignore index 0e516d73..3e8e5edf 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ __pycache__ #VS Code -.vscode \ No newline at end of file +.vscode + +# Ignore all binaries. +bin/ \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..aaf30a4d --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +.PHONY: build-ie build-api build-all run-ie run-api clean + +BINARY_DIR := bin +IE_BINARY := $(BINARY_DIR)/ie +API_BINARY := $(BINARY_DIR)/api + +build-ie: + @echo "Building the Innovation Engine CLI..." + @go build -o "$(IE_BINARY)" cmd/ie/ie.go + +build-api: + @echo "Building the Innovation Engine API..." + @go build -o "$(API_BINARY)" cmd/api/main.go + +build-all: build-ie build-api + +run-ie: build-ie + @echo "Running the Innovation Engine CLI" + @"$(IE_BINARY)" + +run-api: build-api + @echo "Running the Innovation Engine API" + @"$(API_BINARY)" + +clean: + @echo "Cleaning up" + @rm -rf "$(BINARY_DIR)" \ No newline at end of file diff --git a/cmd/api/main.go b/cmd/api/main.go new file mode 100644 index 00000000..f7b60bde --- /dev/null +++ b/cmd/api/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("Hello, world!") +} diff --git a/go.mod b/go.mod index 529453ba..e5e3d3a5 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,6 @@ go 1.20 require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/spf13/cobra v1.7.0 // indirect + github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 // indirect ) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 00a22ef6..ca679bf9 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -1 +1,4 @@ package engine + +type EngineConfiguration struct { +} From e2c39c2a16da5053da0091b610789d0520d21956 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 20 Apr 2023 15:53:38 -0700 Subject: [PATCH 013/226] [add] markdown parsing and codeblock extraction to retrieve commands from the markdown codeblocks. --- cmd/ie/commands/execute.go | 33 +++++++++++++++++--- go.mod | 4 ++- go.sum | 2 ++ internal/parsers/markdown.go | 60 ++++++++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 internal/parsers/markdown.go diff --git a/cmd/ie/commands/execute.go b/cmd/ie/commands/execute.go index bbb620f3..8a96e4a0 100644 --- a/cmd/ie/commands/execute.go +++ b/cmd/ie/commands/execute.go @@ -1,15 +1,40 @@ package commands import ( + "fmt" + "io/ioutil" + + "github.com/Azure/InnovationEngine/internal/parsers" "github.com/spf13/cobra" ) -var executeCommand = &cobra.Command{ - Use: "execute", - Short: "Execute a document.", -} +var markdownFile string // / Register the command with our command runner. func init() { rootCommand.AddCommand(executeCommand) + executeCommand.Flags().StringVar(&markdownFile, "markdown", "", "The markdown file to execute.") +} + +var executeCommand = &cobra.Command{ + Use: "execute", + Short: "Execute a document.", + Run: func(cmd *cobra.Command, args []string) { + if markdownFile == "" { + cmd.Help() + return + } + source, err := ioutil.ReadFile(markdownFile) + + if err != nil { + panic(err) + } + + markdown := parsers.ParseMarkdownIntoAst(source) + commands := parsers.ExtractCodeBlocksFromAst(markdown, source, []string{"bash", "azurecli", "azurecli-interactive", "terraform", "terraform-interactive"}) + + for _, command := range commands { + fmt.Println(command) + } + }, } diff --git a/go.mod b/go.mod index e5e3d3a5..572a234d 100644 --- a/go.mod +++ b/go.mod @@ -2,8 +2,10 @@ module github.com/Azure/InnovationEngine go 1.20 +require github.com/spf13/cobra v1.7.0 + require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 // indirect + github.com/yuin/goldmark v1.5.4 // indirect ) diff --git a/go.sum b/go.sum index f3366a91..deb00343 100644 --- a/go.sum +++ b/go.sum @@ -6,5 +6,7 @@ github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= +github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/parsers/markdown.go b/internal/parsers/markdown.go new file mode 100644 index 00000000..cf586190 --- /dev/null +++ b/internal/parsers/markdown.go @@ -0,0 +1,60 @@ +package parsers + +import ( + "strings" + + "github.com/yuin/goldmark" + "github.com/yuin/goldmark/ast" + "github.com/yuin/goldmark/extension" + "github.com/yuin/goldmark/parser" + "github.com/yuin/goldmark/renderer/html" + "github.com/yuin/goldmark/text" +) + +var markdownParser = goldmark.New( + goldmark.WithExtensions(extension.GFM), + goldmark.WithParserOptions( + parser.WithAutoHeadingID(), + parser.WithBlockParsers(), + ), + goldmark.WithRendererOptions( + html.WithXHTML(), + ), +) + +// Parses a markdown file into an AST representing the markdown document. +func ParseMarkdownIntoAst(source []byte) ast.Node { + document := markdownParser.Parser().Parse(text.NewReader(source)) + return document +} + +// Extracts the code blocks from a provided markdown AST that match the +// languagesToExtract. +func ExtractCodeBlocksFromAst(node ast.Node, source []byte, languagesToExtract []string) []string { + var commands []string + ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) { + if entering && node.Kind() == ast.KindFencedCodeBlock { + codeBlock := node.(*ast.FencedCodeBlock) + for _, language := range languagesToExtract { + if string(codeBlock.Language(source)) == language { + commands = append(commands, extractCommandFromCodeBlock(codeBlock, source)) + } + } + } + return ast.WalkContinue, nil + }) + + return commands +} + +func extractCommandFromCodeBlock(codeBlock *ast.FencedCodeBlock, source []byte) string { + lines := codeBlock.Lines() + var command strings.Builder + + for i := 0; i < lines.Len(); i++ { + line := lines.At(i) + command.WriteString(string(line.Value(source))) + } + + return command.String() +} From bca127d18ba3f87135c527fef2b2460bb8b46272 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 24 Apr 2023 09:29:33 -0700 Subject: [PATCH 014/226] [update] markdown file to be passed in as a positional argument. --- cmd/ie/commands/execute.go | 13 ++++++------- internal/parsers/markdown.go | 1 + 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cmd/ie/commands/execute.go b/cmd/ie/commands/execute.go index 8a96e4a0..6762284e 100644 --- a/cmd/ie/commands/execute.go +++ b/cmd/ie/commands/execute.go @@ -2,29 +2,28 @@ package commands import ( "fmt" - "io/ioutil" + "os" "github.com/Azure/InnovationEngine/internal/parsers" "github.com/spf13/cobra" ) -var markdownFile string - // / Register the command with our command runner. func init() { rootCommand.AddCommand(executeCommand) - executeCommand.Flags().StringVar(&markdownFile, "markdown", "", "The markdown file to execute.") } var executeCommand = &cobra.Command{ - Use: "execute", - Short: "Execute a document.", + Use: "execute [markdown file]", + Args: cobra.MinimumNArgs(1), + Short: "Execute the commands for an Azure deployment scenario.", Run: func(cmd *cobra.Command, args []string) { + markdownFile := args[0] if markdownFile == "" { cmd.Help() return } - source, err := ioutil.ReadFile(markdownFile) + source, err := os.ReadFile(markdownFile) if err != nil { panic(err) diff --git a/internal/parsers/markdown.go b/internal/parsers/markdown.go index cf586190..a57a4c1e 100644 --- a/internal/parsers/markdown.go +++ b/internal/parsers/markdown.go @@ -47,6 +47,7 @@ func ExtractCodeBlocksFromAst(node ast.Node, source []byte, languagesToExtract [ return commands } +// Extracts the command text from an already parsed markdown code block. func extractCommandFromCodeBlock(codeBlock *ast.FencedCodeBlock, source []byte) string { lines := codeBlock.Lines() var command strings.Builder From d38cd3b6a8a601402c5c846e5cde7904d474aeac Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 24 Apr 2023 09:54:06 -0700 Subject: [PATCH 015/226] [update] documentation for how to build & run IE. --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2fb14f95..ef1a9e27 100644 --- a/README.md +++ b/README.md @@ -8,20 +8,18 @@ Executable documentation takes standard markdown language and amplifies it by al # Try Out Executable Documentation Azure Cloud Shell provides an environment with all of the prerequisites installed to run Executable Documentation. This is the recommended method for new users to try and develop tutorials for Innovation Engine. -Open [Azure Cloud Shell](https://ms.portal.azure.com/#cloudshell/) and select Bash as the environment. Paste the following commands into the shell, this will clone the Innovation Engine repo, create a virtual environment and install the necessary dependencies. +Open [Azure Cloud Shell](https://ms.portal.azure.com/#cloudshell/) and select Bash as the environment. Paste the following commands into the shell, this will clone the Innovation Engine repo, install the requirements, and build out the innovation engine executable. ```bash git clone https://github.com/Azure/InnovationEngine; cd InnovationEngine; -virtualenv innovationEngine; -source innovationEngine/bin/activate -pip3 install -r requirements.txt; +make build-ie; ``` Now you can run the interactive Innovation Engine tutorial with the following command: ```bash -python3 main.py interactive tutorial.md +./bin/ie interactive tutorial.md ``` The general format to run an executable document is: From b3a7220ed847cf2afed65fd23fa9e922930bf637 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 25 Apr 2023 21:12:18 -0700 Subject: [PATCH 016/226] [add] INI parser, add command execution via bash, and handle command outputs from the top level. --- cmd/ie/commands/execute.go | 8 +++++++- go.mod | 1 + go.sum | 2 ++ internal/parsers/ini.go | 25 +++++++++++++++++++++++++ internal/parsers/markdown.go | 17 +++++++++++++++++ internal/shells/bash.go | 26 ++++++++++++++++++++++++++ 6 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 internal/parsers/ini.go create mode 100644 internal/shells/bash.go diff --git a/cmd/ie/commands/execute.go b/cmd/ie/commands/execute.go index 6762284e..212e5844 100644 --- a/cmd/ie/commands/execute.go +++ b/cmd/ie/commands/execute.go @@ -5,6 +5,7 @@ import ( "os" "github.com/Azure/InnovationEngine/internal/parsers" + "github.com/Azure/InnovationEngine/internal/shells" "github.com/spf13/cobra" ) @@ -30,10 +31,15 @@ var executeCommand = &cobra.Command{ } markdown := parsers.ParseMarkdownIntoAst(source) - commands := parsers.ExtractCodeBlocksFromAst(markdown, source, []string{"bash", "azurecli", "azurecli-interactive", "terraform", "terraform-interactive"}) + commands := parsers.ExtractCodeBlocksFromAst(markdown, source, []string{"bash", "azurecli", "azurecli-init", "azurecli-interactive", "terraform", "terraform-interactive"}) for _, command := range commands { fmt.Println(command) + out, error := shells.ExecuteBashCommand(command, nil, true) + if error != nil { + fmt.Println(error) + } + fmt.Println(out) } }, } diff --git a/go.mod b/go.mod index 572a234d..4647c1b2 100644 --- a/go.mod +++ b/go.mod @@ -8,4 +8,5 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/yuin/goldmark v1.5.4 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect ) diff --git a/go.sum b/go.sum index deb00343..7f31f735 100644 --- a/go.sum +++ b/go.sum @@ -9,4 +9,6 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/parsers/ini.go b/internal/parsers/ini.go new file mode 100644 index 00000000..abe0abf2 --- /dev/null +++ b/internal/parsers/ini.go @@ -0,0 +1,25 @@ +package parsers + +import ( + "log" + + "gopkg.in/ini.v1" +) + +// Parses an INI file into a flat map of keys mapped to values. This reduces +// the complexity of the INI file to a simple key/value store and ignores the +// sections. +func ParseINIFile(filePath string) map[string]string { + + iniFile, err := ini.Load(filePath) + if err != nil { + log.Fatalf("Failed to read the INI file %s because %v", filePath, err) + } + data := make(map[string]string) + for _, section := range iniFile.Sections() { + for key, value := range section.KeysHash() { + data[key] = value + } + } + return data +} diff --git a/internal/parsers/markdown.go b/internal/parsers/markdown.go index a57a4c1e..7790fe5c 100644 --- a/internal/parsers/markdown.go +++ b/internal/parsers/markdown.go @@ -22,6 +22,23 @@ var markdownParser = goldmark.New( ), ) +type MarkdownElementType string + +const ( + ElementHeading MarkdownElementType = "heading" + ElementCodeBlock MarkdownElementType = "code_block" + ElementList MarkdownElementType = "list" + ElementBlockQuote MarkdownElementType = "block_quote" + ElementParagraph MarkdownElementType = "paragraph" +) + +// Represents a markdown element. +type MarkdownElement struct { + Type MarkdownElementType + Content string + Result string +} + // Parses a markdown file into an AST representing the markdown document. func ParseMarkdownIntoAst(source []byte) ast.Node { document := markdownParser.Parser().Parse(text.NewReader(source)) diff --git a/internal/shells/bash.go b/internal/shells/bash.go new file mode 100644 index 00000000..1a90d211 --- /dev/null +++ b/internal/shells/bash.go @@ -0,0 +1,26 @@ +package shells + +import ( + "fmt" + "os" + "os/exec" +) + +// Executes a bash command and returns the output or error. +func ExecuteBashCommand(command string, env map[string]string, inherit_environment_variables bool) (string, error) { + command_to_execute := exec.Command("bash", "-c", command) + + if inherit_environment_variables { + command_to_execute.Env = os.Environ() + } + + for k, v := range env { + command_to_execute.Env = append(command_to_execute.Env, fmt.Sprintf("%s=%s", k, v)) + } + + out, err := command_to_execute.Output() + if err != nil { + return "", fmt.Errorf("error executing bash command: %w", err) + } + return string(out), nil +} From 4fa30cde6a276b6b3b887da22f200226ceba98ca Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 25 Apr 2023 21:13:11 -0700 Subject: [PATCH 017/226] [update] reference. --- internal/shells/bash.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/shells/bash.go b/internal/shells/bash.go index 1a90d211..9109754e 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -8,17 +8,17 @@ import ( // Executes a bash command and returns the output or error. func ExecuteBashCommand(command string, env map[string]string, inherit_environment_variables bool) (string, error) { - command_to_execute := exec.Command("bash", "-c", command) + commandToExecute := exec.Command("bash", "-c", command) if inherit_environment_variables { - command_to_execute.Env = os.Environ() + commandToExecute.Env = os.Environ() } for k, v := range env { - command_to_execute.Env = append(command_to_execute.Env, fmt.Sprintf("%s=%s", k, v)) + commandToExecute.Env = append(commandToExecute.Env, fmt.Sprintf("%s=%s", k, v)) } - out, err := command_to_execute.Output() + out, err := commandToExecute.Output() if err != nil { return "", fmt.Errorf("error executing bash command: %w", err) } From 134997530736f5ee0bf860b1832b9665c05ca4b7 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sat, 29 Apr 2023 21:26:23 -0700 Subject: [PATCH 018/226] [update] markdown parsing to support parsing html comment blocks & add INI initialization to ie execute. --- cmd/ie/commands/execute.go | 32 ++++++++++++++++++++++++++++- internal/parsers/markdown.go | 39 ++++++++++++++++++++++++++++++++++++ internal/utils/file.go | 9 +++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 internal/utils/file.go diff --git a/cmd/ie/commands/execute.go b/cmd/ie/commands/execute.go index 212e5844..6dc5acfe 100644 --- a/cmd/ie/commands/execute.go +++ b/cmd/ie/commands/execute.go @@ -3,9 +3,12 @@ package commands import ( "fmt" "os" + "path/filepath" + "strings" "github.com/Azure/InnovationEngine/internal/parsers" "github.com/Azure/InnovationEngine/internal/shells" + "github.com/Azure/InnovationEngine/internal/utils" "github.com/spf13/cobra" ) @@ -24,15 +27,42 @@ var executeCommand = &cobra.Command{ cmd.Help() return } - source, err := os.ReadFile(markdownFile) + // Load the markdown file. + if utils.FileExists(markdownFile) == false { + fmt.Println("File does not exist.") + return + } + + source, err := os.ReadFile(markdownFile) if err != nil { panic(err) } + // Load environment variables + markdownINI := strings.TrimSuffix(markdownFile, filepath.Ext(markdownFile)) + ".ini" + environmentVariables := make(map[string]string) + + // Check if the INI file exists & load it. + if !utils.FileExists(markdownINI) { + fmt.Println("INI file does not exist: ", markdownINI) + } else { + fmt.Println("INI file exists. Loading: ", markdownINI) + environmentVariables = parsers.ParseINIFile(markdownINI) + + for key, value := range environmentVariables { + fmt.Printf("Setting %s=%s\n", key, value) + } + } + + fmt.Println(environmentVariables) + markdown := parsers.ParseMarkdownIntoAst(source) + scenarioVariables := parsers.ExtractScenarioVariablesFromAst(markdown, source) commands := parsers.ExtractCodeBlocksFromAst(markdown, source, []string{"bash", "azurecli", "azurecli-init", "azurecli-interactive", "terraform", "terraform-interactive"}) + fmt.Println(scenarioVariables) + for _, command := range commands { fmt.Println(command) out, error := shells.ExecuteBashCommand(command, nil, true) diff --git a/internal/parsers/markdown.go b/internal/parsers/markdown.go index 7790fe5c..5f0da1ab 100644 --- a/internal/parsers/markdown.go +++ b/internal/parsers/markdown.go @@ -1,6 +1,8 @@ package parsers import ( + "fmt" + "regexp" "strings" "github.com/yuin/goldmark" @@ -64,6 +66,30 @@ func ExtractCodeBlocksFromAst(node ast.Node, source []byte, languagesToExtract [ return commands } +// This regex matches HTML comments within markdown blocks that contain +// variables to use within The regex is designed to match the following: +var variableCommentBlockRegex = regexp.MustCompile(`(?s)`) + +// Extracts the variables from a provided markdown AST. +func ExtractScenarioVariablesFromAst(node ast.Node, source []byte) []string { + var inlineVariableBlocks []string + ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) { + if entering && node.Kind() == ast.KindHTMLBlock { + htmlNode := node.(*ast.HTMLBlock) + blockContent := extractVariablesFromHTMLBlock(htmlNode, source) + fmt.Printf("Found HTML block with the content: %s\n", blockContent) + match := variableCommentBlockRegex.FindStringSubmatch(blockContent) + if len(match) > 1 { + fmt.Println("Found: ", match[1]) + inlineVariableBlocks = append(inlineVariableBlocks, strings.TrimSpace(match[1])) + } + } + return ast.WalkContinue, nil + }) + + return inlineVariableBlocks +} + // Extracts the command text from an already parsed markdown code block. func extractCommandFromCodeBlock(codeBlock *ast.FencedCodeBlock, source []byte) string { lines := codeBlock.Lines() @@ -76,3 +102,16 @@ func extractCommandFromCodeBlock(codeBlock *ast.FencedCodeBlock, source []byte) return command.String() } + +// TODO: Merge this with the above function. +func extractVariablesFromHTMLBlock(htmlBlock *ast.HTMLBlock, source []byte) string { + lines := htmlBlock.Lines() + var command strings.Builder + + for i := 0; i < lines.Len(); i++ { + line := lines.At(i) + command.WriteString(string(line.Value(source))) + } + + return command.String() +} diff --git a/internal/utils/file.go b/internal/utils/file.go new file mode 100644 index 00000000..2f53d0d1 --- /dev/null +++ b/internal/utils/file.go @@ -0,0 +1,9 @@ +package utils + +import "os" + +// Checks if a given file exists. +func FileExists(path string) bool { + _, err := os.Stat(path) + return !os.IsNotExist(err) +} From bbd0b9bd84cbc9286df04687ebb398ef334957ca Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sat, 29 Apr 2023 22:28:45 -0700 Subject: [PATCH 019/226] [add] function to extract export statements from variable blocks. --- internal/parsers/markdown.go | 39 ++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/internal/parsers/markdown.go b/internal/parsers/markdown.go index 5f0da1ab..afe2f1b6 100644 --- a/internal/parsers/markdown.go +++ b/internal/parsers/markdown.go @@ -67,27 +67,54 @@ func ExtractCodeBlocksFromAst(node ast.Node, source []byte, languagesToExtract [ } // This regex matches HTML comments within markdown blocks that contain -// variables to use within The regex is designed to match the following: -var variableCommentBlockRegex = regexp.MustCompile(`(?s)`) +// variables to use within the scenario. +var variableCommentBlockRegex = regexp.MustCompile("(?s) +```Output +{ + "id": "/subscriptions//resourceGroups/myResourceGroup", + "location": "eastus", + "managedBy": null, + "name": "myResourceGroup", + "properties": { + "provisioningState": "Succeeded" + }, + "tags": null, + "type": "Microsoft.Resources/resourceGroups" +} +``` + +Now create a Virtual Machine Scale Set with [az vmss create](/cli/azure/vmss). The following example creates a scale set named *myScaleSet* that is set to automatically update as changes are applied, and generates SSH keys if they do not exist in *~/.ssh/id_rsa*. These SSH keys are used if you need to log in to the VM instances. To use an existing set of SSH keys, instead use the `--ssh-key-value` parameter and specify the location of your keys. + +```azurecli-interactive +az vmss create \ + --resource-group $RESOURCE_GROUP_NAME \ + --name $SCALE_SET_NAME \ + --image $BASE_VM_IMAGE \ + --upgrade-policy-mode automatic \ + --admin-username $ADMIN_USERNAME \ + --generate-ssh-keys +``` + +It takes a few minutes to create and configure all the scale set resources and VMs. + + +## Deploy sample application +To test your scale set, install a basic web application. The Azure Custom Script Extension is used to download and run a script that installs an application on the VM instances. This extension is useful for post deployment configuration, software installation, or any other configuration / management task. For more information, see the [Custom Script Extension overview](../virtual-machines/extensions/custom-script-linux.md). + +Use the Custom Script Extension to install a basic NGINX web server. Apply the Custom Script Extension that installs NGINX with [az vmss extension set](/cli/azure/vmss/extension) as follows: + +```azurecli-interactive +az vmss extension set \ + --publisher Microsoft.Azure.Extensions \ + --version 2.0 \ + --name $CUSTOM_SCRIPT_NAME \ + --resource-group $RESOURCE_GROUP_NAME \ + --vmss-name $SCALE_SET_NAME \ + --settings '{"fileUris":["https://raw.githubusercontent.com/Azure-Samples/compute-automation-configurations/master/automate_nginx.sh"],"commandToExecute":"./automate_nginx.sh"}' +``` + +```Output +{ + "vmss": { + "doNotRunExtensionsOnOverprovisionedVMs": false, + "orchestrationMode": "Uniform", + "overprovision": true, + "provisioningState": "Succeeded", + "singlePlacementGroup": true, + "timeCreated": "2023-02-01T22:17:20.1117742+00:00", + "uniqueId": "38328143-69e8-4a9b-9d55-8a404cdb6d8b", + "upgradePolicy": { + "mode": "Automatic", + "rollingUpgradePolicy": { + "maxBatchInstancePercent": 20, + "maxSurge": false, + "maxUnhealthyInstancePercent": 20, + "maxUnhealthyUpgradedInstancePercent": 20, + "pauseTimeBetweenBatches": "PT0S", + "rollbackFailedInstancesOnPolicyBreach": false + } + }, + "virtualMachineProfile": { + "networkProfile": { + "networkInterfaceConfigurations": [ + { + "name": "mysca2132Nic", + "properties": { + "disableTcpStateTracking": false, + "dnsSettings": { + "dnsServers": [] + }, + "enableAcceleratedNetworking": false, + "enableIPForwarding": false, + "ipConfigurations": [ + { + "name": "mysca2132IPConfig", + "properties": { + "loadBalancerBackendAddressPools": [ + { + "id": "/subscriptions/f7a60fca-9977-4899-b907-005a076adbb6/resourceGroups/myResourceGroup/providers/Microsoft.Network/loadBalancers/myScaleSetLB/backendAddressPools/myScaleSetLBBEPool", + "resourceGroup": "myResourceGroup" + } + ], + "loadBalancerInboundNatPools": [ + { + "id": "/subscriptions/f7a60fca-9977-4899-b907-005a076adbb6/resourceGroups/myResourceGroup/providers/Microsoft.Network/loadBalancers/myScaleSetLB/inboundNatPools/myScaleSetLBNatPool", + "resourceGroup": "myResourceGroup" + } + ], + "privateIPAddressVersion": "IPv4", + "subnet": { + "id": "/subscriptions/f7a60fca-9977-4899-b907-005a076adbb6/resourceGroups/myResourceGroup/providers/Microsoft.Network/virtualNetworks/myScaleSetVNET/subnets/myScaleSetSubnet", + "resourceGroup": "myResourceGroup" + } + } + } + ], + "primary": true + } + } + ] + }, + "osProfile": { + "adminUsername": "azureuser", + "allowExtensionOperations": true, + "computerNamePrefix": "mysca2132", + "linuxConfiguration": { + "disablePasswordAuthentication": true, + "enableVMAgentPlatformUpdates": false, + "provisionVMAgent": true, + "ssh": { + "publicKeys": [ + { + "keyData": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCvR1+fGFuVMWS2bAY0SgW4E9QzLZ77ETdbCBUVF46eAyL8JWsLynX214hNSK16l4UYZyC3E6jea5qw2rGHPP4eMp7iif50xqd6qGICS428mqc9Gz29J0LFanM7XpHwLnBiJ6hmKvqvHB5tsGKh44MddW0wv+KiiEHIV1ZdSSvBRJ5MMQhqZoUiqlChHourOhaZxvw2dpJhRCvAEKw1s5RoeoLJAdZ6Qr53ERSkJr3BF7uAoNlGx6gatBVkjV+w9CZXN/YN62b1QQiGnk5/BIXNqEIsyxsa84+GbyieRIN/wYjSEV7ASRxSj60qV7RPexvAI+4JGa9UELYMQDrBElgL", + "path": "/home/azureuser/.ssh/authorized_keys" + } + ] + } + }, + "requireGuestProvisionSignal": true, + "secrets": [] + }, + "storageProfile": { + "imageReference": { + "offer": "UbuntuServer", + "publisher": "Canonical", + "sku": "18.04-LTS", + "version": "latest" + }, + "osDisk": { + "caching": "ReadWrite", + "createOption": "FromImage", + "diskSizeGB": 30, + "managedDisk": { + "storageAccountType": "Premium_LRS" + }, + "osType": "Linux" + } + } + } + } +} +``` + +## Allow traffic to application +When the scale set was created, an Azure load balancer was automatically deployed. The load balancer distributes traffic to the VM instances in the scale set. To allow traffic to reach the sample web application, create a load balancer rule with [az network lb rule create](/cli/azure/network/lb/rule). The following example creates a rule named *myLoadBalancerRuleWeb*: + +```azurecli-interactive +az network lb rule create \ + --resource-group $RESOURCE_GROUP_NAME \ + --name $LOAD_BALANCER_RULE_NAME \ + --lb-name $LOAD_BALANCER_NAME \ + --backend-pool-name $BACKEND_POOL_NAME \ + --backend-port 80 \ + --frontend-ip-name $FRONT_END_IP_NAME \ + --frontend-port 80 \ + --protocol tcp +``` + +## Test your scale set +To see your scale set in action, access the sample web application in a web browser. Obtain the public IP address of your load balancer with [az network public-ip show](/cli/azure/network/public-ip). The following example obtains the IP address for *myScaleSetLBPublicIP* created as part of the scale set: + +```azurecli-interactive +az network public-ip show \ + --resource-group $RESOURCE_GROUP_NAME \ + --name $SCALE_SET_PUBLIC_IP \ + --query '[ipAddress]' \ + --output tsv +``` + +Enter the public IP address of the load balancer in to a web browser. The load balancer distributes traffic to one of your VM instances, as shown in the following example: + +![Default web page in NGINX](media/virtual-machine-scale-sets-create-cli/running-nginx-site.png) + +Or run the following command in a local shell to validate the scale set is set up properly + +```bash + curl $(az network public-ip show --resource-group $RESOURCE_GROUP_NAME --name $SCALE_SET_PUBLIC_IP --query '[ipAddress]' --output tsv) +``` + + +```HTML +Hello World from host myscabd00000000 ! +``` + +## Clean up resources +When no longer needed, you can use [az group delete](/cli/azure/group) to remove the resource group, scale set, and all related resources as follows. The `--no-wait` parameter returns control to the prompt without waiting for the operation to complete. The `--yes` parameter confirms that you wish to delete the resources without an additional prompt to do so. + +```azurecli-interactive +az group delete --name $RESOURCE_GROUP_NAME --yes --no-wait +``` + + +## Next steps +In this quickstart, you created a basic scale set and used the Custom Script Extension to install a basic NGINX web server on the VM instances. To learn more, continue to the tutorial for how to create and manage Azure Virtual Machine Scale Sets. + +> [!div class="nextstepaction"] +> [Create and manage Azure Virtual Machine Scale Sets](tutorial-create-and-manage-cli.md) \ No newline at end of file From d732728276bdb25bf23dbdababfcc32fa8d20536 Mon Sep 17 00:00:00 2001 From: Ross Gardler Date: Tue, 14 Feb 2023 01:05:20 +0000 Subject: [PATCH 027/226] Initial review comments addressed ahead of testing the execution impact comments. --- demoScripts/vmssQuickstart.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/demoScripts/vmssQuickstart.md b/demoScripts/vmssQuickstart.md index f147428d..c95b7156 100644 --- a/demoScripts/vmssQuickstart.md +++ b/demoScripts/vmssQuickstart.md @@ -28,18 +28,22 @@ A Virtual Machine Scale Set allows you to deploy and manage a set of auto-scalin ## Define Environment Variables +Throughout this document we use environment variables to facilitate cut and paste reuse. +The default values below will enable you to work through this document in most cases. The meaning of each +environment variable will be addressed as they are used in the steps below. + ```azurecli-interactive -export RESOURCE_GROUP_NAME=myResourceGroup +export RESOURCE_GROUP_NAME=vmssQuickstartRG export RESOURCE_LOCATION=eastus -export SCALE_SET_NAME=myScaleSet +export SCALE_SET_NAME=vmssQuickstart export BASE_VM_IMAGE=UbuntuLTS export ADMIN_USERNAME=azureuser -export LOAD_BALANCER_NAME=myScaleSetLB -export BACKEND_POOL_NAME=myScaleSetLBBEPool -export LOAD_BALANCER_RULE_NAME=myLoadBalancerRuleWeb -export FRONT_END_IP_NAME=loadBalancerFrontEnd -export CUSTOM_SCRIPT_NAME=customScript -export SCALE_SET_PUBLIC_IP=myScaleSetLBPublicIP +export LOAD_BALANCER_NAME=vmssQuickstartLB +export BACKEND_POOL_NAME=vmssQuickstartPool +export LOAD_BALANCER_RULE_NAME=vmssQuickstartRule +export FRONT_END_IP_NAME=vmssQuickstartLoadBalancerFrontEnd +export CUSTOM_SCRIPT_NAME=vmssQuickstartCustomScript +export SCALE_SET_PUBLIC_IP=vmssQuickstartPublicIP ``` ## Create a scale set From 7665e13fe9ad4864c03f7c8ed23dea3d5b2ce76b Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sat, 6 May 2023 14:16:01 -0700 Subject: [PATCH 028/226] [update] structure of the engine to be better suited for multiple scenarios. --- cmd/ie/commands/execute.go | 46 ++--------------- internal/engine/engine.go | 20 ++++++++ internal/engine/execution.go | 58 +++++++++++++++++++++ internal/engine/scenario.go | 97 +++++++++++++++++++++++++++++++++++ internal/render/codeblocks.go | 81 ----------------------------- 5 files changed, 179 insertions(+), 123 deletions(-) create mode 100644 internal/engine/execution.go create mode 100644 internal/engine/scenario.go diff --git a/cmd/ie/commands/execute.go b/cmd/ie/commands/execute.go index 85532561..4129a52f 100644 --- a/cmd/ie/commands/execute.go +++ b/cmd/ie/commands/execute.go @@ -1,14 +1,7 @@ package commands import ( - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/Azure/InnovationEngine/internal/parsers" - "github.com/Azure/InnovationEngine/internal/render" - "github.com/Azure/InnovationEngine/internal/utils" + "github.com/Azure/InnovationEngine/internal/engine" "github.com/spf13/cobra" ) @@ -28,43 +21,12 @@ var executeCommand = &cobra.Command{ return } - // Check if the markdown file exists. - if !utils.FileExists(markdownFile) { - fmt.Printf("Markdown file '%s' does not exist.\n", markdownFile) - return - } - - source, err := os.ReadFile(markdownFile) + innovationEngine := engine.NewEngine() + scenario, err := engine.CreateScenarioFromMarkdown(markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}) if err != nil { panic(err) } - // Load environment variables - markdownINI := strings.TrimSuffix(markdownFile, filepath.Ext(markdownFile)) + ".ini" - environmentVariables := make(map[string]string) - - // Check if the INI file exists & load it. - if !utils.FileExists(markdownINI) { - fmt.Printf("INI file '%s' does not exist, skipping...", markdownINI) - } else { - fmt.Println("INI file exists. Loading: ", markdownINI) - environmentVariables = parsers.ParseINIFile(markdownINI) - - for key, value := range environmentVariables { - fmt.Printf("Setting %s=%s\n", key, value) - } - } - - fmt.Println(environmentVariables) - - markdown := parsers.ParseMarkdownIntoAst(source) - scenarioVariables := parsers.ExtractScenarioVariablesFromAst(markdown, source) - for key, value := range scenarioVariables { - environmentVariables[key] = value - } - - codeBlocks := parsers.ExtractCodeBlocksFromAst(markdown, source, []string{"bash", "azurecli", "azurecli-init", "azurecli-interactive", "terraform", "terraform-interactive"}) - - render.ExecuteAndRenderCodeBlocks(codeBlocks, environmentVariables) + innovationEngine.ExecuteScenario(scenario) }, } diff --git a/internal/engine/engine.go b/internal/engine/engine.go index ca679bf9..4e201a7c 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -1,4 +1,24 @@ package engine type EngineConfiguration struct { + supportedLanguages []string `yaml:"supportedLanguages"` +} + +type Engine struct { + Configuration EngineConfiguration +} + +func LoadConfiguration() EngineConfiguration { + return EngineConfiguration{ + supportedLanguages: []string{"bash", "azurecli", "azurecli-interactive", "terraform"}, + } +} + +func NewEngine() *Engine { + return &Engine{} +} + +func (e *Engine) ExecuteScenario(scenario *Scenario) error { + ExecuteAndRenderSteps(scenario.Steps, scenario.Environment) + return nil } diff --git a/internal/engine/execution.go b/internal/engine/execution.go new file mode 100644 index 00000000..776c95fc --- /dev/null +++ b/internal/engine/execution.go @@ -0,0 +1,58 @@ +package engine + +import ( + "fmt" + "time" + + "github.com/Azure/InnovationEngine/internal/parsers" + "github.com/Azure/InnovationEngine/internal/shells" + "github.com/charmbracelet/lipgloss" +) + +const ( + // TODO - Make this configurable for terminals that support it. + // spinnerFrames = `⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏` + spinnerFrames = `-\|/` + spinnerLength = 1 + spinnerRefresh = 100 * time.Millisecond +) + +var ( + spinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")) + checkStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#32CD32")) +) + +func ExecuteAndRenderSteps(steps []Step, env map[string]string) { + for _, step := range steps { + for _, block := range step.CodeBlocks { + fmt.Print(spinnerStyle.Render(string(spinnerFrames[0])) + " ") + + done := make(chan bool) + + // Create a goroutine to execute the command and pass in the codeblock to + go func(block parsers.CodeBlock) { + _, err := shells.ExecuteBashCommand(block.Content, env, true) + if err != nil { + fmt.Println("Error executing command: ", err) + } + done <- true + }(block) + + frame := 0 + + // While the command is executing, render the spinner. + loop: + for { + select { + case <-done: + fmt.Printf("\r%s %s\n", checkStyle.Render("✔"), block.Header) + break loop + default: + frame = (frame + 1) % len(spinnerFrames) + fmt.Printf("\r%s %s", spinnerStyle.Render(string(spinnerFrames[frame])), block.Header) + time.Sleep(spinnerRefresh) + } + } + } + } +} diff --git a/internal/engine/scenario.go b/internal/engine/scenario.go new file mode 100644 index 00000000..e576a1a4 --- /dev/null +++ b/internal/engine/scenario.go @@ -0,0 +1,97 @@ +package engine + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/Azure/InnovationEngine/internal/parsers" + "github.com/Azure/InnovationEngine/internal/utils" + "github.com/yuin/goldmark/ast" +) + +// Individual steps within a scenario. +type Step struct { + Name string + CodeBlocks []parsers.CodeBlock +} + +// Scenarios are the top-level object that represents a scenario to be executed. +type Scenario struct { + Name string + Description string + MarkdownEntry ast.Node + Steps []Step + Environment map[string]string +} + +func groupCodeBlocksIntoSteps(blocks []parsers.CodeBlock) []Step { + var groupedSteps []Step + var headerIndex = make(map[string]int) + + for _, block := range blocks { + if index, ok := headerIndex[block.Header]; ok { + groupedSteps[index].CodeBlocks = append(groupedSteps[index].CodeBlocks, block) + } else { + headerIndex[block.Header] = len(groupedSteps) + groupedSteps = append(groupedSteps, Step{ + Name: block.Header, + CodeBlocks: []parsers.CodeBlock{block}, + }) + } + } + + return groupedSteps +} + +func CreateScenarioFromMarkdown(path string, languagesToExecute []string) (*Scenario, error) { + if path == "" { + return nil, nil + } + + if !utils.FileExists(path) { + return nil, fmt.Errorf("markdown file '%s' does not exist", path) + } + + source, err := os.ReadFile(path) + if err != nil { + panic(err) + } + + // Load environment variables + markdownINI := strings.TrimSuffix(path, filepath.Ext(path)) + ".ini" + environmentVariables := make(map[string]string) + + // Check if the INI file exists & load it. + if !utils.FileExists(markdownINI) { + fmt.Printf("INI file '%s' does not exist, skipping...", markdownINI) + } else { + fmt.Println("INI file exists. Loading: ", markdownINI) + environmentVariables = parsers.ParseINIFile(markdownINI) + + for key, value := range environmentVariables { + fmt.Printf("Setting %s=%s\n", key, value) + } + } + + markdown := parsers.ParseMarkdownIntoAst(source) + scenarioVariables := parsers.ExtractScenarioVariablesFromAst(markdown, source) + for key, value := range scenarioVariables { + environmentVariables[key] = value + } + + codeBlocks := parsers.ExtractCodeBlocksFromAst(markdown, source, languagesToExecute) + + steps := groupCodeBlocksIntoSteps(codeBlocks) + + return &Scenario{ + Name: "TODO", + Environment: environmentVariables, + Steps: steps, + }, nil +} + +func groupCodeBlocksByHeader(codeBlocks []parsers.CodeBlock) { + panic("unimplemented") +} diff --git a/internal/render/codeblocks.go b/internal/render/codeblocks.go index 7bbf644f..e69de29b 100644 --- a/internal/render/codeblocks.go +++ b/internal/render/codeblocks.go @@ -1,81 +0,0 @@ -package render - -import ( - "fmt" - "time" - - "github.com/Azure/InnovationEngine/internal/parsers" - "github.com/Azure/InnovationEngine/internal/shells" - "github.com/charmbracelet/lipgloss" -) - -const ( - // TODO - Make this configurable for terminals that support it. - // spinnerFrames = `⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏` - spinnerFrames = `-\|/` - spinnerLength = 1 - spinnerRefresh = 100 * time.Millisecond -) - -var ( - spinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")) - checkStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#32CD32")) -) - -type GroupedCodeBlock struct { - Header string - Blocks []parsers.CodeBlock -} - -func groupCodeBlocksByHeader(blocks []parsers.CodeBlock) []GroupedCodeBlock { - var groupedBlocks []GroupedCodeBlock - var headerIndex = make(map[string]int) - - for _, block := range blocks { - if index, ok := headerIndex[block.Header]; ok { - groupedBlocks[index].Blocks = append(groupedBlocks[index].Blocks, block) - } else { - headerIndex[block.Header] = len(groupedBlocks) - groupedBlocks = append(groupedBlocks, GroupedCodeBlock{ - Header: block.Header, - Blocks: []parsers.CodeBlock{block}, - }) - } - } - - return groupedBlocks -} - -func ExecuteAndRenderCodeBlocks(codeblocks []parsers.CodeBlock, env map[string]string) { - groupedBlocks := groupCodeBlocksByHeader(codeblocks) - - for _, groupedBlock := range groupedBlocks { - for _, block := range groupedBlock.Blocks { - fmt.Print(spinnerStyle.Render(string(spinnerFrames[0])) + " ") - - done := make(chan bool) - - go func(block parsers.CodeBlock) { - _, err := shells.ExecuteBashCommand(block.Content, env, true) - if err != nil { - fmt.Println("Error executing command: ", err) - } - done <- true - }(block) - - frame := 0 - loop: - for { - select { - case <-done: - fmt.Printf("\r%s %s\n", checkStyle.Render("✔"), block.Header) - break loop - default: - frame = (frame + 1) % len(spinnerFrames) - fmt.Printf("\r%s %s", spinnerStyle.Render(string(spinnerFrames[frame])), block.Header) - time.Sleep(spinnerRefresh) - } - } - } - } -} From 4d201330c3d7c1000153be7507f8db7bc40e406b Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sat, 6 May 2023 14:27:23 -0700 Subject: [PATCH 029/226] [add] function to extract the scenario title. --- internal/engine/scenario.go | 23 +++++++-------- internal/parsers/markdown.go | 53 ++++++++++++++++++++--------------- internal/render/codeblocks.go | 0 3 files changed, 43 insertions(+), 33 deletions(-) delete mode 100644 internal/render/codeblocks.go diff --git a/internal/engine/scenario.go b/internal/engine/scenario.go index e576a1a4..c11ca7b8 100644 --- a/internal/engine/scenario.go +++ b/internal/engine/scenario.go @@ -19,11 +19,10 @@ type Step struct { // Scenarios are the top-level object that represents a scenario to be executed. type Scenario struct { - Name string - Description string - MarkdownEntry ast.Node - Steps []Step - Environment map[string]string + Name string + MarkdownAst ast.Node + Steps []Step + Environment map[string]string } func groupCodeBlocksIntoSteps(blocks []parsers.CodeBlock) []Step { @@ -82,16 +81,18 @@ func CreateScenarioFromMarkdown(path string, languagesToExecute []string) (*Scen } codeBlocks := parsers.ExtractCodeBlocksFromAst(markdown, source, languagesToExecute) - steps := groupCodeBlocksIntoSteps(codeBlocks) + title, err := parsers.ExtractScenarioTitleFromAst(markdown, source) + if err != nil { + return nil, err + } + + fmt.Printf("Found scenario: %s\n", title) return &Scenario{ - Name: "TODO", + Name: title, Environment: environmentVariables, Steps: steps, + MarkdownAst: markdown, }, nil } - -func groupCodeBlocksByHeader(codeBlocks []parsers.CodeBlock) { - panic("unimplemented") -} diff --git a/internal/parsers/markdown.go b/internal/parsers/markdown.go index dc222512..a1162f79 100644 --- a/internal/parsers/markdown.go +++ b/internal/parsers/markdown.go @@ -24,23 +24,6 @@ var markdownParser = goldmark.New( ), ) -type MarkdownElementType string - -const ( - ElementHeading MarkdownElementType = "heading" - ElementCodeBlock MarkdownElementType = "code_block" - ElementList MarkdownElementType = "list" - ElementBlockQuote MarkdownElementType = "block_quote" - ElementParagraph MarkdownElementType = "paragraph" -) - -// Represents a markdown element. -type MarkdownElement struct { - Type MarkdownElementType - Content string - Result string -} - // Parses a markdown file into an AST representing the markdown document. func ParseMarkdownIntoAst(source []byte) ast.Node { document := markdownParser.Parser().Parse(text.NewReader(source)) @@ -53,6 +36,30 @@ type CodeBlock struct { Header string } +// Assumes the title of the scenario is the first h1 header in the +// markdown file. +func ExtractScenarioTitleFromAst(node ast.Node, source []byte) (string, error) { + header := "" + ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) { + if entering { + switch n := node.(type) { + case *ast.Heading: + if n.Level == 1 { + header = string(extractTextFromMarkdown(&n.BaseBlock, source)) + return ast.WalkStop, nil + } + } + } + return ast.WalkContinue, nil + }) + + if header == "" { + return "", fmt.Errorf("no header found") + } + + return header, nil +} + // Extracts the code blocks from a provided markdown AST that match the // languagesToExtract. func ExtractCodeBlocksFromAst(node ast.Node, source []byte, languagesToExtract []string) []CodeBlock { @@ -63,7 +70,7 @@ func ExtractCodeBlocksFromAst(node ast.Node, source []byte, languagesToExtract [ switch n := node.(type) { // Set the last header when we encounter a heading. case *ast.Heading: - lastHeader = string(extractTextFromCodeBlock(&n.BaseBlock, source)) + lastHeader = string(extractTextFromMarkdown(&n.BaseBlock, source)) // Extract the code block if it matches the language. case *ast.FencedCodeBlock: language := string(n.Language((source))) @@ -71,7 +78,7 @@ func ExtractCodeBlocksFromAst(node ast.Node, source []byte, languagesToExtract [ if language == desiredLanguage { command := CodeBlock{ Language: language, - Content: extractTextFromCodeBlock(&n.BaseBlock, source), + Content: extractTextFromMarkdown(&n.BaseBlock, source), Header: lastHeader, } commands = append(commands, command) @@ -96,7 +103,7 @@ func ExtractScenarioVariablesFromAst(node ast.Node, source []byte) map[string]st ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) { if entering && node.Kind() == ast.KindHTMLBlock { htmlNode := node.(*ast.HTMLBlock) - blockContent := extractTextFromCodeBlock(&htmlNode.BaseBlock, source) + blockContent := extractTextFromMarkdown(&htmlNode.BaseBlock, source) fmt.Printf("Found HTML block with the content: %s\n", blockContent) match := variableCommentBlockRegex.FindStringSubmatch(blockContent) @@ -114,6 +121,8 @@ func ExtractScenarioVariablesFromAst(node ast.Node, source []byte) map[string]st return scenarioVariables } +// Converts a string of shell variable exports into a map of key/value pairs. +// I.E. `export FOO=bar\nexport BAZ=qux` becomes `{"FOO": "bar", "BAZ": "qux"}` func convertScenarioVariablesToMap(variableBlock string) map[string]string { variableMap := make(map[string]string) @@ -135,8 +144,8 @@ func convertScenarioVariablesToMap(variableBlock string) map[string]string { } // Extract the text from a code blocks base block and return it as a string. -func extractTextFromCodeBlock(codeBlockBase *ast.BaseBlock, source []byte) string { - lines := codeBlockBase.Lines() +func extractTextFromMarkdown(baseBlock *ast.BaseBlock, source []byte) string { + lines := baseBlock.Lines() var command strings.Builder for i := 0; i < lines.Len(); i++ { diff --git a/internal/render/codeblocks.go b/internal/render/codeblocks.go deleted file mode 100644 index e69de29b..00000000 From cf0bd202655dfd388845eb54f43b1a35ac14425c Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sat, 6 May 2023 15:00:32 -0700 Subject: [PATCH 030/226] [render] title. --- internal/engine/engine.go | 3 +++ internal/engine/execution.go | 3 ++- internal/engine/scenario.go | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 4e201a7c..47796395 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -1,5 +1,7 @@ package engine +import "fmt" + type EngineConfiguration struct { supportedLanguages []string `yaml:"supportedLanguages"` } @@ -19,6 +21,7 @@ func NewEngine() *Engine { } func (e *Engine) ExecuteScenario(scenario *Scenario) error { + fmt.Println(titleStyle.Render(scenario.Name)) ExecuteAndRenderSteps(scenario.Steps, scenario.Environment) return nil } diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 776c95fc..67febc1c 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -20,6 +20,7 @@ const ( var ( spinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")) checkStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#32CD32")) + titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Align(lipgloss.Center).Bold(true) ) func ExecuteAndRenderSteps(steps []Step, env map[string]string) { @@ -40,8 +41,8 @@ func ExecuteAndRenderSteps(steps []Step, env map[string]string) { frame := 0 - // While the command is executing, render the spinner. loop: + // While the command is executing, render the spinner. for { select { case <-done: diff --git a/internal/engine/scenario.go b/internal/engine/scenario.go index c11ca7b8..1b043e05 100644 --- a/internal/engine/scenario.go +++ b/internal/engine/scenario.go @@ -44,6 +44,9 @@ func groupCodeBlocksIntoSteps(blocks []parsers.CodeBlock) []Step { return groupedSteps } +// Creates a scenario object from a given markdown file. languagesToExecute is +// used to filter out code blocks that should not be parsed out of the markdown +// file. func CreateScenarioFromMarkdown(path string, languagesToExecute []string) (*Scenario, error) { if path == "" { return nil, nil From f64087264587fb8c0db2488696c8f9230532f63d Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sat, 6 May 2023 20:48:12 -0700 Subject: [PATCH 031/226] [update] formatting for commands. --- internal/engine/execution.go | 52 +++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 67febc1c..9cd331b2 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -2,6 +2,7 @@ package engine import ( "fmt" + "strings" "time" "github.com/Azure/InnovationEngine/internal/parsers" @@ -20,37 +21,68 @@ const ( var ( spinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")) checkStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#32CD32")) + errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000")) titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Align(lipgloss.Center).Bold(true) ) +func indentSubsequentLines(content string, index string) string { + lines := strings.Split(content, "\n") + for i := 1; i < len(lines); i++ { + if strings.HasSuffix(strings.TrimSpace(lines[i-1]), "\\") { + lines[i] = index + lines[i] + } + } + return strings.Join(lines, "\n") +} + func ExecuteAndRenderSteps(steps []Step, env map[string]string) { - for _, step := range steps { + for stepNumber, step := range steps { + fmt.Printf("%d. %s\n", stepNumber+1, step.Name) for _, block := range step.CodeBlocks { - fmt.Print(spinnerStyle.Render(string(spinnerFrames[0])) + " ") + // Render the codeblock. + indentedBlock := indentSubsequentLines(block.Content, " ") + fmt.Print(" " + indentedBlock) - done := make(chan bool) + // Grab the number of lines it contains & set the cursor to the + // beginning of the block. + lines := strings.Count(block.Content, "\n") + fmt.Printf("\033[%dA", lines) + + // Render the spinner and hide the curosr + fmt.Print(spinnerStyle.Render(" "+string(spinnerFrames[0])) + " ") + + fmt.Print("\033[?25l") + done := make(chan error) // Create a goroutine to execute the command and pass in the codeblock to go func(block parsers.CodeBlock) { _, err := shells.ExecuteBashCommand(block.Content, env, true) - if err != nil { - fmt.Println("Error executing command: ", err) - } - done <- true + done <- err }(block) frame := 0 + var err error loop: // While the command is executing, render the spinner. for { select { - case <-done: - fmt.Printf("\r%s %s\n", checkStyle.Render("✔"), block.Header) + case err = <-done: + fmt.Print("\033[?25h") + // Show the cursor & clear the spinner. + if err == nil { + fmt.Printf("\r %s \n", checkStyle.Render("✔")) + fmt.Printf("\033[%dB", lines) + } else { + fmt.Printf("\r %s \n", errorStyle.Render("✗")) + fmt.Printf("\033[%dB", lines) + fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render(err.Error())) + } + break loop default: frame = (frame + 1) % len(spinnerFrames) - fmt.Printf("\r%s %s", spinnerStyle.Render(string(spinnerFrames[frame])), block.Header) + fmt.Printf("\r %s", spinnerStyle.Render(string(spinnerFrames[frame]))) time.Sleep(spinnerRefresh) } } From 27b4c0c64db41201066baaed227ad34adc94d976 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sun, 7 May 2023 14:45:43 -0700 Subject: [PATCH 032/226] [clean] up the mod file & add documentation.: --- go.mod | 13 ++++++++----- go.sum | 18 ++++++++++++++++-- internal/engine/engine.go | 2 ++ internal/engine/execution.go | 19 ++++++++++++------- 4 files changed, 38 insertions(+), 14 deletions(-) diff --git a/go.mod b/go.mod index 5f3e0108..00beb838 100644 --- a/go.mod +++ b/go.mod @@ -2,11 +2,15 @@ module github.com/Azure/InnovationEngine go 1.20 -require github.com/spf13/cobra v1.7.0 +require ( + github.com/charmbracelet/lipgloss v0.7.1 + github.com/spf13/cobra v1.7.0 + github.com/yuin/goldmark v1.5.4 + gopkg.in/ini.v1 v1.67.0 +) require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/lipgloss v0.7.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-isatty v0.0.18 // indirect @@ -15,7 +19,6 @@ require ( github.com/muesli/termenv v0.15.1 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/yuin/goldmark v1.5.4 // indirect - golang.org/x/sys v0.7.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect + github.com/stretchr/testify v1.8.2 // indirect + golang.org/x/sys v0.8.0 // indirect ) diff --git a/go.sum b/go.sum index 3e1d7fad..3f8a1c9a 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,9 @@ github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -16,6 +19,8 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= @@ -25,12 +30,21 @@ github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 47796395..2e8eccbc 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -16,10 +16,12 @@ func LoadConfiguration() EngineConfiguration { } } +// / Create a new engine instance. func NewEngine() *Engine { return &Engine{} } +// / Executes a scenario. func (e *Engine) ExecuteScenario(scenario *Scenario) error { fmt.Println(titleStyle.Render(scenario.Name)) ExecuteAndRenderSteps(scenario.Steps, scenario.Environment) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 9cd331b2..f93dd76f 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -18,6 +18,7 @@ const ( spinnerRefresh = 100 * time.Millisecond ) +// Styles used for rendering output to the terminal. var ( spinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")) checkStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#32CD32")) @@ -25,7 +26,9 @@ var ( titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Align(lipgloss.Center).Bold(true) ) -func indentSubsequentLines(content string, index string) string { +// Indents a multi-line command to be nested under the first line of the +// command. +func indentMultiLineCommand(content string, index string) string { lines := strings.Split(content, "\n") for i := 1; i < len(lines); i++ { if strings.HasSuffix(strings.TrimSpace(lines[i-1]), "\\") { @@ -35,12 +38,13 @@ func indentSubsequentLines(content string, index string) string { return strings.Join(lines, "\n") } +// Executes the steps from a scenario and renders the output to the terminal. func ExecuteAndRenderSteps(steps []Step, env map[string]string) { for stepNumber, step := range steps { fmt.Printf("%d. %s\n", stepNumber+1, step.Name) for _, block := range step.CodeBlocks { // Render the codeblock. - indentedBlock := indentSubsequentLines(block.Content, " ") + indentedBlock := indentMultiLineCommand(block.Content, " ") fmt.Print(" " + indentedBlock) // Grab the number of lines it contains & set the cursor to the @@ -48,13 +52,13 @@ func ExecuteAndRenderSteps(steps []Step, env map[string]string) { lines := strings.Count(block.Content, "\n") fmt.Printf("\033[%dA", lines) - // Render the spinner and hide the curosr + // Render the spinner and hide the cursor. fmt.Print(spinnerStyle.Render(" "+string(spinnerFrames[0])) + " ") - fmt.Print("\033[?25l") - done := make(chan error) - // Create a goroutine to execute the command and pass in the codeblock to + // execute the command as a goroutine to allow for the spinner to be + // rendered while the command is executing. + done := make(chan error) go func(block parsers.CodeBlock) { _, err := shells.ExecuteBashCommand(block.Content, env, true) done <- err @@ -68,8 +72,9 @@ func ExecuteAndRenderSteps(steps []Step, env map[string]string) { for { select { case err = <-done: + // Show the cursor, check the result of the command, and display the + // final status. fmt.Print("\033[?25h") - // Show the cursor & clear the spinner. if err == nil { fmt.Printf("\r %s \n", checkStyle.Render("✔")) fmt.Printf("\033[%dB", lines) From f841175afc395bd05c3cc53881f922c84c46cf8d Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sun, 7 May 2023 16:31:44 -0700 Subject: [PATCH 033/226] [add] the output of the generated script. --- internal/engine/engine.go | 1 + internal/engine/scenario.go | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 2e8eccbc..6c4c1f6f 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -25,5 +25,6 @@ func NewEngine() *Engine { func (e *Engine) ExecuteScenario(scenario *Scenario) error { fmt.Println(titleStyle.Render(scenario.Name)) ExecuteAndRenderSteps(scenario.Steps, scenario.Environment) + fmt.Printf("---Generated script---\n %s", scenario.ToShellScript()) return nil } diff --git a/internal/engine/scenario.go b/internal/engine/scenario.go index 1b043e05..6e673e50 100644 --- a/internal/engine/scenario.go +++ b/internal/engine/scenario.go @@ -99,3 +99,21 @@ func CreateScenarioFromMarkdown(path string, languagesToExecute []string) (*Scen MarkdownAst: markdown, }, nil } + +// Convert a scenario into a shell script +func (s *Scenario) ToShellScript() string { + var script strings.Builder + + for key, value := range s.Environment { + script.WriteString(fmt.Sprintf("export %s=\"%s\"\n", key, value)) + } + + for _, step := range s.Steps { + script.WriteString(fmt.Sprintf("# %s\n", step.Name)) + for _, block := range step.CodeBlocks { + script.WriteString(fmt.Sprintf("%s\n", block.Content)) + } + } + + return script.String() +} From 2118c3013e1bc65b5bcec2312a4ca3d0fdc977da Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 8 May 2023 15:51:37 -0700 Subject: [PATCH 034/226] [update] documentation. --- README.md | 106 +++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 77 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index ef1a9e27..b2ed150c 100644 --- a/README.md +++ b/README.md @@ -3,12 +3,19 @@ Innovation Engine is a tool for rapid innovation and simplification. # Executable Documentation -Executable documentation takes standard markdown language and amplifies it by allowing it to be executed step by step in an educational manner, and tested via automated CI/CD pipelines. +Executable documentation takes standard markdown language and amplifies it by +allowing it to be executed step by step in an educational manner, and tested +via automated CI/CD pipelines. # Try Out Executable Documentation -Azure Cloud Shell provides an environment with all of the prerequisites installed to run Executable Documentation. This is the recommended method for new users to try and develop tutorials for Innovation Engine. +Azure Cloud Shell provides an environment with all of the prerequisites +installed to run Executable Documentation. This is the recommended method for +new users to try and develop tutorials for Innovation Engine. -Open [Azure Cloud Shell](https://ms.portal.azure.com/#cloudshell/) and select Bash as the environment. Paste the following commands into the shell, this will clone the Innovation Engine repo, install the requirements, and build out the innovation engine executable. +Open [Azure Cloud Shell](https://ms.portal.azure.com/#cloudshell/) and select +Bash as the environment. Paste the following commands into the shell, this will +clone the Innovation Engine repo, install the requirements, and build out the +innovation engine executable. ```bash git clone https://github.com/Azure/InnovationEngine; @@ -16,53 +23,76 @@ cd InnovationEngine; make build-ie; ``` -Now you can run the interactive Innovation Engine tutorial with the following command: +Now you can run the interactive Innovation Engine tutorial with the following +command: ```bash ./bin/ie interactive tutorial.md ``` The general format to run an executable document is: -`python3 main.py ` +`ie ` ### Modes of Operation Today, executable documentation can be run in 3 modes of operation: -Interactive: Displays the descriptive text of the tutorial and pauses at code blocks and headings to allow user interaction `python3 main.py interactive tutorial.md` +Interactive: Displays the descriptive text of the tutorial and pauses at code +blocks and headings to allow user interaction +`ie interactive tutorial.md` -Test: Runs the commands and then verifies that the output is sufficiently similar to the expected results (recorded in the markdown file) to be considered correct. `python3 main.py test tutorial.md` +Test: Runs the commands and then verifies that the output is sufficiently +similar to the expected results (recorded in the markdown file) to be +considered correct. `ie test tutorial.md` + +Execute: Reads the document and executes all of the code blocks not pausing for +input or testing output. Essentially executes a markdown file as a script. +`ie execute tutorial.md` -Execute: Reads the document and executes all of the code blocks not pausing for input or testing output. Essentially executes a markdown file as a script. `python3 main.py execute tutorial.md` ## Use Executable documentation for Automated Testing -One of the core benefits of executable documentation is the ability to run automated testing on markdown file. This can be used to ensure freshness of content. +One of the core benefits of executable documentation is the ability to run +automated testing on markdown file. This can be used to ensure freshness of +content. -In order to do this one will need to combine innovation engine executable documentation syntax with GitHub actions. +In order to do this one will need to combine innovation engine executable +documentation syntax with GitHub actions. -In order to test if a command or action ran correctly executable documentation needs something to compare the results against. This requirement is met with result blocks. +In order to test if a command or action ran correctly executable documentation +needs something to compare the results against. This requirement is met with +result blocks. ### Result Blocks -Result blocks are distinguished in Executable documentation by a custom expected_similarity comment tag followed by a code block. For example +Result blocks are distinguished in Executable documentation by a custom +expected_similarity comment tag followed by a code block. For example ```text Hello world ``` -This example purposely breaks the comment syntax so that it shows up in markdown. Otherwise, the tag of expected_similarity is completely invisible. +This example purposely breaks the comment syntax so that it shows up in +markdown. Otherwise, the tag of expected_similarity is completely invisible. -The expected similarity value is a floating point number between 0 and 1 which specifies how closely the output needs to match the results block. 0 being no similarity, 1 being an exact match. +The expected similarity value is a floating point number between 0 and 1 which +specifies how closely the output needs to match the results block. 0 being no +similarity, 1 being an exact match. >**Note** It may take a little bit of trial and error to find the exact value for expected_similarity. ### Environment Variables -Another barrier to automated testing is setting default values for test cases to use in running. This problem can be solved with command line variables in Executable documentation Syntax. +Another barrier to automated testing is setting default values for test cases +to use in running. This problem can be solved with command line variables in +Executable documentation Syntax. -Default environment variables can be set for executable documentation in a few different ways. +Default environment variables can be set for executable documentation in a few +different ways. 1. A matching .ini file to the markdown - - Upon running any document executable documentation will look for a corresponding .ini file. For example if my markdown file is named tutorial.md the corresponding ini file would be tutorial.ini. - - This file is a simple key value match for environment variable and value. For example: + - Upon running any document executable documentation will look for a + corresponding .ini file. For example if my markdown file is named tutorial.md + the corresponding ini file would be tutorial.ini. + - This file is a simple key value match for environment variable and value. + For example: ```ini MY_RESOURCE_GROUP_NAME = myResourceGroup MY_LOCATION = eastus @@ -70,7 +100,9 @@ Default environment variables can be set for executable documentation in a few d MY_VM_IMAGE = debian MY_ADMIN_USERNAME = azureuser ``` -2. A comment at the beginning of the document containing a code blog with the tag 'variables'. This will be invisible to users unless they look at the raw markdown. For example: +2. A comment at the beginning of the document containing a code blog with the +tag 'variables'. This will be invisible to users unless they look at the raw +markdown. For example: >**Note** The below example intentionally has broken comment syntax w/ two !'s. -Variables set in comments will override variables set in a .ini file. Consequently, locally declared variables in code samples will override variables set in comments. +Variables set in comments will override variables set in a .ini file. +Consequently, locally declared variables in code samples will override +variables set in comments. ### Setting Up GitHub Actions to use Innovation Engine -After documentation is set up to take advantage of automated testing a github action will need to be created to run testing on a recurring basis. The action will simply create a basic Linux container, install Innovation Engine Executable Documentation and run Executable documentation in the Test mode on whatever markdown files are specified. +After documentation is set up to take advantage of automated testing a github +action will need to be created to run testing on a recurring basis. The action +will simply create a basic Linux container, install Innovation Engine +Executable Documentation and run Executable documentation in the Test mode on +whatever markdown files are specified. -It is important to note that if you require any specific access or cli tools not included in standard bash that will need to be installed in the container. The following example is how this may be done for a document which runs Azure commands. +It is important to note that if you require any specific access or cli tools +not included in standard bash that will need to be installed in the container. +The following example is how this may be done for a document which runs Azure +commands. ```yml name: 00-testing @@ -125,9 +166,15 @@ jobs: ## Use Executable Documentation for Interactive Documentation -Innovation Engine can also be used for interactive tutorials via a local or remote shell environment. After cloning the project and running `pip3 install -r requirements.txt`, Innovation Engine can be used for interactive tutorials by simply using the interactive flag when executing the program. For example, `python3 main.py interactive tutorial.md` +Innovation Engine can also be used for interactive tutorials via a local or +remote shell environment. After cloning the project and running +`make build-ie`, Innovation Engine can be used for +interactive tutorials by simply using the interactive flag when executing the +program. For example, `./bin/ie interactive tutorial.md` -As it is written the code will pause and wait for input on any header or code block. Any document written in standard markdown can be run as an interactive document. +As it is written the code will pause and wait for input on any header or code +block. Any document written in standard markdown can be run as an interactive +document. ## Contributing @@ -162,8 +209,9 @@ any additional questions or comments. ## Trademarks -This project may contain trademarks or logos for projects, products, or services. Authorized use of Microsoft -trademarks or logos is subject to and must follow -[Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). -Use of Microsoft trademarks or logos in modified versions of this project must not cause confusion or imply Microsoft sponsorship. -Any use of third-party trademarks or logos are subject to those third-party's policies. +This project may contain trademarks or logos for projects, products, or +services. Authorized use of Microsoft trademarks or logos is subject to and +must follow [Microsoft's Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks/usage/general). +Use of Microsoft trademarks or logos in modified versions of this project must +not cause confusion or imply Microsoft sponsorship. Any use of third-party +trademarks or logos are subject to those third-party's policies. From 548b6cb76a8a321c28390988d4794ad8794f3b18 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 8 May 2023 15:51:54 -0700 Subject: [PATCH 035/226] [fix] formatting. --- internal/engine/engine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 6c4c1f6f..27cb4c5e 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -25,6 +25,6 @@ func NewEngine() *Engine { func (e *Engine) ExecuteScenario(scenario *Scenario) error { fmt.Println(titleStyle.Render(scenario.Name)) ExecuteAndRenderSteps(scenario.Steps, scenario.Environment) - fmt.Printf("---Generated script---\n %s", scenario.ToShellScript()) + fmt.Printf("---Generated script---\n%s", scenario.ToShellScript()) return nil } From fa4c730468cf8f5daa3f6ee88cd09e2bdd0d7013 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sun, 14 May 2023 12:55:02 -0700 Subject: [PATCH 036/226] [add] basic docker file to build API & runner, add a hello world http handler in the API. --- Makefile | 6 +++++- cmd/api/main.go | 8 +++++++- cmd/runner/main.go | 0 infra/api/Dockerfile | 14 ++++++++++++++ infra/runner/Dockerfile | 0 5 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 cmd/runner/main.go create mode 100644 infra/api/Dockerfile create mode 100644 infra/runner/Dockerfile diff --git a/Makefile b/Makefile index aaf30a4d..0387d776 100644 --- a/Makefile +++ b/Makefile @@ -24,4 +24,8 @@ run-api: build-api clean: @echo "Cleaning up" - @rm -rf "$(BINARY_DIR)" \ No newline at end of file + @rm -rf "$(BINARY_DIR)" + +build-api-container: + @echo "Building the Innovation Engine API container" + @docker build -t innovation-engine-api -f infra/api/Dockerfile . diff --git a/cmd/api/main.go b/cmd/api/main.go index f7b60bde..e5a802c0 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -1,7 +1,13 @@ package main -import "fmt" +import ( + "fmt" + "net/http" +) func main() { fmt.Println("Hello, world!") + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, world!") + }) } diff --git a/cmd/runner/main.go b/cmd/runner/main.go new file mode 100644 index 00000000..e69de29b diff --git a/infra/api/Dockerfile b/infra/api/Dockerfile new file mode 100644 index 00000000..eb3d0695 --- /dev/null +++ b/infra/api/Dockerfile @@ -0,0 +1,14 @@ +FROM mcr.microsoft.com/cbl-mariner/base/core:2.0 + +ARG HOST=0.0.0.0 +ARG PORT=8080 + +WORKDIR /api + +RUN tdnf install golang make -y + +COPY . . + +RUN make build-api + +CMD ["./bin/api"] \ No newline at end of file diff --git a/infra/runner/Dockerfile b/infra/runner/Dockerfile new file mode 100644 index 00000000..e69de29b From 66118c48e2d7a6797d71c1dcf3435fd8313ef3f9 Mon Sep 17 00:00:00 2001 From: Ross Gardler Date: Sun, 14 May 2023 21:07:40 -0700 Subject: [PATCH 037/226] ignroe bin --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0e516d73..32c26e1c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ __pycache__ #VS Code -.vscode \ No newline at end of file +.vscode + +# Generated Files +bin From e8ad9abec74a2907e78c70d2a68eecac826cbe4b Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 15 May 2023 12:03:49 -0700 Subject: [PATCH 038/226] [add] kubernetes client & code to grab the clientset. --- Makefile | 17 +- cmd/runner/main.go | 13 ++ go.mod | 38 ++++ go.sum | 456 ++++++++++++++++++++++++++++++++++++++++ infra/runner/Dockerfile | 14 ++ internal/kube/client.go | 35 +++ 6 files changed, 572 insertions(+), 1 deletion(-) create mode 100644 internal/kube/client.go diff --git a/Makefile b/Makefile index 0387d776..23d073f9 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,8 @@ BINARY_DIR := bin IE_BINARY := $(BINARY_DIR)/ie API_BINARY := $(BINARY_DIR)/api +# -------------------------- Native build targets ------------------------------ + build-ie: @echo "Building the Innovation Engine CLI..." @go build -o "$(IE_BINARY)" cmd/ie/ie.go @@ -12,7 +14,13 @@ build-api: @echo "Building the Innovation Engine API..." @go build -o "$(API_BINARY)" cmd/api/main.go -build-all: build-ie build-api +build-runner: build-ie build-api + @echo "Building the Innovation Engine Runner..." + @go build -o "$(BINARY_DIR)/runner" cmd/runner/main.go + +build-all: build-ie build-api build-runner + +# ------------------------------- Run targets ---------------------------------- run-ie: build-ie @echo "Running the Innovation Engine CLI" @@ -26,6 +34,13 @@ clean: @echo "Cleaning up" @rm -rf "$(BINARY_DIR)" +# ----------------------------- Docker targets --------------------------------- + +# Builds the API container. build-api-container: @echo "Building the Innovation Engine API container" @docker build -t innovation-engine-api -f infra/api/Dockerfile . + +deploy-api-container: build-api-container + @echo "Deploying the Innovation Engine API container" + @docker run -d -p 8080:8080 innovation-engine-api diff --git a/cmd/runner/main.go b/cmd/runner/main.go index e69de29b..e5a802c0 100644 --- a/cmd/runner/main.go +++ b/cmd/runner/main.go @@ -0,0 +1,13 @@ +package main + +import ( + "fmt" + "net/http" +) + +func main() { + fmt.Println("Hello, world!") + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "Hello, world!") + }) +} diff --git a/go.mod b/go.mod index 00beb838..b6618398 100644 --- a/go.mod +++ b/go.mod @@ -11,14 +11,52 @@ require ( require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/emicklei/go-restful/v3 v3.9.0 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.1 // indirect + github.com/go-openapi/swag v0.22.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.3 // indirect + github.com/google/gnostic v0.5.7-v3refs // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/gofuzz v1.1.0 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/imdario/mergo v0.3.6 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.8.2 // indirect + golang.org/x/net v0.8.0 // indirect + golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect golang.org/x/sys v0.8.0 // indirect + golang.org/x/term v0.6.0 // indirect + golang.org/x/text v0.8.0 // indirect + golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.28.1 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.27.1 // indirect + k8s.io/apimachinery v0.27.1 // indirect + k8s.io/client-go v0.27.1 // indirect + k8s.io/klog/v2 v2.90.1 // indirect + k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a // indirect + k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/go.sum b/go.sum index 3f8a1c9a..f7534ef9 100644 --- a/go.sum +++ b/go.sum @@ -1,50 +1,506 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= +cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= +cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= +cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= +cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= +cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= +cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= +cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= +cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= +cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= +cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= +cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= +cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= +cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= +cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= +cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/charmbracelet/lipgloss v0.7.1 h1:17WMwi7N1b1rVWOjMT+rCh7sQkvDU75B2hbZpc5Kc1E= github.com/charmbracelet/lipgloss v0.7.1/go.mod h1:yG0k3giv8Qj8edTCbbg6AlQ5e8KNWpFujkNawKNhE2c= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/emicklei/go-restful/v3 v3.9.0 h1:XwGDlfxEnQZzuopoqxwSEllNcCOM9DhhFyhFIIGKwxE= +github.com/emicklei/go-restful/v3 v3.9.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8= +github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.1.0 h1:Hsa8mG0dQ46ij8Sl2AYJDUv1oA9/d6Vk+3LG99Oe02g= +github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28= +github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs= github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU= github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= +golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= +golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= +golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b h1:clP8eMhB30EHdc0bd2Twtq6kgU7yl5ub2cQLSdrv1Dg= +golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= +golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= +golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= +google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= +google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= +google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= +google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +k8s.io/api v0.27.1 h1:Z6zUGQ1Vd10tJ+gHcNNNgkV5emCyW+v2XTmn+CLjSd0= +k8s.io/api v0.27.1/go.mod h1:z5g/BpAiD+f6AArpqNjkY+cji8ueZDU/WV1jcj5Jk4E= +k8s.io/apimachinery v0.27.1 h1:EGuZiLI95UQQcClhanryclaQE6xjg1Bts6/L3cD7zyc= +k8s.io/apimachinery v0.27.1/go.mod h1:5ikh59fK3AJ287GUvpUsryoMFtH9zj/ARfWCo3AyXTM= +k8s.io/client-go v0.27.1 h1:oXsfhW/qncM1wDmWBIuDzRHNS2tLhK3BZv512Nc59W8= +k8s.io/client-go v0.27.1/go.mod h1:f8LHMUkVb3b9N8bWturc+EDtVVVwZ7ueTVquFAJb2vA= +k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= +k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a h1:gmovKNur38vgoWfGtP5QOGNOA7ki4n6qNYoFAgMlNvg= +k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a/go.mod h1:y5VtZWM9sHHc2ZodIH/6SHzXj+TPU5USoA8lcIeKEKY= +k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY= +k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= +rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3 h1:PRbqxJClWWYMNV1dhaG4NsibJbArud9kFxnAMREiWFE= +sigs.k8s.io/structured-merge-diff/v4 v4.2.3/go.mod h1:qjx8mGObPmV2aSZepjQjbmb2ihdVs8cGKBraizNC69E= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/infra/runner/Dockerfile b/infra/runner/Dockerfile index e69de29b..da027cf0 100644 --- a/infra/runner/Dockerfile +++ b/infra/runner/Dockerfile @@ -0,0 +1,14 @@ +FROM mcr.microsoft.com/cbl-mariner/base/core:2.0 + +ARG HOST=0.0.0.0 +ARG PORT=8080 + +WORKDIR /api + +RUN tdnf install golang make -y + +COPY . . + +RUN make build-runner + +CMD ["./bin/api"] \ No newline at end of file diff --git a/internal/kube/client.go b/internal/kube/client.go new file mode 100644 index 00000000..becdbbda --- /dev/null +++ b/internal/kube/client.go @@ -0,0 +1,35 @@ +package kube + +import ( + "os" + "path/filepath" + + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" +) + +func GetKubernetesClient() (*kubernetes.Clientset, error) { + var config *rest.Config + var err error + + if _, err := rest.InClusterConfig(); err != nil { + kubeConfig := filepath.Join(os.Getenv("HOME"), ".kube", "config") + config, err = clientcmd.BuildConfigFromFlags("", kubeConfig) + if err != nil { + return nil, err + } + } else { + config, err = rest.InClusterConfig() + if err != nil { + return nil, err + } + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + return nil, err + } + + return clientset, nil +} From ca1987c34ef956d8d084481c075175257b49a25e Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 15 May 2023 19:30:45 -0700 Subject: [PATCH 039/226] [add] API kubernetes configuration for a local cluster. --- Makefile | 22 +++++++++++++++--- cmd/api/main.go | 49 +++++++++++++++++++++++++++++++++++++++ infra/api/Dockerfile | 5 +++- infra/api/deployment.yaml | 20 ++++++++++++++++ infra/api/ingress.yaml | 16 +++++++++++++ infra/api/service.yaml | 11 +++++++++ internal/kube/client.go | 2 ++ 7 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 infra/api/deployment.yaml create mode 100644 infra/api/ingress.yaml create mode 100644 infra/api/service.yaml diff --git a/Makefile b/Makefile index 23d073f9..4c8d82f0 100644 --- a/Makefile +++ b/Makefile @@ -41,6 +41,22 @@ build-api-container: @echo "Building the Innovation Engine API container" @docker build -t innovation-engine-api -f infra/api/Dockerfile . -deploy-api-container: build-api-container - @echo "Deploying the Innovation Engine API container" - @docker run -d -p 8080:8080 innovation-engine-api + +# ----------------------------- Kubernetes targets ----------------------------- + +# Applies the ingress controller to the cluster and waits for it to be ready. +k8s-apply-ingress-controller: + @echo "Deploying the ingress controller to your local cluster..." + @kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.7.1/deploy/static/provider/cloud/deploy.yaml + @kubectl wait --namespace ingress-nginx --for=condition=ready pod --selector=app.kubernetes.io/component=controller --timeout=120s + +# Deploys the API deployment, service, and ingress specifications to the +# cluster, allowing the API to be accessed via the ingress controller. +k8s-apply-api: build-api-container + @echo "Deploying the Innovation Engine API container to your local cluster..." + @kubectl apply -f infra/api/deployment.yaml + @kubectl apply -f infra/api/service.yaml + @kubectl apply -f infra/api/ingress.yaml + +k8s-initialize-cluster: k8s-apply-ingress-controller k8s-apply-api + @echo "Set up Kubernetes cluster for local development." \ No newline at end of file diff --git a/cmd/api/main.go b/cmd/api/main.go index e5a802c0..a1d4c21d 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -1,8 +1,15 @@ package main import ( + "context" "fmt" "net/http" + + batchv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/Azure/InnovationEngine/internal/kube" ) func main() { @@ -10,4 +17,46 @@ func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, world!") }) + + http.HandleFunc("/api/scenario", func(w http.ResponseWriter, r *http.Request) { + clientset, err := kube.GetKubernetesClient() + w.Header().Set("Content-Type", "application/json") + + if err != nil { + fmt.Fprintf(w, "Error: %s", err) + } + + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "innovation-engine-", + }, + Spec: batchv1.JobSpec{ + Template: v1.PodTemplateSpec{ + Spec: v1.PodSpec{ + RestartPolicy: v1.RestartPolicyNever, + Containers: []v1.Container{ + { + Name: "runner", + Image: "innovation-engine-runner", + Command: []string{ + "runner", + }, + }, + }, + }, + }, + }, + } + + job, err = clientset.BatchV1().Jobs("default").Create(context.TODO(), job, metav1.CreateOptions{}) + + if err != nil { + fmt.Fprintf(w, "Error: %s", err) + } + + fmt.Println(job) + + }) + + http.ListenAndServe(":8080", nil) } diff --git a/infra/api/Dockerfile b/infra/api/Dockerfile index eb3d0695..0fb593dc 100644 --- a/infra/api/Dockerfile +++ b/infra/api/Dockerfile @@ -5,10 +5,13 @@ ARG PORT=8080 WORKDIR /api -RUN tdnf install golang make -y +RUN tdnf update && \ + tdnf install golang make ca-certificates -y COPY . . RUN make build-api +EXPOSE ${PORT} + CMD ["./bin/api"] \ No newline at end of file diff --git a/infra/api/deployment.yaml b/infra/api/deployment.yaml new file mode 100644 index 00000000..46151e87 --- /dev/null +++ b/infra/api/deployment.yaml @@ -0,0 +1,20 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: innovation-engine-api +spec: + replicas: 2 + selector: + matchLabels: + app: innovation-engine-api + template: + metadata: + labels: + app: innovation-engine-api + spec: + containers: + - name: innovation-engine-api + image: innovation-engine-api:latest + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8080 \ No newline at end of file diff --git a/infra/api/ingress.yaml b/infra/api/ingress.yaml new file mode 100644 index 00000000..4c89ea61 --- /dev/null +++ b/infra/api/ingress.yaml @@ -0,0 +1,16 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: innovation-engine-api-ingress +spec: + rules: + - host: innovation-engine-api.localhost + http: + paths: + - path: "/" + pathType: Prefix + backend: + service: + name: innovation-engine-api + port: + number: 80 \ No newline at end of file diff --git a/infra/api/service.yaml b/infra/api/service.yaml new file mode 100644 index 00000000..7ff6ce4c --- /dev/null +++ b/infra/api/service.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Service +metadata: + name: innovation-engine-api +spec: + selector: + app: innovation-engine-api + ports: + - protocol: TCP + port: 80 + targetPort: 8080 \ No newline at end of file diff --git a/internal/kube/client.go b/internal/kube/client.go index becdbbda..f26601da 100644 --- a/internal/kube/client.go +++ b/internal/kube/client.go @@ -9,6 +9,8 @@ import ( "k8s.io/client-go/tools/clientcmd" ) +// Obtains the Kubernetes clientset based on the environment +// this function is executing in. func GetKubernetesClient() (*kubernetes.Clientset, error) { var config *rest.Config var err error From 3907629288b34c6d689b381cd74ec2c3712cacf2 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 15 May 2023 20:18:17 -0700 Subject: [PATCH 040/226] [add] more cluster operations & update API infra. --- Makefile | 28 +++++++++++++++++++++++----- cmd/api/main.go | 9 +++++++-- infra/api/deployment.yaml | 2 +- infra/api/ingress.yaml | 4 ++-- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 4c8d82f0..12490067 100644 --- a/Makefile +++ b/Makefile @@ -36,27 +36,45 @@ clean: # ----------------------------- Docker targets --------------------------------- +API_IMAGE_TAG ?= latest + # Builds the API container. build-api-container: @echo "Building the Innovation Engine API container" - @docker build -t innovation-engine-api -f infra/api/Dockerfile . + @docker build -t innovation-engine-api:$(API_IMAGE_TAG) -f infra/api/Dockerfile . # ----------------------------- Kubernetes targets ----------------------------- # Applies the ingress controller to the cluster and waits for it to be ready. -k8s-apply-ingress-controller: +k8s-deploy-ingress-controller: @echo "Deploying the ingress controller to your local cluster..." @kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.7.1/deploy/static/provider/cloud/deploy.yaml @kubectl wait --namespace ingress-nginx --for=condition=ready pod --selector=app.kubernetes.io/component=controller --timeout=120s # Deploys the API deployment, service, and ingress specifications to the # cluster, allowing the API to be accessed via the ingress controller. -k8s-apply-api: build-api-container +k8s-deploy-api: build-api-container @echo "Deploying the Innovation Engine API container to your local cluster..." @kubectl apply -f infra/api/deployment.yaml @kubectl apply -f infra/api/service.yaml @kubectl apply -f infra/api/ingress.yaml -k8s-initialize-cluster: k8s-apply-ingress-controller k8s-apply-api - @echo "Set up Kubernetes cluster for local development." \ No newline at end of file +k8s-initialize-cluster: k8s-deploy-ingress-controller k8s-deploy-api + @echo "Set up Kubernetes cluster for local development." + +k8s-delete-ingress-controller: + @echo "Deleting the ingress controller from your local cluster..." + @kubectl delete -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.7.1/deploy/static/provider/cloud/deploy.yaml + +k8s-delete-api: + @echo "Deleting the Innovation Engine API container from your local cluster..." + @kubectl delete -f infra/api/deployment.yaml + @kubectl delete -f infra/api/service.yaml + @kubectl delete -f infra/api/ingress.yaml + +k8s-delete-cluster: k8s-delete-api k8s-delete-ingress-controller + @echo "Deleted Kubernetes cluster for local development." + +k8s-refresh: k8s-delete-cluster k8s-initialize-cluster + @echo "Refreshed Kubernetes cluster for local development." \ No newline at end of file diff --git a/cmd/api/main.go b/cmd/api/main.go index a1d4c21d..358092db 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "net/http" + "path" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" @@ -12,13 +13,17 @@ import ( "github.com/Azure/InnovationEngine/internal/kube" ) +var PREFIX_BASE = "/api/ie" + func main() { fmt.Println("Hello, world!") - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + http.HandleFunc(PREFIX_BASE, func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "Hello, world!") + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"message": "Hello, world!"}`)) }) - http.HandleFunc("/api/scenario", func(w http.ResponseWriter, r *http.Request) { + http.HandleFunc(path.Join(PREFIX_BASE, "scenario"), func(w http.ResponseWriter, r *http.Request) { clientset, err := kube.GetKubernetesClient() w.Header().Set("Content-Type", "application/json") diff --git a/infra/api/deployment.yaml b/infra/api/deployment.yaml index 46151e87..64672558 100644 --- a/infra/api/deployment.yaml +++ b/infra/api/deployment.yaml @@ -15,6 +15,6 @@ spec: containers: - name: innovation-engine-api image: innovation-engine-api:latest - imagePullPolicy: IfNotPresent + imagePullPolicy: Always ports: - containerPort: 8080 \ No newline at end of file diff --git a/infra/api/ingress.yaml b/infra/api/ingress.yaml index 4c89ea61..3f16328f 100644 --- a/infra/api/ingress.yaml +++ b/infra/api/ingress.yaml @@ -4,10 +4,10 @@ metadata: name: innovation-engine-api-ingress spec: rules: - - host: innovation-engine-api.localhost + - host: innovation-engine.localhost http: paths: - - path: "/" + - path: "/api/ie" pathType: Prefix backend: service: From f2f90e1d46ed91bbf3b5487c0a3bed217551dc61 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 16 May 2023 15:20:29 -0700 Subject: [PATCH 041/226] [update] server to use echo & update some of the infra. --- cmd/api/main.go | 25 ++++++++++++++----------- go.mod | 9 ++++++++- go.sum | 26 ++++++++++++++++++++++++++ infra/api/Dockerfile | 2 +- infra/api/deployment.yaml | 2 +- infra/api/ingress.yaml | 4 ++-- infra/api/service.yaml | 2 +- 7 files changed, 53 insertions(+), 17 deletions(-) diff --git a/cmd/api/main.go b/cmd/api/main.go index 358092db..88e10804 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -11,24 +11,28 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/Azure/InnovationEngine/internal/kube" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" ) var PREFIX_BASE = "/api/ie" func main() { fmt.Println("Hello, world!") - http.HandleFunc(PREFIX_BASE, func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "Hello, world!") - w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`{"message": "Hello, world!"}`)) + server := echo.New() + + server.Use(middleware.Logger()) + server.Use(middleware.Recover()) + + server.GET(path.Join(PREFIX_BASE, "hello"), func(c echo.Context) error { + return c.JSON(http.StatusOK, map[string]string{"message": "Hello, world!"}) }) - http.HandleFunc(path.Join(PREFIX_BASE, "scenario"), func(w http.ResponseWriter, r *http.Request) { + server.POST(PREFIX_BASE, func(c echo.Context) error { clientset, err := kube.GetKubernetesClient() - w.Header().Set("Content-Type", "application/json") if err != nil { - fmt.Fprintf(w, "Error: %s", err) + return c.JSON(http.StatusInternalServerError, map[string]string{"message": err.Error()}) } job := &batchv1.Job{ @@ -56,12 +60,11 @@ func main() { job, err = clientset.BatchV1().Jobs("default").Create(context.TODO(), job, metav1.CreateOptions{}) if err != nil { - fmt.Fprintf(w, "Error: %s", err) + return c.JSON(http.StatusInternalServerError, map[string]string{"message": err.Error()}) } - fmt.Println(job) - + return c.JSON(http.StatusOK, map[string]string{"message": "Hello, world!", "job": job.Name}) }) - http.ListenAndServe(":8080", nil) + server.Logger.Fatal(server.Start("0.0.0.0:8080")) } diff --git a/go.mod b/go.mod index b6618398..89e155d2 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/go-openapi/jsonreference v0.20.1 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/gnostic v0.5.7-v3refs // indirect github.com/google/go-cmp v0.5.9 // indirect @@ -27,8 +28,11 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/labstack/echo/v4 v4.10.2 // indirect + github.com/labstack/gommon v0.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.18 // indirect github.com/mattn/go-runewidth v0.0.14 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -39,12 +43,15 @@ require ( github.com/rivo/uniseg v0.4.4 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.8.2 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/crypto v0.6.0 // indirect golang.org/x/net v0.8.0 // indirect golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect golang.org/x/sys v0.8.0 // indirect golang.org/x/term v0.6.0 // indirect golang.org/x/text v0.8.0 // indirect - golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect + golang.org/x/time v0.3.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index f7534ef9..6f78f89f 100644 --- a/go.sum +++ b/go.sum @@ -69,6 +69,8 @@ github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/ github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= +github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -149,10 +151,19 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M= +github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k= +github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8= +github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.11/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= @@ -189,11 +200,17 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -210,6 +227,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -313,7 +332,11 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -333,6 +356,8 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -474,6 +499,7 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/infra/api/Dockerfile b/infra/api/Dockerfile index 0fb593dc..38bd2c7f 100644 --- a/infra/api/Dockerfile +++ b/infra/api/Dockerfile @@ -12,6 +12,6 @@ COPY . . RUN make build-api -EXPOSE ${PORT} +EXPOSE 8080 CMD ["./bin/api"] \ No newline at end of file diff --git a/infra/api/deployment.yaml b/infra/api/deployment.yaml index 64672558..46151e87 100644 --- a/infra/api/deployment.yaml +++ b/infra/api/deployment.yaml @@ -15,6 +15,6 @@ spec: containers: - name: innovation-engine-api image: innovation-engine-api:latest - imagePullPolicy: Always + imagePullPolicy: IfNotPresent ports: - containerPort: 8080 \ No newline at end of file diff --git a/infra/api/ingress.yaml b/infra/api/ingress.yaml index 3f16328f..ec544529 100644 --- a/infra/api/ingress.yaml +++ b/infra/api/ingress.yaml @@ -7,10 +7,10 @@ spec: - host: innovation-engine.localhost http: paths: - - path: "/api/ie" + - path: "/" pathType: Prefix backend: service: - name: innovation-engine-api + name: innovation-engine-api-service port: number: 80 \ No newline at end of file diff --git a/infra/api/service.yaml b/infra/api/service.yaml index 7ff6ce4c..938112f0 100644 --- a/infra/api/service.yaml +++ b/infra/api/service.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Service metadata: - name: innovation-engine-api + name: innovation-engine-api-service spec: selector: app: innovation-engine-api From 0e6e4725fbd3911bc4f6075e09c948ce6ba5e67a Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 16 May 2023 15:56:41 -0700 Subject: [PATCH 042/226] [update] routes & add refresh only for the API. --- Makefile | 5 ++++- cmd/api/main.go | 16 ++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 12490067..6926c18b 100644 --- a/Makefile +++ b/Makefile @@ -73,8 +73,11 @@ k8s-delete-api: @kubectl delete -f infra/api/service.yaml @kubectl delete -f infra/api/ingress.yaml +k8s-refresh-api: k8s-delete-api k8s-deploy-api + @echo "Refreshed the Innovation Engine API container in your local cluster..." + k8s-delete-cluster: k8s-delete-api k8s-delete-ingress-controller @echo "Deleted Kubernetes cluster for local development." -k8s-refresh: k8s-delete-cluster k8s-initialize-cluster +k8s-refresh-cluster: k8s-delete-cluster k8s-initialize-cluster @echo "Refreshed Kubernetes cluster for local development." \ No newline at end of file diff --git a/cmd/api/main.go b/cmd/api/main.go index 88e10804..6be5d18f 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "fmt" "net/http" "path" @@ -15,20 +14,25 @@ import ( "github.com/labstack/echo/v4/middleware" ) -var PREFIX_BASE = "/api/ie" +var ( + BASE_ROUTE = "/api" + HEALTH_ROUTE = path.Join(BASE_ROUTE, "health") + EXECUTION_ROUTE = path.Join(BASE_ROUTE, "execute") + DEPLOYMENTS_ROUTE = path.Join(BASE_ROUTE, "deployments") +) func main() { - fmt.Println("Hello, world!") server := echo.New() + // Setup middleware. server.Use(middleware.Logger()) server.Use(middleware.Recover()) - server.GET(path.Join(PREFIX_BASE, "hello"), func(c echo.Context) error { + server.GET(HEALTH_ROUTE, func(c echo.Context) error { return c.JSON(http.StatusOK, map[string]string{"message": "Hello, world!"}) }) - server.POST(PREFIX_BASE, func(c echo.Context) error { + server.POST(EXECUTION_ROUTE, func(c echo.Context) error { clientset, err := kube.GetKubernetesClient() if err != nil { @@ -66,5 +70,5 @@ func main() { return c.JSON(http.StatusOK, map[string]string{"message": "Hello, world!", "job": job.Name}) }) - server.Logger.Fatal(server.Start("0.0.0.0:8080")) + server.Logger.Fatal(server.Start(":8080")) } From 8432ed14bd5136ca6296f78b30abd0133e19e4cb Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 16 May 2023 16:38:15 -0700 Subject: [PATCH 043/226] [add] runner deployment & service implementations. --- internal/kube/deployments.go | 49 ++++++++++++++++++++++++++++++++++++ internal/kube/services.go | 27 ++++++++++++++++++++ internal/utils/ints.go | 3 +++ 3 files changed, 79 insertions(+) create mode 100644 internal/kube/deployments.go create mode 100644 internal/kube/services.go create mode 100644 internal/utils/ints.go diff --git a/internal/kube/deployments.go b/internal/kube/deployments.go new file mode 100644 index 00000000..cb4d2e4b --- /dev/null +++ b/internal/kube/deployments.go @@ -0,0 +1,49 @@ +package kube + +import ( + "github.com/Azure/InnovationEngine/internal/utils" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func GetRunnerDeployment(id string) *appsv1.Deployment { + + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "runner-" + id, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: utils.Int32Ptr(1), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + "app": "runner", + "id": id, + }, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + "app": "runner", + "id": id, + }, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "runner", + Image: "innovation-engine-runner", + Ports: []corev1.ContainerPort{ + { + Name: "http", + ContainerPort: 8080, + Protocol: corev1.ProtocolTCP, + }, + }, + }, + }, + }, + }, + }, + } +} diff --git a/internal/kube/services.go b/internal/kube/services.go new file mode 100644 index 00000000..f4332640 --- /dev/null +++ b/internal/kube/services.go @@ -0,0 +1,27 @@ +package kube + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func GetRunnerService(id string) *corev1.Service { + return &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: "runner - " + id, + }, + Spec: corev1.ServiceSpec{ + Selector: map[string]string{ + "app": "runner", + "id": id, + }, + Ports: []corev1.ServicePort{ + { + Name: "http", + Port: 8080, + Protocol: corev1.ProtocolTCP, + }, + }, + }, + } +} diff --git a/internal/utils/ints.go b/internal/utils/ints.go new file mode 100644 index 00000000..eaae8c30 --- /dev/null +++ b/internal/utils/ints.go @@ -0,0 +1,3 @@ +package utils + +func Int32Ptr(i int32) *int32 { return &i } From 6cb40981100c5ddba6758ac88d8598d33bd5fc0b Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 16 May 2023 20:53:23 -0700 Subject: [PATCH 044/226] [update] runner creation to be done through deployment & service specifications. --- cmd/api/main.go | 43 ++++++++++-------------------------- cmd/api/types.go | 8 +++++++ infra/runner/Dockerfile | 2 +- internal/kube/deployments.go | 7 ++++++ internal/kube/services.go | 7 ++++++ 5 files changed, 35 insertions(+), 32 deletions(-) create mode 100644 cmd/api/types.go diff --git a/cmd/api/main.go b/cmd/api/main.go index 6be5d18f..f39273f8 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -1,15 +1,11 @@ package main import ( - "context" "net/http" "path" - batchv1 "k8s.io/api/batch/v1" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "github.com/Azure/InnovationEngine/internal/kube" + "github.com/google/uuid" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" ) @@ -29,45 +25,30 @@ func main() { server.Use(middleware.Recover()) server.GET(HEALTH_ROUTE, func(c echo.Context) error { - return c.JSON(http.StatusOK, map[string]string{"message": "Hello, world!"}) + return c.JSON(http.StatusOK, map[string]string{"message": "OK"}) }) server.POST(EXECUTION_ROUTE, func(c echo.Context) error { clientset, err := kube.GetKubernetesClient() + id := uuid.New().String() + + // Create deployment + deployment := kube.GetRunnerDeployment(id) + _, err = kube.CreateRunnerDeployment(clientset, deployment) + if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"message": err.Error()}) } - job := &batchv1.Job{ - ObjectMeta: metav1.ObjectMeta{ - GenerateName: "innovation-engine-", - }, - Spec: batchv1.JobSpec{ - Template: v1.PodTemplateSpec{ - Spec: v1.PodSpec{ - RestartPolicy: v1.RestartPolicyNever, - Containers: []v1.Container{ - { - Name: "runner", - Image: "innovation-engine-runner", - Command: []string{ - "runner", - }, - }, - }, - }, - }, - }, - } - - job, err = clientset.BatchV1().Jobs("default").Create(context.TODO(), job, metav1.CreateOptions{}) - + // Create service + service := kube.GetRunnerService(id) + _, err = kube.CreateRunnerService(clientset, service) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"message": err.Error()}) } - return c.JSON(http.StatusOK, map[string]string{"message": "Hello, world!", "job": job.Name}) + return c.JSON(http.StatusOK, map[string]string{"deployment": deployment.Name, "service": service.Name}) }) server.Logger.Fatal(server.Start(":8080")) diff --git a/cmd/api/types.go b/cmd/api/types.go new file mode 100644 index 00000000..e8959542 --- /dev/null +++ b/cmd/api/types.go @@ -0,0 +1,8 @@ +package main + +import "github.com/Azure/InnovationEngine/internal/engine" + +type ExecuteResponse struct { + RunnerID string `json:"runnerID"` + Steps []engine.Step `json:"steps"` +} diff --git a/infra/runner/Dockerfile b/infra/runner/Dockerfile index da027cf0..3ccd4c1a 100644 --- a/infra/runner/Dockerfile +++ b/infra/runner/Dockerfile @@ -11,4 +11,4 @@ COPY . . RUN make build-runner -CMD ["./bin/api"] \ No newline at end of file +CMD ["./bin/runner"] \ No newline at end of file diff --git a/internal/kube/deployments.go b/internal/kube/deployments.go index cb4d2e4b..dcda32dd 100644 --- a/internal/kube/deployments.go +++ b/internal/kube/deployments.go @@ -1,10 +1,13 @@ package kube import ( + "context" + "github.com/Azure/InnovationEngine/internal/utils" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" ) func GetRunnerDeployment(id string) *appsv1.Deployment { @@ -47,3 +50,7 @@ func GetRunnerDeployment(id string) *appsv1.Deployment { }, } } + +func CreateRunnerDeployment(clientset *kubernetes.Clientset, deployment *appsv1.Deployment) (*appsv1.Deployment, error) { + return clientset.AppsV1().Deployments("default").Create(context.TODO(), deployment, metav1.CreateOptions{}) +} diff --git a/internal/kube/services.go b/internal/kube/services.go index f4332640..02f4ed0d 100644 --- a/internal/kube/services.go +++ b/internal/kube/services.go @@ -1,8 +1,11 @@ package kube import ( + "context" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" ) func GetRunnerService(id string) *corev1.Service { @@ -25,3 +28,7 @@ func GetRunnerService(id string) *corev1.Service { }, } } + +func CreateRunnerService(clientset *kubernetes.Clientset, service *corev1.Service) (*corev1.Service, error) { + return clientset.CoreV1().Services("default").Create(context.TODO(), service, metav1.CreateOptions{}) +} From 8206edf2994e951cfe73fea9185c067944a9e952 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Wed, 17 May 2023 09:17:05 -0700 Subject: [PATCH 045/226] [update] runners -> agents. --- cmd/api/main.go | 8 ++++---- internal/kube/deployments.go | 4 ++-- internal/kube/services.go | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd/api/main.go b/cmd/api/main.go index f39273f8..dffdaadb 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -34,16 +34,16 @@ func main() { id := uuid.New().String() // Create deployment - deployment := kube.GetRunnerDeployment(id) - _, err = kube.CreateRunnerDeployment(clientset, deployment) + deployment := kube.GetAgentDeployment(id) + _, err = kube.CreateAgentDeployment(clientset, deployment) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"message": err.Error()}) } // Create service - service := kube.GetRunnerService(id) - _, err = kube.CreateRunnerService(clientset, service) + service := kube.GetAgentService(id) + _, err = kube.CreateAgentService(clientset, service) if err != nil { return c.JSON(http.StatusInternalServerError, map[string]string{"message": err.Error()}) } diff --git a/internal/kube/deployments.go b/internal/kube/deployments.go index dcda32dd..98db6afd 100644 --- a/internal/kube/deployments.go +++ b/internal/kube/deployments.go @@ -10,7 +10,7 @@ import ( "k8s.io/client-go/kubernetes" ) -func GetRunnerDeployment(id string) *appsv1.Deployment { +func GetAgentDeployment(id string) *appsv1.Deployment { return &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{ @@ -51,6 +51,6 @@ func GetRunnerDeployment(id string) *appsv1.Deployment { } } -func CreateRunnerDeployment(clientset *kubernetes.Clientset, deployment *appsv1.Deployment) (*appsv1.Deployment, error) { +func CreateAgentDeployment(clientset *kubernetes.Clientset, deployment *appsv1.Deployment) (*appsv1.Deployment, error) { return clientset.AppsV1().Deployments("default").Create(context.TODO(), deployment, metav1.CreateOptions{}) } diff --git a/internal/kube/services.go b/internal/kube/services.go index 02f4ed0d..39c6cbfa 100644 --- a/internal/kube/services.go +++ b/internal/kube/services.go @@ -8,7 +8,7 @@ import ( "k8s.io/client-go/kubernetes" ) -func GetRunnerService(id string) *corev1.Service { +func GetAgentService(id string) *corev1.Service { return &corev1.Service{ ObjectMeta: metav1.ObjectMeta{ Name: "runner - " + id, @@ -29,6 +29,6 @@ func GetRunnerService(id string) *corev1.Service { } } -func CreateRunnerService(clientset *kubernetes.Clientset, service *corev1.Service) (*corev1.Service, error) { +func CreateAgentService(clientset *kubernetes.Clientset, service *corev1.Service) (*corev1.Service, error) { return clientset.CoreV1().Services("default").Create(context.TODO(), service, metav1.CreateOptions{}) } From 03c1b80c7c7f5a360f8ff2e87fd10bef19a364a0 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Wed, 17 May 2023 15:23:55 -0700 Subject: [PATCH 046/226] [add] error output to command. --- internal/shells/bash.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/shells/bash.go b/internal/shells/bash.go index 9109754e..813d8cec 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -20,7 +20,7 @@ func ExecuteBashCommand(command string, env map[string]string, inherit_environme out, err := commandToExecute.Output() if err != nil { - return "", fmt.Errorf("error executing bash command: %w", err) + return "", fmt.Errorf("command exited with '%w' and the message '%s'", err, out) } return string(out), nil } From fd91b5e82a847e92c5006d0e3557b23e11fda6e8 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Wed, 17 May 2023 21:25:32 -0700 Subject: [PATCH 047/226] [update] error message output. --- internal/shells/bash.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/shells/bash.go b/internal/shells/bash.go index 813d8cec..7525a16b 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -18,9 +18,9 @@ func ExecuteBashCommand(command string, env map[string]string, inherit_environme commandToExecute.Env = append(commandToExecute.Env, fmt.Sprintf("%s=%s", k, v)) } - out, err := commandToExecute.Output() + stdOutAndErr, err := commandToExecute.CombinedOutput() if err != nil { - return "", fmt.Errorf("command exited with '%w' and the message '%s'", err, out) + return "", fmt.Errorf("command exited with '%w' and the message '%s'", err, stdOutAndErr) } - return string(out), nil + return string(stdOutAndErr), nil } From 8f99a3b9b7b57b6a028dc26ff5ebd8b188ea9841 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 18 May 2023 17:01:26 -0700 Subject: [PATCH 048/226] [update] environment state to be shared across commands, add flag for verbose. --- cmd/api/types.go | 11 +++--- cmd/ie/commands/execute.go | 7 +++- internal/engine/engine.go | 23 +++++++------ internal/engine/execution.go | 10 ++++-- internal/shells/bash.go | 65 ++++++++++++++++++++++++++++++++++-- internal/utils/maps.go | 17 ++++++++++ 6 files changed, 112 insertions(+), 21 deletions(-) create mode 100644 internal/utils/maps.go diff --git a/cmd/api/types.go b/cmd/api/types.go index e8959542..454727e7 100644 --- a/cmd/api/types.go +++ b/cmd/api/types.go @@ -1,8 +1,11 @@ package main -import "github.com/Azure/InnovationEngine/internal/engine" +type DeploymentStep struct { + Name string `json:"name"` + Command string `json:"command"` +} -type ExecuteResponse struct { - RunnerID string `json:"runnerID"` - Steps []engine.Step `json:"steps"` +type DeploymentResponse struct { + AgentWebsocketUrl string `json:"agentWebsocketUrl"` + Steps []DeploymentStep `json:"steps"` } diff --git a/cmd/ie/commands/execute.go b/cmd/ie/commands/execute.go index 4129a52f..0aa345c7 100644 --- a/cmd/ie/commands/execute.go +++ b/cmd/ie/commands/execute.go @@ -8,6 +8,7 @@ import ( // / Register the command with our command runner. func init() { rootCommand.AddCommand(executeCommand) + rootCommand.PersistentFlags().Bool("verbose", false, "Enable verbose logging & standard output.") } var executeCommand = &cobra.Command{ @@ -21,7 +22,11 @@ var executeCommand = &cobra.Command{ return } - innovationEngine := engine.NewEngine() + verbose, _ := cmd.Flags().GetBool("verbose") + + innovationEngine := engine.NewEngine(engine.EngineConfiguration{ + Verbose: verbose, + }) scenario, err := engine.CreateScenarioFromMarkdown(markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}) if err != nil { panic(err) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 27cb4c5e..f34185d6 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -1,30 +1,31 @@ package engine -import "fmt" +import ( + "fmt" + + "github.com/Azure/InnovationEngine/internal/shells" +) type EngineConfiguration struct { - supportedLanguages []string `yaml:"supportedLanguages"` + Verbose bool } type Engine struct { Configuration EngineConfiguration } -func LoadConfiguration() EngineConfiguration { - return EngineConfiguration{ - supportedLanguages: []string{"bash", "azurecli", "azurecli-interactive", "terraform"}, - } -} - // / Create a new engine instance. -func NewEngine() *Engine { - return &Engine{} +func NewEngine(configuration EngineConfiguration) *Engine { + return &Engine{ + Configuration: configuration, + } } // / Executes a scenario. func (e *Engine) ExecuteScenario(scenario *Scenario) error { fmt.Println(titleStyle.Render(scenario.Name)) - ExecuteAndRenderSteps(scenario.Steps, scenario.Environment) + ExecuteAndRenderSteps(scenario.Steps, scenario.Environment, e.Configuration.Verbose) + shells.ResetStoredEnvironmentVariables() fmt.Printf("---Generated script---\n%s", scenario.ToShellScript()) return nil } diff --git a/internal/engine/execution.go b/internal/engine/execution.go index f93dd76f..7623a1fd 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -7,6 +7,7 @@ import ( "github.com/Azure/InnovationEngine/internal/parsers" "github.com/Azure/InnovationEngine/internal/shells" + "github.com/Azure/InnovationEngine/internal/utils" "github.com/charmbracelet/lipgloss" ) @@ -39,7 +40,7 @@ func indentMultiLineCommand(content string, index string) string { } // Executes the steps from a scenario and renders the output to the terminal. -func ExecuteAndRenderSteps(steps []Step, env map[string]string) { +func ExecuteAndRenderSteps(steps []Step, env map[string]string, verbose bool) { for stepNumber, step := range steps { fmt.Printf("%d. %s\n", stepNumber+1, step.Name) for _, block := range step.CodeBlocks { @@ -59,8 +60,10 @@ func ExecuteAndRenderSteps(steps []Step, env map[string]string) { // execute the command as a goroutine to allow for the spinner to be // rendered while the command is executing. done := make(chan error) + var commandOutput string go func(block parsers.CodeBlock) { - _, err := shells.ExecuteBashCommand(block.Content, env, true) + output, err := shells.ExecuteBashCommand(block.Content, utils.CopyMap(env), true) + commandOutput = output done <- err }(block) @@ -78,6 +81,9 @@ func ExecuteAndRenderSteps(steps []Step, env map[string]string) { if err == nil { fmt.Printf("\r %s \n", checkStyle.Render("✔")) fmt.Printf("\033[%dB", lines) + if verbose { + fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Render(commandOutput)) + } } else { fmt.Printf("\r %s \n", errorStyle.Render("✗")) fmt.Printf("\033[%dB", lines) diff --git a/internal/shells/bash.go b/internal/shells/bash.go index 7525a16b..75945b3b 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -1,26 +1,85 @@ package shells import ( + "bufio" "fmt" "os" "os/exec" + "strings" + + "github.com/Azure/InnovationEngine/internal/utils" ) +// Location where the environment state from commands is captured and sent to +// for being able to share state across commands. +var environmentStateFile = "/tmp/env.txt" + +func loadEnvFile(path string) (map[string]string, error) { + if !utils.FileExists(path) { + return nil, fmt.Errorf("env file '%s' does not exist", path) + } + + file, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("failed to open env file '%s': %w", path, err) + } + + defer file.Close() + + scanner := bufio.NewScanner(file) + env := make(map[string]string) + + for scanner.Scan() { + line := scanner.Text() + if strings.Contains(line, "=") { + parts := strings.Split(line, "=") + env[parts[0]] = parts[1] + } + } + return env, nil +} + +// Resets the stored environment variables file. +func ResetStoredEnvironmentVariables() error { + return os.Remove(environmentStateFile) +} + +func mergeMaps(a, b map[string]string) map[string]string { + for k, v := range b { + a[k] = v + } + + return a +} + // Executes a bash command and returns the output or error. func ExecuteBashCommand(command string, env map[string]string, inherit_environment_variables bool) (string, error) { - commandToExecute := exec.Command("bash", "-c", command) + var commandWithState = []string{ + command, + "env > /tmp/env.txt", + } + commandToExecute := exec.Command("bash", "-c", strings.Join(commandWithState, "\n")) if inherit_environment_variables { commandToExecute.Env = os.Environ() } - for k, v := range env { - commandToExecute.Env = append(commandToExecute.Env, fmt.Sprintf("%s=%s", k, v)) + envFromPreviousStep, err := loadEnvFile("/tmp/env.txt") + if err == nil { + merged := utils.MergeMaps(env, envFromPreviousStep) + for k, v := range merged { + commandToExecute.Env = append(commandToExecute.Env, fmt.Sprintf("%s=%s", k, v)) + } + } else { + for k, v := range env { + commandToExecute.Env = append(commandToExecute.Env, fmt.Sprintf("%s=%s", k, v)) + } } stdOutAndErr, err := commandToExecute.CombinedOutput() if err != nil { return "", fmt.Errorf("command exited with '%w' and the message '%s'", err, stdOutAndErr) } + return string(stdOutAndErr), nil } diff --git a/internal/utils/maps.go b/internal/utils/maps.go new file mode 100644 index 00000000..a97dcbc7 --- /dev/null +++ b/internal/utils/maps.go @@ -0,0 +1,17 @@ +package utils + +func CopyMap(m map[string]string) map[string]string { + result := make(map[string]string) + for k, v := range m { + result[k] = v + } + return result +} + +func MergeMaps(a, b map[string]string) map[string]string { + for k, v := range b { + a[k] = v + } + + return a +} From a0a82ad123e6b1ba9495fdf2ba578432ed1fe52a Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 18 May 2023 22:11:26 -0700 Subject: [PATCH 049/226] [update] indendation. --- internal/engine/execution.go | 11 +++++++---- internal/shells/bash.go | 19 ++++++++----------- internal/utils/maps.go | 7 +++++-- 3 files changed, 20 insertions(+), 17 deletions(-) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 7623a1fd..7689f98c 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -29,12 +29,15 @@ var ( // Indents a multi-line command to be nested under the first line of the // command. -func indentMultiLineCommand(content string, index string) string { +func indentMultiLineCommand(content string, indentation int) string { lines := strings.Split(content, "\n") for i := 1; i < len(lines); i++ { if strings.HasSuffix(strings.TrimSpace(lines[i-1]), "\\") { - lines[i] = index + lines[i] + lines[i] = strings.Repeat(" ", indentation) + lines[i] + } else if strings.TrimSpace(lines[i]) != "" { + lines[i] = strings.Repeat(" ", indentation) + lines[i] } + } return strings.Join(lines, "\n") } @@ -45,7 +48,7 @@ func ExecuteAndRenderSteps(steps []Step, env map[string]string, verbose bool) { fmt.Printf("%d. %s\n", stepNumber+1, step.Name) for _, block := range step.CodeBlocks { // Render the codeblock. - indentedBlock := indentMultiLineCommand(block.Content, " ") + indentedBlock := indentMultiLineCommand(block.Content, 4) fmt.Print(" " + indentedBlock) // Grab the number of lines it contains & set the cursor to the @@ -80,7 +83,7 @@ func ExecuteAndRenderSteps(steps []Step, env map[string]string, verbose bool) { fmt.Print("\033[?25h") if err == nil { fmt.Printf("\r %s \n", checkStyle.Render("✔")) - fmt.Printf("\033[%dB", lines) + fmt.Printf("\033[%dB\n", lines) if verbose { fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Render(commandOutput)) } diff --git a/internal/shells/bash.go b/internal/shells/bash.go index 75945b3b..f4df54f3 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -44,27 +44,24 @@ func ResetStoredEnvironmentVariables() error { return os.Remove(environmentStateFile) } -func mergeMaps(a, b map[string]string) map[string]string { - for k, v := range b { - a[k] = v - } - - return a -} - // Executes a bash command and returns the output or error. func ExecuteBashCommand(command string, env map[string]string, inherit_environment_variables bool) (string, error) { - var commandWithState = []string{ + var commandWithStateSaved = []string{ command, "env > /tmp/env.txt", } - commandToExecute := exec.Command("bash", "-c", strings.Join(commandWithState, "\n")) + commandToExecute := exec.Command("bash", "-c", strings.Join(commandWithStateSaved, "\n")) if inherit_environment_variables { commandToExecute.Env = os.Environ() } - envFromPreviousStep, err := loadEnvFile("/tmp/env.txt") + // Sharing environment variable state between isolated shell executions is a + // bit tough, but how we handle it is by storing the environment variables + // after a command is executed within a file and then loading that file + // before executing the next command. This allows us to share state between + // isolated command calls. + envFromPreviousStep, err := loadEnvFile(environmentStateFile) if err == nil { merged := utils.MergeMaps(env, envFromPreviousStep) for k, v := range merged { diff --git a/internal/utils/maps.go b/internal/utils/maps.go index a97dcbc7..a12bae61 100644 --- a/internal/utils/maps.go +++ b/internal/utils/maps.go @@ -1,5 +1,6 @@ package utils +// Makes a copy of a map func CopyMap(m map[string]string) map[string]string { result := make(map[string]string) for k, v := range m { @@ -8,10 +9,12 @@ func CopyMap(m map[string]string) map[string]string { return result } +// Merge two maps together. func MergeMaps(a, b map[string]string) map[string]string { + merged := CopyMap(a) for k, v := range b { - a[k] = v + merged[k] = v } - return a + return merged } From 83af4a89521d522ce5908df68b133d640af362a4 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 18 May 2023 22:28:56 -0700 Subject: [PATCH 050/226] [update] shell script output. --- internal/engine/engine.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index f34185d6..a953c193 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -4,6 +4,12 @@ import ( "fmt" "github.com/Azure/InnovationEngine/internal/shells" + "github.com/charmbracelet/lipgloss" +) + +var ( + scriptHeader = lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Bold(true) + scriptText = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")) ) type EngineConfiguration struct { @@ -26,6 +32,6 @@ func (e *Engine) ExecuteScenario(scenario *Scenario) error { fmt.Println(titleStyle.Render(scenario.Name)) ExecuteAndRenderSteps(scenario.Steps, scenario.Environment, e.Configuration.Verbose) shells.ResetStoredEnvironmentVariables() - fmt.Printf("---Generated script---\n%s", scenario.ToShellScript()) + fmt.Printf(scriptHeader.Render("# Generated bash replicate the deployment:")+"\n%s", scriptText.Render(scenario.ToShellScript())) return nil } From d3a149bb4317d1927eb345f5915cba4f2c655fc5 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 18 May 2023 22:36:03 -0700 Subject: [PATCH 051/226] [update] script header. --- internal/engine/engine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index a953c193..0fd9472b 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -32,6 +32,6 @@ func (e *Engine) ExecuteScenario(scenario *Scenario) error { fmt.Println(titleStyle.Render(scenario.Name)) ExecuteAndRenderSteps(scenario.Steps, scenario.Environment, e.Configuration.Verbose) shells.ResetStoredEnvironmentVariables() - fmt.Printf(scriptHeader.Render("# Generated bash replicate the deployment:")+"\n%s", scriptText.Render(scenario.ToShellScript())) + fmt.Printf(scriptHeader.Render("# Generated bash replicate what just happened:")+"\n%s", scriptText.Render(scenario.ToShellScript())) return nil } From 676a644a5c64db39367ff34583274d8e8a3ff890 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 23 May 2023 16:03:42 -0700 Subject: [PATCH 052/226] [add] tracking ID UUID implementation + flag & implement do-not-delete for preserving resources from scenarios with deletion commands at the end. --- cmd/ie/commands/execute.go | 8 +++++- internal/engine/engine.go | 7 ++++-- internal/engine/execution.go | 47 +++++++++++++++++++++++++++++++++--- 3 files changed, 56 insertions(+), 6 deletions(-) diff --git a/cmd/ie/commands/execute.go b/cmd/ie/commands/execute.go index 0aa345c7..782385da 100644 --- a/cmd/ie/commands/execute.go +++ b/cmd/ie/commands/execute.go @@ -9,6 +9,8 @@ import ( func init() { rootCommand.AddCommand(executeCommand) rootCommand.PersistentFlags().Bool("verbose", false, "Enable verbose logging & standard output.") + rootCommand.PersistentFlags().Bool("tracking", false, "Enable tracking for Azure resources created by the Azure CLI commands executed.") + rootCommand.PersistentFlags().Bool("do-not-delete", false, "Do not delete the Azure resources created by the Azure CLI commands executed.") } var executeCommand = &cobra.Command{ @@ -23,9 +25,13 @@ var executeCommand = &cobra.Command{ } verbose, _ := cmd.Flags().GetBool("verbose") + tracking, _ := cmd.Flags().GetBool("tracking") + do_not_delete, _ := cmd.Flags().GetBool("do-not-delete") innovationEngine := engine.NewEngine(engine.EngineConfiguration{ - Verbose: verbose, + Verbose: verbose, + ResourceTracking: tracking, + DoNotDelete: do_not_delete, }) scenario, err := engine.CreateScenarioFromMarkdown(markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}) if err != nil { diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 0fd9472b..e6ef53bf 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/Azure/InnovationEngine/internal/shells" + "github.com/Azure/InnovationEngine/internal/utils" "github.com/charmbracelet/lipgloss" ) @@ -13,7 +14,9 @@ var ( ) type EngineConfiguration struct { - Verbose bool + Verbose bool + ResourceTracking bool + DoNotDelete bool } type Engine struct { @@ -30,7 +33,7 @@ func NewEngine(configuration EngineConfiguration) *Engine { // / Executes a scenario. func (e *Engine) ExecuteScenario(scenario *Scenario) error { fmt.Println(titleStyle.Render(scenario.Name)) - ExecuteAndRenderSteps(scenario.Steps, scenario.Environment, e.Configuration.Verbose) + e.ExecuteAndRenderSteps(scenario.Steps, utils.CopyMap(scenario.Environment)) shells.ResetStoredEnvironmentVariables() fmt.Printf(scriptHeader.Render("# Generated bash replicate what just happened:")+"\n%s", scriptText.Render(scenario.ToShellScript())) return nil diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 7689f98c..50fafe4c 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -2,6 +2,7 @@ package engine import ( "fmt" + "regexp" "strings" "time" @@ -27,6 +28,8 @@ var ( titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Align(lipgloss.Center).Bold(true) ) +var az_group_delete = regexp.MustCompile(`az group delete`) + // Indents a multi-line command to be nested under the first line of the // command. func indentMultiLineCommand(content string, indentation int) string { @@ -43,8 +46,46 @@ func indentMultiLineCommand(content string, indentation int) string { } // Executes the steps from a scenario and renders the output to the terminal. -func ExecuteAndRenderSteps(steps []Step, env map[string]string, verbose bool) { - for stepNumber, step := range steps { +func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { + + // Enable resource tracking for all of the deployments performed by the + // innovation engine. + if e.Configuration.ResourceTracking { + tracking_id := "6edbe7b9-4e03-4ab0-8213-230ba21aeaba" + env["AZURE_HTTP_USER_AGENT"] = fmt.Sprintf("pid-%s", tracking_id) + if e.Configuration.Verbose { + fmt.Println("Resource tracking enabled. Tracking ID: " + env["AZURE_HTTP_USER_AGENT"]) + } + } + + // If a scenario has an `az group delete` command and the `--do-not-delete` + // flag is set, we remove it from the steps. + + stepsToExecute := []Step{} + if e.Configuration.DoNotDelete { + for _, step := range steps { + newBlocks := []parsers.CodeBlock{} + for _, block := range step.CodeBlocks { + if az_group_delete.MatchString(block.Content) { + if e.Configuration.Verbose { + fmt.Printf("Found az group delete command within the step: %s\n", step.Name) + } + } else { + newBlocks = append(newBlocks, block) + } + } + if len(newBlocks) > 0 { + stepsToExecute = append(stepsToExecute, Step{ + Name: step.Name, + CodeBlocks: newBlocks, + }) + } + } + } else { + stepsToExecute = steps + } + + for stepNumber, step := range stepsToExecute { fmt.Printf("%d. %s\n", stepNumber+1, step.Name) for _, block := range step.CodeBlocks { // Render the codeblock. @@ -84,7 +125,7 @@ func ExecuteAndRenderSteps(steps []Step, env map[string]string, verbose bool) { if err == nil { fmt.Printf("\r %s \n", checkStyle.Render("✔")) fmt.Printf("\033[%dB\n", lines) - if verbose { + if e.Configuration.Verbose { fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Render(commandOutput)) } } else { From a5e7854d2524d66c9975c5976bd93f4a8e8282d6 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sun, 4 Jun 2023 13:53:11 -0700 Subject: [PATCH 053/226] [update] the markdown parser to start capturing the expected output blocks as they're parsed. --- internal/engine/scenario.go | 3 +++ internal/engine/validation.go | 4 +++ internal/parsers/markdown.go | 47 ++++++++++++++++++++++++++++++++--- 3 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 internal/engine/validation.go diff --git a/internal/engine/scenario.go b/internal/engine/scenario.go index 6e673e50..df1f163f 100644 --- a/internal/engine/scenario.go +++ b/internal/engine/scenario.go @@ -84,6 +84,9 @@ func CreateScenarioFromMarkdown(path string, languagesToExecute []string) (*Scen } codeBlocks := parsers.ExtractCodeBlocksFromAst(markdown, source, languagesToExecute) + fmt.Println(codeBlocks) + os.Exit(0) + steps := groupCodeBlocksIntoSteps(codeBlocks) title, err := parsers.ExtractScenarioTitleFromAst(markdown, source) if err != nil { diff --git a/internal/engine/validation.go b/internal/engine/validation.go new file mode 100644 index 00000000..7516fb77 --- /dev/null +++ b/internal/engine/validation.go @@ -0,0 +1,4 @@ +package engine + +func (e *Engine) ValidateScenario() { +} diff --git a/internal/parsers/markdown.go b/internal/parsers/markdown.go index a1162f79..0f73678f 100644 --- a/internal/parsers/markdown.go +++ b/internal/parsers/markdown.go @@ -3,6 +3,7 @@ package parsers import ( "fmt" "regexp" + "strconv" "strings" "github.com/yuin/goldmark" @@ -30,10 +31,17 @@ func ParseMarkdownIntoAst(source []byte) ast.Node { return document } +type ExpectedOutputBlock struct { + Language string + Content string + ExpectedSimilarity float64 +} + type CodeBlock struct { - Language string - Content string - Header string + Language string + Content string + Header string + ExpectedOutput ExpectedOutputBlock } // Assumes the title of the scenario is the first h1 header in the @@ -60,11 +68,16 @@ func ExtractScenarioTitleFromAst(node ast.Node, source []byte) (string, error) { return header, nil } +var expectedSimilarityRegex = regexp.MustCompile(``) + // Extracts the code blocks from a provided markdown AST that match the // languagesToExtract. func ExtractCodeBlocksFromAst(node ast.Node, source []byte, languagesToExtract []string) []CodeBlock { var lastHeader string var commands []CodeBlock + var nextBlockIsExpectedOutput bool + var lastExpectedSimilarityScore float64 + ast.Walk(node, func(node ast.Node, entering bool) (ast.WalkStatus, error) { if entering { switch n := node.(type) { @@ -72,6 +85,22 @@ func ExtractCodeBlocksFromAst(node ast.Node, source []byte, languagesToExtract [ case *ast.Heading: lastHeader = string(extractTextFromMarkdown(&n.BaseBlock, source)) // Extract the code block if it matches the language. + case *ast.HTMLBlock: + content := extractTextFromMarkdown(&n.BaseBlock, source) + match := expectedSimilarityRegex.FindStringSubmatch(content) + + // TODO(vmarcella): Add better error handling for when the + // score isn't parsable as a float. + if match != nil { + score, err := strconv.ParseFloat(match[1], 64) + fmt.Println("score", score) + if err != nil { + return ast.WalkStop, err + } + lastExpectedSimilarityScore = score + nextBlockIsExpectedOutput = true + } + case *ast.FencedCodeBlock: language := string(n.Language((source))) for _, desiredLanguage := range languagesToExtract { @@ -82,6 +111,18 @@ func ExtractCodeBlocksFromAst(node ast.Node, source []byte, languagesToExtract [ Header: lastHeader, } commands = append(commands, command) + break + } else if nextBlockIsExpectedOutput { + if len(commands) > 0 { + expectedOutputBlock := ExpectedOutputBlock{ + Language: language, + Content: extractTextFromMarkdown(&n.BaseBlock, source), + ExpectedSimilarity: lastExpectedSimilarityScore, + } + commands[len(commands)-1].ExpectedOutput = expectedOutputBlock + nextBlockIsExpectedOutput = false + } + break } } } From f5cd7efad34ba7e172f0528ef7390a4373939341 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sun, 4 Jun 2023 13:59:26 -0700 Subject: [PATCH 054/226] [refactor] some names & add comments. --- internal/engine/execution.go | 4 ++-- internal/parsers/markdown.go | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 50fafe4c..d06d62aa 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -28,7 +28,7 @@ var ( titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Align(lipgloss.Center).Bold(true) ) -var az_group_delete = regexp.MustCompile(`az group delete`) +var azGroupDelete = regexp.MustCompile(`az group delete`) // Indents a multi-line command to be nested under the first line of the // command. @@ -66,7 +66,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { for _, step := range steps { newBlocks := []parsers.CodeBlock{} for _, block := range step.CodeBlocks { - if az_group_delete.MatchString(block.Content) { + if azGroupDelete.MatchString(block.Content) { if e.Configuration.Verbose { fmt.Printf("Found az group delete command within the step: %s\n", step.Name) } diff --git a/internal/parsers/markdown.go b/internal/parsers/markdown.go index 0f73678f..b630773b 100644 --- a/internal/parsers/markdown.go +++ b/internal/parsers/markdown.go @@ -31,12 +31,16 @@ func ParseMarkdownIntoAst(source []byte) ast.Node { return document } +// The representation of an expected output block in a markdown file. This is +// for scenarios that have expected output that should be validated against the +// actual output. type ExpectedOutputBlock struct { Language string Content string ExpectedSimilarity float64 } +// The representation of a code block in a markdown file. type CodeBlock struct { Language string Content string @@ -113,6 +117,8 @@ func ExtractCodeBlocksFromAst(node ast.Node, source []byte, languagesToExtract [ commands = append(commands, command) break } else if nextBlockIsExpectedOutput { + // Map the expected output to the last command. If there + // are no commands, then we ignore the expected output. if len(commands) > 0 { expectedOutputBlock := ExpectedOutputBlock{ Language: language, @@ -120,7 +126,10 @@ func ExtractCodeBlocksFromAst(node ast.Node, source []byte, languagesToExtract [ ExpectedSimilarity: lastExpectedSimilarityScore, } commands[len(commands)-1].ExpectedOutput = expectedOutputBlock + + // Reset the expected output state. nextBlockIsExpectedOutput = false + lastExpectedSimilarityScore = 0 } break } From 28612a4dab5321275ff43a8e5ea7178f674a9da4 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sun, 4 Jun 2023 14:01:24 -0700 Subject: [PATCH 055/226] [remove] test exit statement. --- internal/engine/scenario.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/engine/scenario.go b/internal/engine/scenario.go index df1f163f..a3815dfc 100644 --- a/internal/engine/scenario.go +++ b/internal/engine/scenario.go @@ -85,7 +85,6 @@ func CreateScenarioFromMarkdown(path string, languagesToExecute []string) (*Scen codeBlocks := parsers.ExtractCodeBlocksFromAst(markdown, source, languagesToExecute) fmt.Println(codeBlocks) - os.Exit(0) steps := groupCodeBlocksIntoSteps(codeBlocks) title, err := parsers.ExtractScenarioTitleFromAst(markdown, source) From 266a67ccc4ba72a99fe9fbe2874c8278e2e9bff7 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sun, 4 Jun 2023 15:35:56 -0700 Subject: [PATCH 056/226] [add] import dependencies for testing, setup initial testing code, and fix flags. --- cmd/ie/commands/execute.go | 7 ++- cmd/ie/commands/test.go | 34 ++++++++-- demoScripts/createVMEnvVars.md | 2 +- go.mod | 2 + go.sum | 4 ++ internal/engine/engine.go | 13 ++-- internal/engine/execution.go | 1 + internal/engine/testing.go | 110 +++++++++++++++++++++++++++++++++ internal/engine/validation.go | 4 -- internal/utils/diff.go | 14 +++++ internal/utils/json.go | 19 ++++++ 11 files changed, 194 insertions(+), 16 deletions(-) create mode 100644 internal/engine/testing.go delete mode 100644 internal/engine/validation.go create mode 100644 internal/utils/diff.go create mode 100644 internal/utils/json.go diff --git a/cmd/ie/commands/execute.go b/cmd/ie/commands/execute.go index 782385da..2a64389a 100644 --- a/cmd/ie/commands/execute.go +++ b/cmd/ie/commands/execute.go @@ -8,9 +8,10 @@ import ( // / Register the command with our command runner. func init() { rootCommand.AddCommand(executeCommand) - rootCommand.PersistentFlags().Bool("verbose", false, "Enable verbose logging & standard output.") - rootCommand.PersistentFlags().Bool("tracking", false, "Enable tracking for Azure resources created by the Azure CLI commands executed.") - rootCommand.PersistentFlags().Bool("do-not-delete", false, "Do not delete the Azure resources created by the Azure CLI commands executed.") + + executeCommand.PersistentFlags().Bool("verbose", false, "Enable verbose logging & standard output.") + executeCommand.PersistentFlags().Bool("tracking", false, "Enable tracking for Azure resources created by the Azure CLI commands executed.") + executeCommand.PersistentFlags().Bool("do-not-delete", false, "Do not delete the Azure resources created by the Azure CLI commands executed.") } var executeCommand = &cobra.Command{ diff --git a/cmd/ie/commands/test.go b/cmd/ie/commands/test.go index 4128e16d..d7ee52ea 100644 --- a/cmd/ie/commands/test.go +++ b/cmd/ie/commands/test.go @@ -1,15 +1,41 @@ package commands import ( + "github.com/Azure/InnovationEngine/internal/engine" "github.com/spf13/cobra" ) +// / Register the command with our command runner. +func init() { + rootCommand.AddCommand(testCommand) + testCommand.PersistentFlags().Bool("verbose", false, "Enable verbose logging & standard output.") +} + var testCommand = &cobra.Command{ Use: "test", + Args: cobra.MinimumNArgs(1), Short: "Test document commands against it's expected outputs.", -} + Run: func(cmd *cobra.Command, args []string) { + markdownFile := args[0] + if markdownFile == "" { + cmd.Help() + return + } -// / Register the command with our command runner. -func init() { - rootCommand.AddCommand(testCommand) + verbose, _ := cmd.Flags().GetBool("verbose") + + innovationEngine := engine.NewEngine(engine.EngineConfiguration{ + Verbose: verbose, + ResourceTracking: false, + DoNotDelete: false, + }) + + scenario, err := engine.CreateScenarioFromMarkdown(markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}) + if err != nil { + panic(err) + } + + innovationEngine.TestScenario(scenario) + + }, } diff --git a/demoScripts/createVMEnvVars.md b/demoScripts/createVMEnvVars.md index 2f5ce350..d3646ccf 100644 --- a/demoScripts/createVMEnvVars.md +++ b/demoScripts/createVMEnvVars.md @@ -46,7 +46,7 @@ az vm create \ It takes a few minutes to create the VM and supporting resources. The following example output shows the VM create operation was successful. -```Output +```json { "fqdns": "", "id": "/subscriptions//resourceGroups/myResourceGroup/providers/Microsoft.Compute/virtualMachines/myVM", diff --git a/go.mod b/go.mod index 89e155d2..a2faac99 100644 --- a/go.mod +++ b/go.mod @@ -41,10 +41,12 @@ require ( github.com/muesli/termenv v0.15.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/rivo/uniseg v0.4.4 // indirect + github.com/sergi/go-diff v1.3.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.8.2 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect golang.org/x/crypto v0.6.0 // indirect golang.org/x/net v0.8.0 // indirect golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect diff --git a/go.sum b/go.sum index 6f78f89f..463e20d8 100644 --- a/go.sum +++ b/go.sum @@ -189,6 +189,8 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= +github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -211,6 +213,8 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC github.com/valyala/fasttemplate v1.2.1/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= +github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/internal/engine/engine.go b/internal/engine/engine.go index e6ef53bf..5bfd139f 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -3,7 +3,6 @@ package engine import ( "fmt" - "github.com/Azure/InnovationEngine/internal/shells" "github.com/Azure/InnovationEngine/internal/utils" "github.com/charmbracelet/lipgloss" ) @@ -30,11 +29,17 @@ func NewEngine(configuration EngineConfiguration) *Engine { } } -// / Executes a scenario. +// Executes a deployment scenario. func (e *Engine) ExecuteScenario(scenario *Scenario) error { fmt.Println(titleStyle.Render(scenario.Name)) e.ExecuteAndRenderSteps(scenario.Steps, utils.CopyMap(scenario.Environment)) - shells.ResetStoredEnvironmentVariables() - fmt.Printf(scriptHeader.Render("# Generated bash replicate what just happened:")+"\n%s", scriptText.Render(scenario.ToShellScript())) + fmt.Printf(scriptHeader.Render("# Generated bash to replicate what just happened:")+"\n%s", scriptText.Render(scenario.ToShellScript())) + return nil +} + +// Validates a deployment scenario. +func (e *Engine) TestScenario(scenario *Scenario) error { + fmt.Println(titleStyle.Render(scenario.Name)) + e.TestSteps(scenario.Steps, utils.CopyMap(scenario.Environment)) return nil } diff --git a/internal/engine/execution.go b/internal/engine/execution.go index d06d62aa..b6401b33 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -143,4 +143,5 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { } } } + shells.ResetStoredEnvironmentVariables() } diff --git a/internal/engine/testing.go b/internal/engine/testing.go new file mode 100644 index 00000000..887e786e --- /dev/null +++ b/internal/engine/testing.go @@ -0,0 +1,110 @@ +package engine + +import ( + "fmt" + "strings" + "time" + + "github.com/Azure/InnovationEngine/internal/parsers" + "github.com/Azure/InnovationEngine/internal/shells" + "github.com/Azure/InnovationEngine/internal/utils" + "github.com/charmbracelet/lipgloss" + "github.com/xrash/smetrics" +) + +func (e *Engine) TestSteps(steps []Step, env map[string]string) { + for stepNumber, step := range steps { + fmt.Printf("%d. %s\n", stepNumber+1, step.Name) + for _, block := range step.CodeBlocks { + // Render the codeblock. + indentedBlock := indentMultiLineCommand(block.Content, 4) + fmt.Print(" " + indentedBlock) + + // Grab the number of lines it contains & set the cursor to the + // beginning of the block. + lines := strings.Count(block.Content, "\n") + fmt.Printf("\033[%dA", lines) + + // Render the spinner and hide the cursor. + fmt.Print(spinnerStyle.Render(" "+string(spinnerFrames[0])) + " ") + fmt.Print("\033[?25l") + + // execute the command as a goroutine to allow for the spinner to be + // rendered while the command is executing. + done := make(chan error) + var commandOutput string + go func(block parsers.CodeBlock) { + output, err := shells.ExecuteBashCommand(block.Content, utils.CopyMap(env), true) + commandOutput = output + done <- err + }(block) + + frame := 0 + var err error + + loop: + // While the command is executing, render the spinner. + for { + select { + case err = <-done: + // Show the cursor, check the result of the command, and display the + // final status. + fmt.Print("\033[?25h") + if err == nil { + if block.ExpectedOutput.Language == "json" { + actualOutput, err := utils.OrderJsonFields(commandOutput) + if err != nil { + fmt.Printf("\r %s \n", errorStyle.Render("✗")) + fmt.Printf("\033[%dB", lines) + fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render(err.Error())) + break loop + } + + expectedOutput, err := utils.OrderJsonFields(block.ExpectedOutput.Content) + if err != nil { + fmt.Printf("\r %s \n", errorStyle.Render("✗")) + fmt.Printf("\033[%dB", lines) + fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render(err.Error())) + break loop + } + + score := smetrics.JaroWinkler(expectedOutput, actualOutput, 0.7, 4) + + if block.ExpectedOutput.ExpectedSimilarity > score { + fmt.Printf("\r %s \n", errorStyle.Render("✗")) + fmt.Printf("\033[%dB", lines) + fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render("Expected output does not match actual output.")) + fmt.Printf(" %s\n", utils.GetDifferenceBetweenStrings(expectedOutput, actualOutput)) + } + } else { + score := smetrics.JaroWinkler(block.ExpectedOutput.Content, commandOutput, 0.7, 4) + if block.ExpectedOutput.ExpectedSimilarity > score { + fmt.Printf("\r %s \n", errorStyle.Render("✗")) + fmt.Printf("\033[%dB", lines) + fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render("Expected output does not match actual output.")) + fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render(utils.GetDifferenceBetweenStrings(block.ExpectedOutput.Content, commandOutput))) + } + } + + fmt.Printf("\r %s \n", checkStyle.Render("✔")) + fmt.Printf("\033[%dB\n", lines) + if e.Configuration.Verbose { + fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Render(commandOutput)) + } + } else { + fmt.Printf("\r %s \n", errorStyle.Render("✗")) + fmt.Printf("\033[%dB", lines) + fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render(err.Error())) + } + + break loop + default: + frame = (frame + 1) % len(spinnerFrames) + fmt.Printf("\r %s", spinnerStyle.Render(string(spinnerFrames[frame]))) + time.Sleep(spinnerRefresh) + } + } + } + } + shells.ResetStoredEnvironmentVariables() +} diff --git a/internal/engine/validation.go b/internal/engine/validation.go deleted file mode 100644 index 7516fb77..00000000 --- a/internal/engine/validation.go +++ /dev/null @@ -1,4 +0,0 @@ -package engine - -func (e *Engine) ValidateScenario() { -} diff --git a/internal/utils/diff.go b/internal/utils/diff.go new file mode 100644 index 00000000..4bf9ddfd --- /dev/null +++ b/internal/utils/diff.go @@ -0,0 +1,14 @@ +package utils + +import ( + "fmt" + + "github.com/sergi/go-diff/diffmatchpatch" +) + +func GetDifferenceBetweenStrings(a, b string) string { + dmp := diffmatchpatch.New() + + diffs := dmp.DiffMain(a, b, false) + return fmt.Sprintf("%s", dmp.DiffPrettyText(diffs)) +} diff --git a/internal/utils/json.go b/internal/utils/json.go new file mode 100644 index 00000000..62099d5a --- /dev/null +++ b/internal/utils/json.go @@ -0,0 +1,19 @@ +package utils + +import ( + "encoding/json" +) + +func OrderJsonFields(jsonStr string) (string, error) { + expectedMap := make(map[string]interface{}) + err := json.Unmarshal([]byte(jsonStr), &expectedMap) + if err != nil { + return "", err + } + + orderedJson, err := json.Marshal(expectedMap) + if err != nil { + return "", err + } + return string(orderedJson), nil +} From 8123d25c46f06ad3b63357ecbf0381f27d7421c6 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sun, 11 Jun 2023 14:25:00 -0700 Subject: [PATCH 057/226] [add] common engine functionality to one place & separate std out and std err. --- demoScripts/createVMEnvVars.md | 1 + internal/engine/common.go | 30 ++++++++++++++ internal/engine/execution.go | 74 ++++++++++++---------------------- internal/engine/testing.go | 14 ++++--- internal/shells/bash.go | 24 +++++++++-- 5 files changed, 86 insertions(+), 57 deletions(-) create mode 100644 internal/engine/common.go diff --git a/demoScripts/createVMEnvVars.md b/demoScripts/createVMEnvVars.md index d3646ccf..2f3faa6d 100644 --- a/demoScripts/createVMEnvVars.md +++ b/demoScripts/createVMEnvVars.md @@ -41,6 +41,7 @@ az vm create \ --name $MY_VM_NAME \ --image $MY_VM_IMAGE \ --admin-username $MY_ADMIN_USERNAME \ + --public-ip-sku Standard \ --generate-ssh-keys ``` diff --git a/internal/engine/common.go b/internal/engine/common.go new file mode 100644 index 00000000..c59ad704 --- /dev/null +++ b/internal/engine/common.go @@ -0,0 +1,30 @@ +package engine + +import ( + "strings" + + "github.com/charmbracelet/lipgloss" +) + +// Styles used for rendering output to the terminal. +var ( + spinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")) + checkStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#32CD32")) + errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000")) + titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Align(lipgloss.Center).Bold(true) +) + +// Indents a multi-line command to be nested under the first line of the +// command. +func indentMultiLineCommand(content string, indentation int) string { + lines := strings.Split(content, "\n") + for i := 1; i < len(lines); i++ { + if strings.HasSuffix(strings.TrimSpace(lines[i-1]), "\\") { + lines[i] = strings.Repeat(" ", indentation) + lines[i] + } else if strings.TrimSpace(lines[i]) != "" { + lines[i] = strings.Repeat(" ", indentation) + lines[i] + } + + } + return strings.Join(lines, "\n") +} diff --git a/internal/engine/execution.go b/internal/engine/execution.go index b6401b33..9d6f6a07 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -20,29 +20,33 @@ const ( spinnerRefresh = 100 * time.Millisecond ) -// Styles used for rendering output to the terminal. -var ( - spinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")) - checkStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#32CD32")) - errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000")) - titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Align(lipgloss.Center).Bold(true) -) - var azGroupDelete = regexp.MustCompile(`az group delete`) -// Indents a multi-line command to be nested under the first line of the -// command. -func indentMultiLineCommand(content string, indentation int) string { - lines := strings.Split(content, "\n") - for i := 1; i < len(lines); i++ { - if strings.HasSuffix(strings.TrimSpace(lines[i-1]), "\\") { - lines[i] = strings.Repeat(" ", indentation) + lines[i] - } else if strings.TrimSpace(lines[i]) != "" { - lines[i] = strings.Repeat(" ", indentation) + lines[i] +// If a scenario has an `az group delete` command and the `--preserve-resources` +// flag is set, we remove it from the steps. +func filterDeletionCommands(steps []Step, preserveResources bool) []Step { + filteredSteps := []Step{} + if preserveResources { + for _, step := range steps { + newBlocks := []parsers.CodeBlock{} + for _, block := range step.CodeBlocks { + if azGroupDelete.MatchString(block.Content) { + continue + } else { + newBlocks = append(newBlocks, block) + } + } + if len(newBlocks) > -1 { + filteredSteps = append(filteredSteps, Step{ + Name: step.Name, + CodeBlocks: newBlocks, + }) + } } - + } else { + filteredSteps = steps } - return strings.Join(lines, "\n") + return filteredSteps } // Executes the steps from a scenario and renders the output to the terminal. @@ -58,33 +62,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { } } - // If a scenario has an `az group delete` command and the `--do-not-delete` - // flag is set, we remove it from the steps. - - stepsToExecute := []Step{} - if e.Configuration.DoNotDelete { - for _, step := range steps { - newBlocks := []parsers.CodeBlock{} - for _, block := range step.CodeBlocks { - if azGroupDelete.MatchString(block.Content) { - if e.Configuration.Verbose { - fmt.Printf("Found az group delete command within the step: %s\n", step.Name) - } - } else { - newBlocks = append(newBlocks, block) - } - } - if len(newBlocks) > 0 { - stepsToExecute = append(stepsToExecute, Step{ - Name: step.Name, - CodeBlocks: newBlocks, - }) - } - } - } else { - stepsToExecute = steps - } - + stepsToExecute := filterDeletionCommands(steps, e.Configuration.DoNotDelete) for stepNumber, step := range stepsToExecute { fmt.Printf("%d. %s\n", stepNumber+1, step.Name) for _, block := range step.CodeBlocks { @@ -104,7 +82,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { // execute the command as a goroutine to allow for the spinner to be // rendered while the command is executing. done := make(chan error) - var commandOutput string + var commandOutput shells.CommandOutput go func(block parsers.CodeBlock) { output, err := shells.ExecuteBashCommand(block.Content, utils.CopyMap(env), true) commandOutput = output @@ -126,7 +104,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { fmt.Printf("\r %s \n", checkStyle.Render("✔")) fmt.Printf("\033[%dB\n", lines) if e.Configuration.Verbose { - fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Render(commandOutput)) + fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Render(commandOutput.StdOut)) } } else { fmt.Printf("\r %s \n", errorStyle.Render("✗")) diff --git a/internal/engine/testing.go b/internal/engine/testing.go index 887e786e..d31f47b0 100644 --- a/internal/engine/testing.go +++ b/internal/engine/testing.go @@ -32,7 +32,7 @@ func (e *Engine) TestSteps(steps []Step, env map[string]string) { // execute the command as a goroutine to allow for the spinner to be // rendered while the command is executing. done := make(chan error) - var commandOutput string + var commandOutput shells.CommandOutput go func(block parsers.CodeBlock) { output, err := shells.ExecuteBashCommand(block.Content, utils.CopyMap(env), true) commandOutput = output @@ -52,7 +52,7 @@ func (e *Engine) TestSteps(steps []Step, env map[string]string) { fmt.Print("\033[?25h") if err == nil { if block.ExpectedOutput.Language == "json" { - actualOutput, err := utils.OrderJsonFields(commandOutput) + actualOutput, err := utils.OrderJsonFields(commandOutput.StdOut) if err != nil { fmt.Printf("\r %s \n", errorStyle.Render("✗")) fmt.Printf("\033[%dB", lines) @@ -76,20 +76,24 @@ func (e *Engine) TestSteps(steps []Step, env map[string]string) { fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render("Expected output does not match actual output.")) fmt.Printf(" %s\n", utils.GetDifferenceBetweenStrings(expectedOutput, actualOutput)) } + + if e.Configuration.Verbose { + fmt.Printf("Score %f threshold: %f\n", score, block.ExpectedOutput.ExpectedSimilarity) + } } else { - score := smetrics.JaroWinkler(block.ExpectedOutput.Content, commandOutput, 0.7, 4) + score := smetrics.JaroWinkler(block.ExpectedOutput.Content, commandOutput.StdOut, 0.7, 4) if block.ExpectedOutput.ExpectedSimilarity > score { fmt.Printf("\r %s \n", errorStyle.Render("✗")) fmt.Printf("\033[%dB", lines) fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render("Expected output does not match actual output.")) - fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render(utils.GetDifferenceBetweenStrings(block.ExpectedOutput.Content, commandOutput))) + fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render(utils.GetDifferenceBetweenStrings(block.ExpectedOutput.Content, commandOutput.StdOut))) } } fmt.Printf("\r %s \n", checkStyle.Render("✔")) fmt.Printf("\033[%dB\n", lines) if e.Configuration.Verbose { - fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Render(commandOutput)) + fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Render(commandOutput.StdOut)) } } else { fmt.Printf("\r %s \n", errorStyle.Render("✗")) diff --git a/internal/shells/bash.go b/internal/shells/bash.go index f4df54f3..7907b9d2 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -2,6 +2,7 @@ package shells import ( "bufio" + "bytes" "fmt" "os" "os/exec" @@ -44,14 +45,24 @@ func ResetStoredEnvironmentVariables() error { return os.Remove(environmentStateFile) } +type CommandOutput struct { + StdOut string + StdErr string +} + // Executes a bash command and returns the output or error. -func ExecuteBashCommand(command string, env map[string]string, inherit_environment_variables bool) (string, error) { +func ExecuteBashCommand(command string, env map[string]string, inherit_environment_variables bool) (CommandOutput, error) { var commandWithStateSaved = []string{ command, "env > /tmp/env.txt", } commandToExecute := exec.Command("bash", "-c", strings.Join(commandWithStateSaved, "\n")) + // Capture std out and std err as separate buffers. + var stdout, stderr bytes.Buffer + commandToExecute.Stdout = &stdout + commandToExecute.Stderr = &stderr + if inherit_environment_variables { commandToExecute.Env = os.Environ() } @@ -73,10 +84,15 @@ func ExecuteBashCommand(command string, env map[string]string, inherit_environme } } - stdOutAndErr, err := commandToExecute.CombinedOutput() + // Execute command, handle errors, and return output. + err = commandToExecute.Run() + standardOutput, standardError := stdout.String(), stderr.String() if err != nil { - return "", fmt.Errorf("command exited with '%w' and the message '%s'", err, stdOutAndErr) + return CommandOutput{}, fmt.Errorf("command exited with '%w' and the message '%s'", err, standardError) } - return string(stdOutAndErr), nil + return CommandOutput{ + StdOut: standardOutput, + StdErr: standardError, + }, nil } From fe3cf39a94349518664fdd46690c4a1f103c2c7b Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sun, 11 Jun 2023 17:00:08 -0700 Subject: [PATCH 058/226] [refactor] comparing json fields to better handle testing. --- demoScripts/createVMEnvVars.md | 2 +- internal/engine/testing.go | 21 ++++++++------------- internal/utils/json.go | 30 ++++++++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 14 deletions(-) diff --git a/demoScripts/createVMEnvVars.md b/demoScripts/createVMEnvVars.md index 2f3faa6d..876a554d 100644 --- a/demoScripts/createVMEnvVars.md +++ b/demoScripts/createVMEnvVars.md @@ -46,7 +46,7 @@ az vm create \ ``` It takes a few minutes to create the VM and supporting resources. The following example output shows the VM create operation was successful. - + ```json { "fqdns": "", diff --git a/internal/engine/testing.go b/internal/engine/testing.go index d31f47b0..13844c68 100644 --- a/internal/engine/testing.go +++ b/internal/engine/testing.go @@ -51,16 +51,12 @@ func (e *Engine) TestSteps(steps []Step, env map[string]string) { // final status. fmt.Print("\033[?25h") if err == nil { - if block.ExpectedOutput.Language == "json" { - actualOutput, err := utils.OrderJsonFields(commandOutput.StdOut) - if err != nil { - fmt.Printf("\r %s \n", errorStyle.Render("✗")) - fmt.Printf("\033[%dB", lines) - fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render(err.Error())) - break loop - } + actualOutput := commandOutput.StdOut + expectedOutput := block.ExpectedOutput.Content + expectedSimilarity := block.ExpectedOutput.ExpectedSimilarity - expectedOutput, err := utils.OrderJsonFields(block.ExpectedOutput.Content) + if block.ExpectedOutput.Language == "json" { + meetsThreshold, err := utils.CompareJsonStrings(actualOutput, expectedOutput, expectedSimilarity) if err != nil { fmt.Printf("\r %s \n", errorStyle.Render("✗")) fmt.Printf("\033[%dB", lines) @@ -68,9 +64,7 @@ func (e *Engine) TestSteps(steps []Step, env map[string]string) { break loop } - score := smetrics.JaroWinkler(expectedOutput, actualOutput, 0.7, 4) - - if block.ExpectedOutput.ExpectedSimilarity > score { + if !meetsThreshold { fmt.Printf("\r %s \n", errorStyle.Render("✗")) fmt.Printf("\033[%dB", lines) fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render("Expected output does not match actual output.")) @@ -78,7 +72,8 @@ func (e *Engine) TestSteps(steps []Step, env map[string]string) { } if e.Configuration.Verbose { - fmt.Printf("Score %f threshold: %f\n", score, block.ExpectedOutput.ExpectedSimilarity) + score, _ := utils.ComputeJaroWinklerScore(actualOutput, expectedOutput) + fmt.Printf("Score %f threshold: %f\n", score, expectedSimilarity) } } else { score := smetrics.JaroWinkler(block.ExpectedOutput.Content, commandOutput.StdOut, 0.7, 4) diff --git a/internal/utils/json.go b/internal/utils/json.go index 62099d5a..2da5068e 100644 --- a/internal/utils/json.go +++ b/internal/utils/json.go @@ -2,6 +2,8 @@ package utils import ( "encoding/json" + + "github.com/xrash/smetrics" ) func OrderJsonFields(jsonStr string) (string, error) { @@ -17,3 +19,31 @@ func OrderJsonFields(jsonStr string) (string, error) { } return string(orderedJson), nil } + +// Compute the Jaro-Winkler score for two JSON strings. The score is computed +// by ordering the fields alphabetically and then comparing the strings using +// the Jaro-Winkler algorithm. +func ComputeJaroWinklerScore(actualJson string, expectedJson string) (float64, error) { + actualOutput, err := OrderJsonFields(actualJson) + if err != nil { + return 0, err + } + + expectedOutput, err := OrderJsonFields(expectedJson) + if err != nil { + return 0, err + } + + return smetrics.JaroWinkler(expectedOutput, actualOutput, 0.7, 4), nil +} + +// Compare two JSON strings by ordering the fields alphabetically and then +// comparing the strings using the Jaro-Winkler algorithm to compute a score. +// If the score is greater than the threshold, return true. +func CompareJsonStrings(actualJson string, expectedJson string, threshold float64) (bool, error) { + score, err := ComputeJaroWinklerScore(actualJson, expectedJson) + if err != nil { + return false, err + } + return score > threshold, nil +} From 839e4be0f709a4692c95fa604b8e812254e88787 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sun, 11 Jun 2023 17:43:48 -0700 Subject: [PATCH 059/226] [add] logging. --- cmd/ie/commands/test.go | 4 ++++ go.mod | 1 + go.sum | 3 +++ internal/engine/execution.go | 2 ++ internal/engine/testing.go | 5 +++- internal/logging/logging.go | 46 ++++++++++++++++++++++++++++++++++++ 6 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 internal/logging/logging.go diff --git a/cmd/ie/commands/test.go b/cmd/ie/commands/test.go index d7ee52ea..2c4555c0 100644 --- a/cmd/ie/commands/test.go +++ b/cmd/ie/commands/test.go @@ -2,6 +2,7 @@ package commands import ( "github.com/Azure/InnovationEngine/internal/engine" + "github.com/Azure/InnovationEngine/internal/logging" "github.com/spf13/cobra" ) @@ -16,6 +17,9 @@ var testCommand = &cobra.Command{ Args: cobra.MinimumNArgs(1), Short: "Test document commands against it's expected outputs.", Run: func(cmd *cobra.Command, args []string) { + // TODO(vmarcella): Initialize this via a flag. + logging.Init("info") + markdownFile := args[0] if markdownFile == "" { cmd.Help() diff --git a/go.mod b/go.mod index a2faac99..5eb19c5e 100644 --- a/go.mod +++ b/go.mod @@ -42,6 +42,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/sergi/go-diff v1.3.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.8.2 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect diff --git a/go.sum b/go.sum index 463e20d8..72c076b4 100644 --- a/go.sum +++ b/go.sum @@ -191,6 +191,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -340,6 +342,7 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 9d6f6a07..1ce8d9ff 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -6,6 +6,7 @@ import ( "strings" "time" + "github.com/Azure/InnovationEngine/internal/logging" "github.com/Azure/InnovationEngine/internal/parsers" "github.com/Azure/InnovationEngine/internal/shells" "github.com/Azure/InnovationEngine/internal/utils" @@ -58,6 +59,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { tracking_id := "6edbe7b9-4e03-4ab0-8213-230ba21aeaba" env["AZURE_HTTP_USER_AGENT"] = fmt.Sprintf("pid-%s", tracking_id) if e.Configuration.Verbose { + logging.Info("Resource tracking enabled. Tracking ID: " + env["AZURE_HTTP_USER_AGENT"]) fmt.Println("Resource tracking enabled. Tracking ID: " + env["AZURE_HTTP_USER_AGENT"]) } } diff --git a/internal/engine/testing.go b/internal/engine/testing.go index 13844c68..4751d8d7 100644 --- a/internal/engine/testing.go +++ b/internal/engine/testing.go @@ -5,6 +5,7 @@ import ( "strings" "time" + "github.com/Azure/InnovationEngine/internal/logging" "github.com/Azure/InnovationEngine/internal/parsers" "github.com/Azure/InnovationEngine/internal/shells" "github.com/Azure/InnovationEngine/internal/utils" @@ -69,11 +70,13 @@ func (e *Engine) TestSteps(steps []Step, env map[string]string) { fmt.Printf("\033[%dB", lines) fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render("Expected output does not match actual output.")) fmt.Printf(" %s\n", utils.GetDifferenceBetweenStrings(expectedOutput, actualOutput)) + break loop } if e.Configuration.Verbose { score, _ := utils.ComputeJaroWinklerScore(actualOutput, expectedOutput) - fmt.Printf("Score %f threshold: %f\n", score, expectedSimilarity) + logging.Debug("JaroWinkler score: %f Expected Similarity: %f", score, expectedSimilarity) + logging.Debug("Actual Output: %s", actualOutput) } } else { score := smetrics.JaroWinkler(block.ExpectedOutput.Content, commandOutput.StdOut, 0.7, 4) diff --git a/internal/logging/logging.go b/internal/logging/logging.go new file mode 100644 index 00000000..4a85897d --- /dev/null +++ b/internal/logging/logging.go @@ -0,0 +1,46 @@ +package logging + +import ( + "github.com/sirupsen/logrus" +) + +func Init(level string) { + logrus.SetFormatter(&logrus.TextFormatter{ + DisableColors: false, + FullTimestamp: true, + }) + + switch level { + case "info": + logrus.SetLevel(logrus.InfoLevel) + case "warn": + logrus.SetLevel(logrus.WarnLevel) + case "error": + logrus.SetLevel(logrus.ErrorLevel) + case "debug": + logrus.SetLevel(logrus.DebugLevel) + default: + logrus.SetLevel(logrus.InfoLevel) + } + +} + +func Info(args ...interface{}) { + logrus.Info(args...) +} + +func Warn(args ...interface{}) { + logrus.Warn(args...) +} + +func Error(args ...interface{}) { + logrus.Error(args...) +} + +func Debug(args ...interface{}) { + logrus.Debug(args...) +} + +func Fatal(args ...interface{}) { + logrus.Fatal(args...) +} From 7c1b32b6208eacfa37003f02df1af9833d0fac2a Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 19 Jun 2023 12:33:47 -0700 Subject: [PATCH 060/226] [add] ie logs to gitignore. --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 3e8e5edf..c76f479a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,7 @@ __pycache__ .vscode # Ignore all binaries. -bin/ \ No newline at end of file +bin/ + +# Ignore ie logs +ie.log From c05434de3763f222b9dc05c9d487da52c0163400 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 19 Jun 2023 12:34:54 -0700 Subject: [PATCH 061/226] [update] logging to store the low level output into a file, update log statements across the codebase. --- cmd/ie/commands/root.go | 12 +++++ cmd/ie/commands/test.go | 3 -- internal/engine/execution.go | 2 +- internal/engine/scenario.go | 11 +++-- internal/engine/testing.go | 18 ++++--- internal/logging/logging.go | 92 ++++++++++++++++++++++++------------ internal/parsers/ini.go | 5 +- internal/parsers/markdown.go | 7 +-- internal/utils/json.go | 6 +-- 9 files changed, 102 insertions(+), 54 deletions(-) diff --git a/cmd/ie/commands/root.go b/cmd/ie/commands/root.go index fff85cce..691d3ddf 100644 --- a/cmd/ie/commands/root.go +++ b/cmd/ie/commands/root.go @@ -4,16 +4,28 @@ import ( "fmt" "os" + "github.com/Azure/InnovationEngine/internal/logging" "github.com/spf13/cobra" ) +// The root command for the CLI. Currently initializes the logging for all other +// commands. var rootCommand = &cobra.Command{ Use: "ie", Short: "The innovation engine.", + PersistentPreRun: func(cmd *cobra.Command, args []string) { + logLevel, err := cmd.Flags().GetString("log-level") + if err != nil { + panic(err) + } + logging.Init(logging.LevelFromString(logLevel)) + }, } // Entrypoint into the Innovation Engine CLI. func ExecuteCLI() { + rootCommand.PersistentFlags().String("log-level", string(logging.Error), "Configure the log level") + if err := rootCommand.Execute(); err != nil { fmt.Println(err) os.Exit(1) diff --git a/cmd/ie/commands/test.go b/cmd/ie/commands/test.go index 2c4555c0..3b021e89 100644 --- a/cmd/ie/commands/test.go +++ b/cmd/ie/commands/test.go @@ -2,7 +2,6 @@ package commands import ( "github.com/Azure/InnovationEngine/internal/engine" - "github.com/Azure/InnovationEngine/internal/logging" "github.com/spf13/cobra" ) @@ -17,8 +16,6 @@ var testCommand = &cobra.Command{ Args: cobra.MinimumNArgs(1), Short: "Test document commands against it's expected outputs.", Run: func(cmd *cobra.Command, args []string) { - // TODO(vmarcella): Initialize this via a flag. - logging.Init("info") markdownFile := args[0] if markdownFile == "" { diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 1ce8d9ff..4fcad473 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -59,7 +59,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { tracking_id := "6edbe7b9-4e03-4ab0-8213-230ba21aeaba" env["AZURE_HTTP_USER_AGENT"] = fmt.Sprintf("pid-%s", tracking_id) if e.Configuration.Verbose { - logging.Info("Resource tracking enabled. Tracking ID: " + env["AZURE_HTTP_USER_AGENT"]) + logging.GlobalLogger.Info("Resource tracking enabled. Tracking ID: " + env["AZURE_HTTP_USER_AGENT"]) fmt.Println("Resource tracking enabled. Tracking ID: " + env["AZURE_HTTP_USER_AGENT"]) } } diff --git a/internal/engine/scenario.go b/internal/engine/scenario.go index a3815dfc..864709cc 100644 --- a/internal/engine/scenario.go +++ b/internal/engine/scenario.go @@ -6,6 +6,7 @@ import ( "path/filepath" "strings" + "github.com/Azure/InnovationEngine/internal/logging" "github.com/Azure/InnovationEngine/internal/parsers" "github.com/Azure/InnovationEngine/internal/utils" "github.com/yuin/goldmark/ast" @@ -67,13 +68,13 @@ func CreateScenarioFromMarkdown(path string, languagesToExecute []string) (*Scen // Check if the INI file exists & load it. if !utils.FileExists(markdownINI) { - fmt.Printf("INI file '%s' does not exist, skipping...", markdownINI) + logging.GlobalLogger.Infof("INI file '%s' does not exist, skipping...", markdownINI) } else { - fmt.Println("INI file exists. Loading: ", markdownINI) + logging.GlobalLogger.Infof("INI file '%s' exists, loading...", markdownINI) environmentVariables = parsers.ParseINIFile(markdownINI) for key, value := range environmentVariables { - fmt.Printf("Setting %s=%s\n", key, value) + logging.GlobalLogger.Debugf("Setting %s=%s\n", key, value) } } @@ -84,7 +85,7 @@ func CreateScenarioFromMarkdown(path string, languagesToExecute []string) (*Scen } codeBlocks := parsers.ExtractCodeBlocksFromAst(markdown, source, languagesToExecute) - fmt.Println(codeBlocks) + logging.GlobalLogger.WithField("CodeBlocks", codeBlocks).Debugf("Found %d code blocks", len(codeBlocks)) steps := groupCodeBlocksIntoSteps(codeBlocks) title, err := parsers.ExtractScenarioTitleFromAst(markdown, source) @@ -92,7 +93,7 @@ func CreateScenarioFromMarkdown(path string, languagesToExecute []string) (*Scen return nil, err } - fmt.Printf("Found scenario: %s\n", title) + logging.GlobalLogger.Infof("Successfully built out the scenario: %s", title) return &Scenario{ Name: title, diff --git a/internal/engine/testing.go b/internal/engine/testing.go index 4751d8d7..fee57389 100644 --- a/internal/engine/testing.go +++ b/internal/engine/testing.go @@ -57,26 +57,30 @@ func (e *Engine) TestSteps(steps []Step, env map[string]string) { expectedSimilarity := block.ExpectedOutput.ExpectedSimilarity if block.ExpectedOutput.Language == "json" { + logging.GlobalLogger.Debugf("Comparing JSON strings:\nExpected: %s\nActual%s", expectedOutput, actualOutput) meetsThreshold, err := utils.CompareJsonStrings(actualOutput, expectedOutput, expectedSimilarity) if err != nil { fmt.Printf("\r %s \n", errorStyle.Render("✗")) fmt.Printf("\033[%dB", lines) - fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render(err.Error())) + fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render(err.Error())) break loop } if !meetsThreshold { fmt.Printf("\r %s \n", errorStyle.Render("✗")) fmt.Printf("\033[%dB", lines) - fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render("Expected output does not match actual output.")) + fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render("Expected output does not match actual output.")) fmt.Printf(" %s\n", utils.GetDifferenceBetweenStrings(expectedOutput, actualOutput)) break loop } if e.Configuration.Verbose { - score, _ := utils.ComputeJaroWinklerScore(actualOutput, expectedOutput) - logging.Debug("JaroWinkler score: %f Expected Similarity: %f", score, expectedSimilarity) - logging.Debug("Actual Output: %s", actualOutput) + score, _ := utils.ComputeJsonStringSimilarity(actualOutput, expectedOutput) + + actual, _ := utils.OrderJsonFields(actualOutput) + expected, _ := utils.OrderJsonFields(expectedOutput) + + logging.GlobalLogger.WithField("actual", actual).WithField("expected", expected).Debugf("JaroWinkler score: %f Expected Similarity: %f", score, expectedSimilarity) } } else { score := smetrics.JaroWinkler(block.ExpectedOutput.Content, commandOutput.StdOut, 0.7, 4) @@ -84,14 +88,14 @@ func (e *Engine) TestSteps(steps []Step, env map[string]string) { fmt.Printf("\r %s \n", errorStyle.Render("✗")) fmt.Printf("\033[%dB", lines) fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render("Expected output does not match actual output.")) - fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render(utils.GetDifferenceBetweenStrings(block.ExpectedOutput.Content, commandOutput.StdOut))) + fmt.Printf(" %s\n", utils.GetDifferenceBetweenStrings(block.ExpectedOutput.Content, commandOutput.StdOut)) } } fmt.Printf("\r %s \n", checkStyle.Render("✔")) fmt.Printf("\033[%dB\n", lines) if e.Configuration.Verbose { - fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Render(commandOutput.StdOut)) + fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Render(commandOutput.StdOut)) } } else { fmt.Printf("\r %s \n", errorStyle.Render("✗")) diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 4a85897d..44647638 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -1,46 +1,80 @@ package logging import ( + "os" + "github.com/sirupsen/logrus" ) -func Init(level string) { - logrus.SetFormatter(&logrus.TextFormatter{ - DisableColors: false, - FullTimestamp: true, - }) +type Level string - switch level { - case "info": - logrus.SetLevel(logrus.InfoLevel) - case "warn": - logrus.SetLevel(logrus.WarnLevel) - case "error": - logrus.SetLevel(logrus.ErrorLevel) - case "debug": - logrus.SetLevel(logrus.DebugLevel) +const ( + Trace Level = "trace" + Debug Level = "debug" + Info Level = "info" + Warn Level = "warn" + Error Level = "error" + Fatal Level = "fatal" +) + +// / Convert a logging level to a logrus level (uint32). +func (l Level) Integer() logrus.Level { + switch l { + case Trace: + return logrus.TraceLevel + case Debug: + return logrus.DebugLevel + case Info: + return logrus.InfoLevel + case Warn: + return logrus.WarnLevel + case Error: + return logrus.ErrorLevel + case Fatal: + return logrus.FatalLevel default: - logrus.SetLevel(logrus.InfoLevel) + return logrus.InfoLevel } - } -func Info(args ...interface{}) { - logrus.Info(args...) +// / Convert a string to a logging level. +func LevelFromString(level string) Level { + switch level { + case string(Trace): + return Trace + case string(Debug): + return Debug + case string(Info): + return Info + case string(Warn): + return Warn + case string(Error): + return Error + case string(Fatal): + return Fatal + default: + return Info + } } -func Warn(args ...interface{}) { - logrus.Warn(args...) -} +var GlobalLogger = logrus.New() -func Error(args ...interface{}) { - logrus.Error(args...) -} +func Init(level Level) { + GlobalLogger.SetFormatter(&logrus.TextFormatter{ + DisableColors: false, + FullTimestamp: true, + DisableQuote: true, + }) -func Debug(args ...interface{}) { - logrus.Debug(args...) -} + GlobalLogger.SetReportCaller(true) + GlobalLogger.SetLevel(level.Integer()) -func Fatal(args ...interface{}) { - logrus.Fatal(args...) + file, err := os.OpenFile("ie.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) + + if err == nil { + GlobalLogger.SetOutput(file) + } else { + GlobalLogger.SetOutput(os.Stdout) + GlobalLogger.Info("Failed to log to file, using default stderr") + } } diff --git a/internal/parsers/ini.go b/internal/parsers/ini.go index abe0abf2..020d278c 100644 --- a/internal/parsers/ini.go +++ b/internal/parsers/ini.go @@ -1,8 +1,7 @@ package parsers import ( - "log" - + "github.com/Azure/InnovationEngine/internal/logging" "gopkg.in/ini.v1" ) @@ -13,7 +12,7 @@ func ParseINIFile(filePath string) map[string]string { iniFile, err := ini.Load(filePath) if err != nil { - log.Fatalf("Failed to read the INI file %s because %v", filePath, err) + logging.GlobalLogger.Fatalf("Failed to read the INI file %s because %v", filePath, err) } data := make(map[string]string) for _, section := range iniFile.Sections() { diff --git a/internal/parsers/markdown.go b/internal/parsers/markdown.go index b630773b..7246b701 100644 --- a/internal/parsers/markdown.go +++ b/internal/parsers/markdown.go @@ -6,6 +6,7 @@ import ( "strconv" "strings" + "github.com/Azure/InnovationEngine/internal/logging" "github.com/yuin/goldmark" "github.com/yuin/goldmark/ast" "github.com/yuin/goldmark/extension" @@ -97,7 +98,7 @@ func ExtractCodeBlocksFromAst(node ast.Node, source []byte, languagesToExtract [ // score isn't parsable as a float. if match != nil { score, err := strconv.ParseFloat(match[1], 64) - fmt.Println("score", score) + logging.GlobalLogger.Debugf("Simalrity score of %f found", score) if err != nil { return ast.WalkStop, err } @@ -154,7 +155,7 @@ func ExtractScenarioVariablesFromAst(node ast.Node, source []byte) map[string]st if entering && node.Kind() == ast.KindHTMLBlock { htmlNode := node.(*ast.HTMLBlock) blockContent := extractTextFromMarkdown(&htmlNode.BaseBlock, source) - fmt.Printf("Found HTML block with the content: %s\n", blockContent) + logging.GlobalLogger.Debugf("Found HTML block with the content: %s\n", blockContent) match := variableCommentBlockRegex.FindStringSubmatch(blockContent) // Extract the variables from the comment block. @@ -183,7 +184,7 @@ func convertScenarioVariablesToMap(variableBlock string) map[string]string { if len(parts) == 2 { key := strings.TrimPrefix(parts[0], "export ") value := parts[1] - fmt.Printf("Found variable: %s=%s\n", key, value) + logging.GlobalLogger.Debugf("Found variable: %s=%s\n", key, value) variableMap[key] = value } } diff --git a/internal/utils/json.go b/internal/utils/json.go index 2da5068e..8fa82c38 100644 --- a/internal/utils/json.go +++ b/internal/utils/json.go @@ -23,7 +23,7 @@ func OrderJsonFields(jsonStr string) (string, error) { // Compute the Jaro-Winkler score for two JSON strings. The score is computed // by ordering the fields alphabetically and then comparing the strings using // the Jaro-Winkler algorithm. -func ComputeJaroWinklerScore(actualJson string, expectedJson string) (float64, error) { +func ComputeJsonStringSimilarity(actualJson string, expectedJson string) (float64, error) { actualOutput, err := OrderJsonFields(actualJson) if err != nil { return 0, err @@ -34,14 +34,14 @@ func ComputeJaroWinklerScore(actualJson string, expectedJson string) (float64, e return 0, err } - return smetrics.JaroWinkler(expectedOutput, actualOutput, 0.7, 4), nil + return smetrics.Jaro(actualOutput, expectedOutput), nil } // Compare two JSON strings by ordering the fields alphabetically and then // comparing the strings using the Jaro-Winkler algorithm to compute a score. // If the score is greater than the threshold, return true. func CompareJsonStrings(actualJson string, expectedJson string, threshold float64) (bool, error) { - score, err := ComputeJaroWinklerScore(actualJson, expectedJson) + score, err := ComputeJsonStringSimilarity(actualJson, expectedJson) if err != nil { return false, err } From e63a09805a290cf3b1e3827e57f9d7bd9fb76805 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 14 Jul 2023 11:36:04 -0700 Subject: [PATCH 062/226] [update] formatting for IE & add build & test pipeline --- .github/workflows/build-test-release.yaml | 19 +++++++++++++++++++ Makefile | 8 +++++++- internal/engine/common.go | 10 ++++++---- internal/engine/execution.go | 5 ++--- internal/engine/scenario.go | 3 +++ internal/engine/testing.go | 11 +++++------ testScripts/brokenMarkdown.md | 2 +- 7 files changed, 43 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/build-test-release.yaml diff --git a/.github/workflows/build-test-release.yaml b/.github/workflows/build-test-release.yaml new file mode 100644 index 00000000..d9e73189 --- /dev/null +++ b/.github/workflows/build-test-release.yaml @@ -0,0 +1,19 @@ +name: build-test-release + +on: + push: + branches: + - main + - vmarcella/port-to-go + +jobs: + build-test-release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Build the Innovation Engine CLI. + run: | + make build-ie + - name: Test + run: | + make test-all diff --git a/Makefile b/Makefile index 6926c18b..e79494b2 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,12 @@ build-runner: build-ie build-api build-all: build-ie build-api build-runner +# ------------------------------ Test targets ---------------------------------- + +test-all: + @echo "Running all tests..." + @go test -v ./... + # ------------------------------- Run targets ---------------------------------- run-ie: build-ie @@ -80,4 +86,4 @@ k8s-delete-cluster: k8s-delete-api k8s-delete-ingress-controller @echo "Deleted Kubernetes cluster for local development." k8s-refresh-cluster: k8s-delete-cluster k8s-initialize-cluster - @echo "Refreshed Kubernetes cluster for local development." \ No newline at end of file + @echo "Refreshed Kubernetes cluster for local development." diff --git a/internal/engine/common.go b/internal/engine/common.go index c59ad704..5b4310f9 100644 --- a/internal/engine/common.go +++ b/internal/engine/common.go @@ -8,10 +8,12 @@ import ( // Styles used for rendering output to the terminal. var ( - spinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")) - checkStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#32CD32")) - errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000")) - titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Align(lipgloss.Center).Bold(true) + spinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")) + checkStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#32CD32")) + errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000")) + errorMessageStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")) + verboseStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Align(lipgloss.Left) + titleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Align(lipgloss.Center).Bold(true) ) // Indents a multi-line command to be nested under the first line of the diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 4fcad473..54bf3a18 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -10,7 +10,6 @@ import ( "github.com/Azure/InnovationEngine/internal/parsers" "github.com/Azure/InnovationEngine/internal/shells" "github.com/Azure/InnovationEngine/internal/utils" - "github.com/charmbracelet/lipgloss" ) const ( @@ -106,12 +105,12 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { fmt.Printf("\r %s \n", checkStyle.Render("✔")) fmt.Printf("\033[%dB\n", lines) if e.Configuration.Verbose { - fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Render(commandOutput.StdOut)) + fmt.Printf(" %s\n", verboseStyle.Render(commandOutput.StdOut)) } } else { fmt.Printf("\r %s \n", errorStyle.Render("✗")) fmt.Printf("\033[%dB", lines) - fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render(err.Error())) + fmt.Printf(" %s\n", errorMessageStyle.Render(err.Error())) } break loop diff --git a/internal/engine/scenario.go b/internal/engine/scenario.go index 864709cc..635de18b 100644 --- a/internal/engine/scenario.go +++ b/internal/engine/scenario.go @@ -26,6 +26,9 @@ type Scenario struct { Environment map[string]string } +// Groups the codeblocks into steps based on the header of the codeblock. +// This organizes the codeblocks into steps that can be executed in a linear +// order. func groupCodeBlocksIntoSteps(blocks []parsers.CodeBlock) []Step { var groupedSteps []Step var headerIndex = make(map[string]int) diff --git a/internal/engine/testing.go b/internal/engine/testing.go index fee57389..0f960188 100644 --- a/internal/engine/testing.go +++ b/internal/engine/testing.go @@ -9,7 +9,6 @@ import ( "github.com/Azure/InnovationEngine/internal/parsers" "github.com/Azure/InnovationEngine/internal/shells" "github.com/Azure/InnovationEngine/internal/utils" - "github.com/charmbracelet/lipgloss" "github.com/xrash/smetrics" ) @@ -62,14 +61,14 @@ func (e *Engine) TestSteps(steps []Step, env map[string]string) { if err != nil { fmt.Printf("\r %s \n", errorStyle.Render("✗")) fmt.Printf("\033[%dB", lines) - fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render(err.Error())) + fmt.Printf(" %s\n", errorMessageStyle.Render(err.Error())) break loop } if !meetsThreshold { fmt.Printf("\r %s \n", errorStyle.Render("✗")) fmt.Printf("\033[%dB", lines) - fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render("Expected output does not match actual output.")) + fmt.Printf(" %s\n", errorMessageStyle.Render("Expected output does not match actual output.")) fmt.Printf(" %s\n", utils.GetDifferenceBetweenStrings(expectedOutput, actualOutput)) break loop } @@ -87,7 +86,7 @@ func (e *Engine) TestSteps(steps []Step, env map[string]string) { if block.ExpectedOutput.ExpectedSimilarity > score { fmt.Printf("\r %s \n", errorStyle.Render("✗")) fmt.Printf("\033[%dB", lines) - fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render("Expected output does not match actual output.")) + fmt.Printf(" %s\n", errorMessageStyle.Render("Expected output does not match actual output.")) fmt.Printf(" %s\n", utils.GetDifferenceBetweenStrings(block.ExpectedOutput.Content, commandOutput.StdOut)) } } @@ -95,12 +94,12 @@ func (e *Engine) TestSteps(steps []Step, env map[string]string) { fmt.Printf("\r %s \n", checkStyle.Render("✔")) fmt.Printf("\033[%dB\n", lines) if e.Configuration.Verbose { - fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Render(commandOutput.StdOut)) + fmt.Printf(" %s\n", verboseStyle.Render(commandOutput.StdOut)) } } else { fmt.Printf("\r %s \n", errorStyle.Render("✗")) fmt.Printf("\033[%dB", lines) - fmt.Printf(" %s\n", lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")).Render(err.Error())) + fmt.Printf(" %s\n", errorMessageStyle.Render(err.Error())) } break loop diff --git a/testScripts/brokenMarkdown.md b/testScripts/brokenMarkdown.md index 4e2e5197..2754fdb4 100644 --- a/testScripts/brokenMarkdown.md +++ b/testScripts/brokenMarkdown.md @@ -4,4 +4,4 @@ Innovation Engine should be able to exit the program automatically instead of ha ```bash echo "hello World" -`` \ No newline at end of file +`` From 68a1b53a48e4bd3511e2363660b0ee88eaea1250 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 14 Jul 2023 11:47:39 -0700 Subject: [PATCH 063/226] [remove] redundant actions. --- .github/workflows/00-testing.yml | 25 ----------------------- .github/workflows/github-actions-demo.yml | 7 ------- 2 files changed, 32 deletions(-) delete mode 100644 .github/workflows/00-testing.yml delete mode 100644 .github/workflows/github-actions-demo.yml diff --git a/.github/workflows/00-testing.yml b/.github/workflows/00-testing.yml deleted file mode 100644 index 6e7c3145..00000000 --- a/.github/workflows/00-testing.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: 00-testing - -on: - push: - branches: - - ParserAndExecutor - - workflow_dispatch: - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: azure/login@v1 - with: - creds: ${{ secrets.AZURE_CREDENTIALS }} - - name: Deploy - env: - AZURE_CREDENTIALS: ${{ secrets.AZURE_CREDENTIALS }} - GITHUB_SHA: ${{ github.sha }} - run: | - cd $GITHUB_WORKSPACE/ - pip3 install -r requirements.txt - python3 main.py test test.md \ No newline at end of file diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml deleted file mode 100644 index 48305a73..00000000 --- a/.github/workflows/github-actions-demo.yml +++ /dev/null @@ -1,7 +0,0 @@ -name: GitHub Actions Demo -on: [push] -jobs: - Explore-Gitub-Actions: - runs-on: ubuntu-latest - steps: - - run: echo "Hello World" \ No newline at end of file From ac2e39398680c2cefbee63e8f0e0692317e7436d Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 14 Jul 2023 11:48:29 -0700 Subject: [PATCH 064/226] [update] pipeline to build all targets in the first step. --- .github/workflows/build-test-release.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-test-release.yaml b/.github/workflows/build-test-release.yaml index d9e73189..40e4e23f 100644 --- a/.github/workflows/build-test-release.yaml +++ b/.github/workflows/build-test-release.yaml @@ -11,9 +11,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Build the Innovation Engine CLI. + - name: Build all targets. run: | - make build-ie - - name: Test + make build-all + - name: Run unit tests across all targets. run: | make test-all From e9cfaad1339f56b1c947bd667f84437782b87e1c Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 14 Jul 2023 12:01:08 -0700 Subject: [PATCH 065/226] [add] release step. --- .github/workflows/build-test-release.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/build-test-release.yaml b/.github/workflows/build-test-release.yaml index 40e4e23f..e754e5a5 100644 --- a/.github/workflows/build-test-release.yaml +++ b/.github/workflows/build-test-release.yaml @@ -17,3 +17,11 @@ jobs: - name: Run unit tests across all targets. run: | make test-all + - name: Release Innovation Engine + uses: softprops/action-gh-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + draft: true + prelease: true + files: ./bin From be94521fae57a56ecff7a52d049dcdb60b0370de Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 14 Jul 2023 12:06:06 -0700 Subject: [PATCH 066/226] [update] release step. --- .github/workflows/build-test-release.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-test-release.yaml b/.github/workflows/build-test-release.yaml index e754e5a5..0f3425f2 100644 --- a/.github/workflows/build-test-release.yaml +++ b/.github/workflows/build-test-release.yaml @@ -18,10 +18,10 @@ jobs: run: | make test-all - name: Release Innovation Engine - uses: softprops/action-gh-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + uses: "marvinpinto/action-automatic-releases@latest" with: - draft: true - prelease: true - files: ./bin + repo_token: ${{ secrets.GITHUB_TOKEN }} + title: "IE" + automatic_release_tag: "latest" + prerelease: true + files: ./bin/ie From 8c10a8f78ea1ad4287d641bd9753496f177ee2ea Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 14 Jul 2023 12:41:14 -0700 Subject: [PATCH 067/226] [update] build to not rely on C bindings. --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index e79494b2..01445a23 100644 --- a/Makefile +++ b/Makefile @@ -8,15 +8,15 @@ API_BINARY := $(BINARY_DIR)/api build-ie: @echo "Building the Innovation Engine CLI..." - @go build -o "$(IE_BINARY)" cmd/ie/ie.go + @CGO_ENABLED=0 go build -o "$(IE_BINARY)" cmd/ie/ie.go build-api: @echo "Building the Innovation Engine API..." - @go build -o "$(API_BINARY)" cmd/api/main.go + @CGO_ENABLED=0 go build -o "$(API_BINARY)" cmd/api/main.go build-runner: build-ie build-api @echo "Building the Innovation Engine Runner..." - @go build -o "$(BINARY_DIR)/runner" cmd/runner/main.go + @CGO_ENABLED=0 go build -o "$(BINARY_DIR)/runner" cmd/runner/main.go build-all: build-ie build-api build-runner From 5b1aa45af25bd4b2249c866b5c0e76fbf35b64c7 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 17 Jul 2023 10:54:27 -0700 Subject: [PATCH 068/226] [add] install from release script for OCD. --- scripts/install_from_release.sh | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 scripts/install_from_release.sh diff --git a/scripts/install_from_release.sh b/scripts/install_from_release.sh new file mode 100644 index 00000000..6f8c9dd2 --- /dev/null +++ b/scripts/install_from_release.sh @@ -0,0 +1,14 @@ +set -e + +# Download the binary from the latest +wget -O ie https://github.com/Azure/InnovationEngine/releases/download/latest/ie + +# Setup permissions & move to the local bin +chmod +x ie +mkdir -p ~/.local/bin +mv ie ~/.local/bin + +# Export the path to IE if it's not already available +if ![[ "$PATH" =~ "~/.local/bin" || "$PATH" =~ "$HOME/.local/bin" ]]; then + export PATH="$PATH:~/.local/bin" +fi From 02f43557c2d758fc60f7b4b3106f8cf96d3ead09 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 17 Jul 2023 11:08:55 -0700 Subject: [PATCH 069/226] [update] path check. --- scripts/install_from_release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/install_from_release.sh b/scripts/install_from_release.sh index 6f8c9dd2..fe5001f0 100644 --- a/scripts/install_from_release.sh +++ b/scripts/install_from_release.sh @@ -9,6 +9,6 @@ mkdir -p ~/.local/bin mv ie ~/.local/bin # Export the path to IE if it's not already available -if ![[ "$PATH" =~ "~/.local/bin" || "$PATH" =~ "$HOME/.local/bin" ]]; then +if [[ !"$PATH" =~ "~/.local/bin" || !"$PATH" =~ "$HOME/.local/bin" ]]; then export PATH="$PATH:~/.local/bin" fi From ae78d3061a171779a7fad8dc30a48c530b940e87 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 17 Jul 2023 19:39:20 -0700 Subject: [PATCH 070/226] [add] scenarios into repository for ocd. --- scenarios/ocd/CreateAKSDeployment/README.md | 351 ++++++++++++++++++ .../azure-vote-agic-ssl.yml | 112 ++++++ .../CreateAKSDeployment/azure-vote-agic.yml | 104 ++++++ .../CreateAKSDeployment/azure-vote-start.yml | 85 +++++ .../cluster-issuer-prod.yml | 34 ++ .../README.md | 136 +++++++ scenarios/ocd/CreateLinuxVMAndSSH/README.md | 104 ++++++ .../CreateLinuxVMSecureWebServer/README.md | 163 ++++++++ 8 files changed, 1089 insertions(+) create mode 100644 scenarios/ocd/CreateAKSDeployment/README.md create mode 100644 scenarios/ocd/CreateAKSDeployment/azure-vote-agic-ssl.yml create mode 100644 scenarios/ocd/CreateAKSDeployment/azure-vote-agic.yml create mode 100644 scenarios/ocd/CreateAKSDeployment/azure-vote-start.yml create mode 100644 scenarios/ocd/CreateAKSDeployment/cluster-issuer-prod.yml create mode 100644 scenarios/ocd/CreateCloudNativePythonContainer/README.md create mode 100644 scenarios/ocd/CreateLinuxVMAndSSH/README.md create mode 100644 scenarios/ocd/CreateLinuxVMSecureWebServer/README.md diff --git a/scenarios/ocd/CreateAKSDeployment/README.md b/scenarios/ocd/CreateAKSDeployment/README.md new file mode 100644 index 00000000..b3f8e805 --- /dev/null +++ b/scenarios/ocd/CreateAKSDeployment/README.md @@ -0,0 +1,351 @@ +# Quickstart: Deploy a Scalable & Secure Azure Kubernetes Service cluster using the Azure CLI +Welcome to this tutorial where we will take you step by step in creating an Azure Kubernetes Web Application with a custom domain that is secured via https. This tutorial assumes you are logged into Azure CLI already and have selected a subscription to use with the CLI. It also assumes that you have Helm installed (Instructions can be found here https://helm.sh/docs/intro/install/). If you have not done this already. Press b and hit ctl c to exit the program. + +To Login to Az CLI and select a subscription +'az login' followed by 'az account list --output table' and 'az account set --subscription "name of subscription to use"' + +To Install Az CLI +If you need to install Azure CLI run the following command - curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + + +Assuming the pre requisites are met press enter to proceed + +## Define Command Line Variables +Most of these variables should be set to a smart default. However, if you want to change them +press b and run the command export VARIABLE_NAME="new variable value" + +```bash +echo $RESOURCE_GROUP_NAME +echo $RESOURCE_LOCATION +echo $AKS_CLUSTER_NAME +echo $PUBLIC_IP_NAME +echo $VNET_NAME +echo $SUBNET_NAME +echo $APPLICATION_GATEWAY_NAME +``` + +For the following variables, unless you manually added them in the env.json, you will be asked to provide an input + +The custom domain must be unique and fit the pattern: ^[a-z][a-z0-9-]{1,61}[a-z0-9]$ +For example mycooldomain - this domain is already taken btw :) + +Note - Do not add any capitalization or .com +```bash +if [[ ! $CUSTOM_DOMAIN_NAME =~ ^[a-z][a-z0-9-]{1,61}[a-z0-9] ]]; then echo "Invalid Domain, re enter your domain by pressing b and running 'export CUSTOM_DOMAIN_NAME="customdomainname"' then press r to re-run the previous command and validate the custom domain"; else echo "Custom Domain Set!"; fi; +``` + +For the email address any enter a valid email. I.e sarajane@gmail.com +```bash +echo $SSL_EMAIL_ADDRESS +``` + +## Create A Resource Group +An Azure resource group is a logical group in which Azure resources are deployed and managed. When you create a resource group, you are prompted to specify a location. This location is: + - The storage location of your resource group metadata. + - Where your resources will run in Azure if you don't specify another region during resource creation. + +Validate Resource Group does not already exist. If it does, select a new resource group name by running the following: + +```bash +if [ "$(az group exists --name $RESOURCE_GROUP_NAME)" = 'true' ]; then export RAND=$RANDOM; export RESOURCE_GROUP_NAME="$RESOURCE_GROUP_NAME$RAND"; echo "Your new Resource Group Name is $RESOURCE_GROUP_NAME"; fi +``` + +Create a resource group using the az group create command: +```bash +az group create --name $RESOURCE_GROUP_NAME --location $RESOURCE_LOCATION +``` +Results: + +```expected_similarity=0.5 +{ + "id": "/subscriptions/bb318642-28fd-482d-8d07-79182df07999/resourceGroups/testResourceGroup24763", + "location": "eastus", + "managedBy": null, + "name": "testResourceGroup", + "properties": { + "provisioningState": "Succeeded" + }, + "tags": null, + "type": "Microsoft.Resources/resourceGroups" +} +``` + +## Register to AKS Azure Resource Providers +Verify Microsoft.OperationsManagement and Microsoft.OperationalInsights providers are registered on your subscription. These are Azure resource providers required to support [Container insights](https://docs.microsoft.com/en-us/azure/azure-monitor/containers/container-insights-overview). To check the registration status, run the following commands + +```bash +az provider register --namespace Microsoft.OperationsManagement +az provider register --namespace Microsoft.OperationalInsights +``` + +## Create AKS Cluster +Create an AKS cluster using the az aks create command with the --enable-addons monitoring parameter to enable Container insights. The following example creates a cluster named myAKSCluster with one node: + +This will take a few minutes +```bash +az aks create --resource-group $RESOURCE_GROUP_NAME --name $AKS_CLUSTER_NAME --node-count 1 --enable-addons monitoring --generate-ssh-keys +``` + +## Connect to the cluster +To manage a Kubernetes cluster, use the Kubernetes command-line client, kubectl. kubectl is already installed if you use Azure Cloud Shell. + +1. Install az aks CLI locally using the az aks install-cli command + +```bash +if ! [ -x "$(command -v kubectl)" ]; then az aks install-cli; fi +``` + +2. Configure kubectl to connect to your Kubernetes cluster using the az aks get-credentials command. The following command: + - Downloads credentials and configures the Kubernetes CLI to use them. + - Uses ~/.kube/config, the default location for the Kubernetes configuration file. Specify a different location for your Kubernetes configuration file using --file argument. + +> [!WARNING] +> This will overwrite any existing credentials with the same entry + +```bash +az aks get-credentials --resource-group $RESOURCE_GROUP_NAME --name $AKS_CLUSTER_NAME --overwrite-existing +``` + +3. Verify the connection to your cluster using the kubectl get command. This command returns a list of the cluster nodes. + +```bash +kubectl get nodes +``` + +## Deploy the Application +A Kubernetes manifest file defines a cluster's desired state, such as which container images to run. + +In this quickstart, you will use a manifest to create all objects needed to run the Azure Vote application. This manifest includes two Kubernetes deployments: + +- The sample Azure Vote Python applications. +- A Redis instance. + +Two Kubernetes Services are also created: + +- An internal service for the Redis instance. +- An external service to access the Azure Vote application from the internet. + +A test voting app YML file is already prepared. To deploy this app run the following command +```bash +kubectl apply -f azure-vote-start.yml +``` + +## Test The Application +When the application runs, a Kubernetes service exposes the application front end to the internet. This process can take a few minutes to complete. + +Check progress using the kubectl get service command. + +```bash +kubectl get service +``` + +Store the public IP Address as an environment variable for later use. +>[!Note] +> This commmand loops for 2 minutes and queries the output of kubectl get service for the IP Address. Sometimes it can take a few seconds to propogate correctly +```bash +runtime="2 minute"; endtime=$(date -ud "$runtime" +%s); while [[ $(date -u +%s) -le $endtime ]]; do export IP_ADDRESS=$(kubectl get service azure-vote-front --output jsonpath='{.status.loadBalancer.ingress[0].ip}'); if ! [ -z $IP_ADDRESS ]; then break; else sleep 10; fi; done +``` + +Validate IP Address by running the following: +```bash +echo $IP_ADDRESS +``` + +# Add Application Gateway Ingress Controller +The Application Gateway Ingress Controller (AGIC) is a Kubernetes application, which makes it possible for Azure Kubernetes Service (AKS) customers to leverage Azure's native Application Gateway L7 load-balancer to expose cloud software to the Internet. AGIC monitors the Kubernetes cluster it is hosted on and continuously updates an Application Gateway, so that selected services are exposed to the Internet + +AGIC helps eliminate the need to have another load balancer/public IP in front of the AKS cluster and avoids multiple hops in your datapath before requests reach the AKS cluster. Application Gateway talks to pods using their private IP directly and does not require NodePort or KubeProxy services. This also brings better performance to your deployments. + +## Deploy a new Application Gateway +1. Create a Public IP for Application Gateway by running the following: +```bash +az network public-ip create --name $PUBLIC_IP_NAME --resource-group $RESOURCE_GROUP_NAME --allocation-method Static --sku Standard +``` + +2. Create a Virtual Network(Vnet) for Application Gateway by running the following: +```bash +az network vnet create --name $VNET_NAME --resource-group $RESOURCE_GROUP_NAME --address-prefix 11.0.0.0/8 --subnet-name $SUBNET_NAME --subnet-prefix 11.1.0.0/16 +``` + +3. Create Application Gateway by running the following: + +> [!NOTE] +> This will take around 5 minutes +```bash +az network application-gateway create --name $APPLICATION_GATEWAY_NAME --location $RESOURCE_LOCATION --resource-group $RESOURCE_GROUP_NAME --sku Standard_v2 --public-ip-address $PUBLIC_IP_NAME --vnet-name $VNET_NAME --subnet $SUBNET_NAME --priority 100 +``` + +## Enable the AGIC add-on in existing AKS cluster + +1. Store Application Gateway ID by running the following: +```bash +APPLICATION_GATEWAY_ID=$(az network application-gateway show --name $APPLICATION_GATEWAY_NAME --resource-group $RESOURCE_GROUP_NAME --output tsv --query "id") +``` + +2. Enable Application Gateway Ingress Addon by running the following: + +> [!NOTE] +> This will take a few minutes +```bash +az aks enable-addons --name $AKS_CLUSTER_NAME --resource-group $RESOURCE_GROUP_NAME --addon ingress-appgw --appgw-id $APPLICATION_GATEWAY_ID +``` + +3. Store the node resource as an environment variable group by running the following: +```bash +NODE_RESOURCE_GROUP=$(az aks show --name myAKSCluster --resource-group $RESOURCE_GROUP_NAME --output tsv --query "nodeResourceGroup") +``` +4. Store the Vnet name as an environment variable by running the following: +```bash +AKS_VNET_NAME=$(az network vnet list --resource-group $NODE_RESOURCE_GROUP --output tsv --query "[0].name") +``` + +5. Store the Vnet ID as an environment variable by running the following: +```bash +AKS_VNET_ID=$(az network vnet show --name $AKS_VNET_NAME --resource-group $NODE_RESOURCE_GROUP --output tsv --query "id") +``` +## Peer the two virtual networks together +Since we deployed the AKS cluster in its own virtual network and the Application Gateway in another virtual network, you'll need to peer the two virtual networks together in order for traffic to flow from the Application Gateway to the pods in the cluster. Peering the two virtual networks requires running the Azure CLI command two separate times, to ensure that the connection is bi-directional. The first command will create a peering connection from the Application Gateway virtual network to the AKS virtual network; the second command will create a peering connection in the other direction. + +1. Create the peering from Application Gateway to AKS by runnig the following: +```bash +az network vnet peering create --name $APPGW_TO_AKS_PEERING_NAME --resource-group $RESOURCE_GROUP_NAME --vnet-name $VNET_NAME --remote-vnet $AKS_VNET_ID --allow-vnet-access +``` + +2. Store Id of Application Gateway Vnet As enviornment variable by running the following: +```bash +APPLICATION_GATEWAY_VNET_ID=$(az network vnet show --name $VNET_NAME --resource-group $RESOURCE_GROUP_NAME --output tsv --query "id") +``` +3. Create Vnet Peering from AKS to Application Gateway +```bash +az network vnet peering create --name $AKS_TO_APPGW_PEERING_NAME --resource-group $NODE_RESOURCE_GROUP --vnet-name $AKS_VNET_NAME --remote-vnet $APPLICATION_GATEWAY_VNET_ID --allow-vnet-access +``` +4. Store New IP address as environment variable by running the following command: +```bash +runtime="2 minute"; endtime=$(date -ud "$runtime" +%s); while [[ $(date -u +%s) -le $endtime ]]; do export IP_ADDRESS=$(az network public-ip show --resource-group $RESOURCE_GROUP_NAME --name $PUBLIC_IP_NAME --query ipAddress --output tsv); if ! [ -z $IP_ADDRESS ]; then break; else sleep 10; fi; done +``` + +## Apply updated application YAML complete with AGIC +In order to use the Application Gateway Ingress Controller we deployed we need to re-deploy an update Voting App YML file. The following command will update the application: + +The full updated YML file can be viewed at `azure-vote-agic-yml` +```bash +kubectl apply -f azure-vote-agic.yml +``` + +## Check that the application is reachable +Now that the Application Gateway is set up to serve traffic to the AKS cluster, let's verify that your application is reachable. + +Check that the sample application you created is up and running by either visiting the IP address of the Application Gateway that get from running the following command or check with curl. It may take Application Gateway a minute to get the update, so if the Application Gateway is still in an "Updating" state on Portal, then let it finish before trying to reach the IP address. Run the following to check the status: +```bash +kubectl get ingress +``` + +## Add custom subdomain to AGIC +Now Application Gateway Ingress has been added to the application gateway the next step is to add a custom domain. This will allow the endpoint to be reached by a human readable URL as well as allow for SSL Termination at the endpoint. + +1. Store Unique ID of the Public IP Address as an environment variable by running the following: +```bash +export PUBLIC_IP_ID=$(az network public-ip list --query "[?ipAddress!=null]|[?contains(ipAddress, '$IP_ADDRESS')].[id]" --output tsv) +``` + +2. Update public IP to respond to custom domain requests by running the following: +```bash +az network public-ip update --ids $PUBLIC_IP_ID --dns-name $CUSTOM_DOMAIN_NAME +``` + +3. Validate the resource is reachable via the custom domain. +```bash +az network public-ip show --ids $PUBLIC_IP_ID --query "[dnsSettings.fqdn]" --output tsv +``` + +4. Store the custom domain as en enviornment variable. This will be used later when setting up https termination. +```bash +export FQDN=$(az network public-ip show --ids $PUBLIC_IP_ID --query "[dnsSettings.fqdn]" --output tsv) +``` + +# Add HTTPS termination to custom domain +At this point in the tutorial you have an AKS web app with Application Gateway as the Ingress controller and a custom domain you can use to access your application. The next step is to add an SSL certificate to the domain so that users can reach your application securely via https. + +## Set Up Cert Manager +In order to add HTTPS we are going to use Cert Manager. Cert Manager is an open source tool used to obtain and manage SSL certificate for Kubernetes deployments. Cert Manager will obtain certificates from a variety of Issuers, both popular public Issuers as well as private Issuers, and ensure the certificates are valid and up-to-date, and will attempt to renew certificates at a configured time before expiry. + +1. In order to install cert-manager, we must first create a namespace to run it in. This tutorial will install cert-manager into the cert-manager namespace. It is possible to run cert-manager in a different namespace, although you will need to make modifications to the deployment manifests. +```bash +kubectl create namespace cert-manager +``` + +2. We can now install cert-manager. All resources are included in a single YAML manifest file. This can be installed by running the following: +```bash +kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.7.0/cert-manager.crds.yaml +``` + + +3. Add the certmanager.k8s.io/disable-validation: "true" label to the cert-manager namespace by running the following. This will allow the system resources that cert-manager requires to bootstrap TLS to be created in its own namespace. +```bash +kubectl label namespace cert-manager certmanager.k8s.io/disable-validation=true +``` + +## Obtain certificate via Helm Charts +Helm is a Kubernetes deployment tool for automating creation, packaging, configuration, and deployment of applications and services to Kubernetes clusters. + +Cert-manager provides Helm charts as a first-class method of installation on Kubernetes. + +1. Add the Jetstack Helm repository +This repository is the only supported source of cert-manager charts. There are some other mirrors and copies across the internet, but those are entirely unofficial and could present a security risk. +```bash +helm repo add jetstack https://charts.jetstack.io +``` + +2. Update local Helm Chart repository cache +```bash +helm repo update +``` + +3. Install Cert-Manager addon via helm by running the following: +```bash +helm install cert-manager jetstack/cert-manager --namespace cert-manager --version v1.7.0 +``` + +4. Apply Certificate Issuer YAML File + + ClusterIssuers are Kubernetes resources that represent certificate authorities (CAs) that are able to generate signed certificates by honoring certificate signing requests. All cert-manager certificates require a referenced issuer that is in a ready condition to attempt to honor the request. + + The issuer we are using can be found in the `cluster-issuer-prod.yaml file` +```bash +envsubst < cluster-issuer-prod.yaml | kubectl apply -f - +``` + +5. Upate Voting App Application to use Cert-Manager to obtain an SSL Certificate. + + The full YAML file can be found in `azure-vote-agic-ssl-yml` +```bash +envsubst < azure-vote-agic-ssl.yml | kubectl apply -f - +``` +## Validate application is working + +Wait for SSL certificate to issue. The following command will query the status of the SSL certificate for 3 minutes. + In rare occasions it may take up to 15 minutes for Lets Encrypt to issue a successful challenge and the ready state to be 'True' +```bash +runtime="10 minute"; endtime=$(date -ud "$runtime" +%s); while [[ $(date -u +%s) -le $endtime ]]; do STATUS=$(kubectl get certificate --output jsonpath={..status.conditions[0].status}); echo $STATUS; if [ "$STATUS" = 'True' ]; then break; else sleep 10; fi; done +``` + +Validate SSL certificate is True by running the follow command: +```bash +kubectl get certificate --output jsonpath={..status.conditions[0].status} +``` + +Results: + +```expected_similarity=0.8 +True +``` + +## Browse your AKS Deployment Secured via HTTPS! +Run the following command to get the HTTPS endpoint for your application: + +>[!Note] +> It often takes 2-3 minutes for the SSL certificate to propogate and the site to be reachable via https +```bash +echo https://$FQDN +``` +Paste this into the browser to validate your deployment. diff --git a/scenarios/ocd/CreateAKSDeployment/azure-vote-agic-ssl.yml b/scenarios/ocd/CreateAKSDeployment/azure-vote-agic-ssl.yml new file mode 100644 index 00000000..3c595a31 --- /dev/null +++ b/scenarios/ocd/CreateAKSDeployment/azure-vote-agic-ssl.yml @@ -0,0 +1,112 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: azure-vote-back +spec: + replicas: 1 + selector: + matchLabels: + app: azure-vote-back + template: + metadata: + labels: + app: azure-vote-back + spec: + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: azure-vote-back + image: mcr.microsoft.com/oss/bitnami/redis:6.0.8 + env: + - name: ALLOW_EMPTY_PASSWORD + value: "yes" + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 250m + memory: 256Mi + ports: + - containerPort: 6379 + name: redis +--- +apiVersion: v1 +kind: Service +metadata: + name: azure-vote-back +spec: + ports: + - port: 6379 + selector: + app: azure-vote-back +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: azure-vote-front +spec: + replicas: 1 + selector: + matchLabels: + app: azure-vote-front + template: + metadata: + labels: + app: azure-vote-front + spec: + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: azure-vote-front + image: mcr.microsoft.com/azuredocs/azure-vote-front:v1 + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 250m + memory: 256Mi + ports: + - containerPort: 80 + env: + - name: REDIS + value: "azure-vote-back" +--- +apiVersion: v1 +kind: Service +metadata: + name: azure-vote-front +spec: + type: + ports: + - port: 80 + selector: + app: azure-vote-front +--- +# INGRESS WITH SSL PROD +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: azure-vote-ingress-agic-ssl + annotations: + kubernetes.io/ingress.class: azure/application-gateway + kubernetes.io/tls-acme: 'true' + appgw.ingress.kubernetes.io/ssl-redirect: "true" + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + tls: + - hosts: + - $FQDN + secretName: azure-vote-agic-secret + rules: + - host: $FQDN + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: azure-vote-front + port: + number: 80 diff --git a/scenarios/ocd/CreateAKSDeployment/azure-vote-agic.yml b/scenarios/ocd/CreateAKSDeployment/azure-vote-agic.yml new file mode 100644 index 00000000..f93cdcdc --- /dev/null +++ b/scenarios/ocd/CreateAKSDeployment/azure-vote-agic.yml @@ -0,0 +1,104 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: azure-vote-back +spec: + replicas: 1 + selector: + matchLabels: + app: azure-vote-back + template: + metadata: + labels: + app: azure-vote-back + spec: + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: azure-vote-back + image: mcr.microsoft.com/oss/bitnami/redis:6.0.8 + env: + - name: ALLOW_EMPTY_PASSWORD + value: "yes" + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 250m + memory: 256Mi + ports: + - containerPort: 6379 + name: redis +--- +apiVersion: v1 +kind: Service +metadata: + name: azure-vote-back +spec: + ports: + - port: 6379 + selector: + app: azure-vote-back +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: azure-vote-front +spec: + replicas: 1 + selector: + matchLabels: + app: azure-vote-front + template: + metadata: + labels: + app: azure-vote-front + spec: + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: azure-vote-front + image: mcr.microsoft.com/azuredocs/azure-vote-front:v1 + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 250m + memory: 256Mi + ports: + - containerPort: 80 + env: + - name: REDIS + value: "azure-vote-back" +--- +apiVersion: v1 +kind: Service +metadata: + name: azure-vote-front +spec: + type: + ports: + - port: 80 + selector: + app: azure-vote-front +--- +#Application Gateway Ingress +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: azure-vote-front + annotations: + kubernetes.io/ingress.class: azure/application-gateway +spec: + rules: + - http: + paths: + - path: / + backend: + service: + name: azure-vote-front + port: + number: 80 + pathType: Exact diff --git a/scenarios/ocd/CreateAKSDeployment/azure-vote-start.yml b/scenarios/ocd/CreateAKSDeployment/azure-vote-start.yml new file mode 100644 index 00000000..9b34a0c6 --- /dev/null +++ b/scenarios/ocd/CreateAKSDeployment/azure-vote-start.yml @@ -0,0 +1,85 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: azure-vote-back +spec: + replicas: 1 + selector: + matchLabels: + app: azure-vote-back + template: + metadata: + labels: + app: azure-vote-back + spec: + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: azure-vote-back + image: mcr.microsoft.com/oss/bitnami/redis:6.0.8 + env: + - name: ALLOW_EMPTY_PASSWORD + value: "yes" + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 250m + memory: 256Mi + ports: + - containerPort: 6379 + name: redis +--- +apiVersion: v1 +kind: Service +metadata: + name: azure-vote-back +spec: + ports: + - port: 6379 + selector: + app: azure-vote-back +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: azure-vote-front +spec: + replicas: 1 + selector: + matchLabels: + app: azure-vote-front + template: + metadata: + labels: + app: azure-vote-front + spec: + nodeSelector: + "kubernetes.io/os": linux + containers: + - name: azure-vote-front + image: mcr.microsoft.com/azuredocs/azure-vote-front:v1 + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 250m + memory: 256Mi + ports: + - containerPort: 80 + env: + - name: REDIS + value: "azure-vote-back" +--- +apiVersion: v1 +kind: Service +metadata: + name: azure-vote-front +spec: + type: LoadBalancer + ports: + - port: 80 + selector: + app: azure-vote-front diff --git a/scenarios/ocd/CreateAKSDeployment/cluster-issuer-prod.yml b/scenarios/ocd/CreateAKSDeployment/cluster-issuer-prod.yml new file mode 100644 index 00000000..3f4a9661 --- /dev/null +++ b/scenarios/ocd/CreateAKSDeployment/cluster-issuer-prod.yml @@ -0,0 +1,34 @@ +#!/bin/bash +#kubectl apply -f - <, which is also referenced in step 1 of the video walkthrough. The updated sample is at: . diff --git a/scenarios/ocd/CreateLinuxVMAndSSH/README.md b/scenarios/ocd/CreateLinuxVMAndSSH/README.md new file mode 100644 index 00000000..67136eff --- /dev/null +++ b/scenarios/ocd/CreateLinuxVMAndSSH/README.md @@ -0,0 +1,104 @@ +# Create a Linux VM and SSH On Azure + +## Define Environment Variables + +The First step in this tutorial is to define environment variables + +```bash +export MY_RESOURCE_GROUP_NAME=myResourceGroup +export MY_LOCATION=EastUS +export MY_VM_NAME=myVM +export MY_USERNAME=azureuser +export MY_VM_IMAGE=UbuntuLTS +``` + +# Login to Azure using the CLI + +In order to run commands against Azure using the CLI you need to login. This is done, very simply, though the `az login` command: + +# Create a resource group + +A resource group is a container for related resources. All resources must be placed in a resource group. We will create one for this tutorial. The following command creates a resource group with the previously defined $MY_RESOURCE_GROUP_NAME and $MY_LOCATION parameters. + +```bash +az group create --name $MY_RESOURCE_GROUP_NAME --location $MY_LOCATION +``` + +Results: + +```json expected-similarity=0.7 +{ + "id": "/subscriptions/325e7c34-99fb-4190-aa87-1df746c67705/resourceGroups/myResourceGroup", + "location": "eastus", + "managedBy": null, + "name": "myResourceGroup", + "properties": { + "provisioningState": "Succeeded" + }, + "tags": null, + "type": "Microsoft.Resources/resourceGroups" +} +``` + +## Create the Virtual Machine + +To create a VM in this resource group we need to run a simple command, here we have provided the `--generate-ssh-keys` flag, this will cause the CLI to look for an avialable ssh key in `~/.ssh`, if one is found it will be used, otherwise one will be generated and stored in `~/.ssh`. We also provide the `--public-ip-sku Standard` flag to ensure that the machine is accessible via a public IP. Finally, we are deploying an `UbuntuLTS` image. + +All other values are configured using environment variables. + +```bash +az vm create --resource-group $MY_RESOURCE_GROUP_NAME --name $MY_VM_NAME --image $MY_VM_IMAGE --assign-identity --admin-username $MY_USERNAME --generate-ssh-keys --public-ip-sku Standard +``` + +Results: + +```json expected-similarity=0.7 +{ + "fqdns": "", + "id": "/subscriptions/325e7c34-99fb-4190-aa87-1df746c67705/resourceGroups/myResourceGroup/providers/Microsoft.Compute/virtualMachines/myVM", + "location": "eastus", + "macAddress": "00-0D-3A-10-4F-70", + "powerState": "VM running", + "privateIpAddress": "10.0.0.4", + "publicIpAddress": "52.147.208.85", + "resourceGroup": "myResourceGroup", + "zones": "" +} +``` + +## Add VM AAD Extension + +In order to use Azure AD Login for a Linux VM an extension needs to be installed on the VM. VM extensions are small applications that provide post-deployment configuration and automation tasks on Azure virtual machines + +The following installs the extension to enable Azure AD Login on the recently deployed VM + +```bash +az vm extension set --publisher Microsoft.Azure.ActiveDirectory --name AADSSHLoginForLinux --resource-group $MY_RESOURCE_GROUP_NAME --vm-name $MY_VM_NAME +``` +## Configure role assignments for the VM + +Now that you've created the VM, you need to configure an Azure RBAC policy to determine who can log in to the VM. Two Azure roles are used to authorize VM login: + +Virtual Machine Administrator Login: Users who have this role assigned can log in to an Azure virtual machine with administrator privileges. +Virtual Machine User Login: Users who have this role assigned can log in to an Azure virtual machine with regular user privileges. +To allow a user to log in to a VM over SSH, you must assign the Virtual Machine Administrator Login or Virtual Machine User Login role on the resource group that contains the VM and its associated virtual network, network interface, public IP address, or load balancer resources. + +An Azure user who has the Owner or Contributor role assigned for a VM doesn't automatically have privileges to Azure AD login to the VM over SSH. There's an intentional (and audited) separation between the set of people who control virtual machines and the set of people who can access virtual machines. + +The following example uses az role assignment create to assign the Virtual Machine Administrator Login role to the VM for your current Azure user. You obtain the username of your current Azure account by using az account show, and you set the scope to the VM created in a previous step by using az vm show. + +You can also assign the scope at a resource group or subscription level. Normal Azure RBAC inheritance permissions apply + +```bash +USERNAME=$(az account show --query user.name --output tsv) +RESOURCE_GROUP_ID=$(az group show --resource-group $MY_RESOURCE_GROUP_NAME --query id -o tsv) +az role assignment create --role "Virtual Machine Administrator Login" --assignee $USERNAME --scope $RESOURCE_GROUP_ID +``` + +# SSH Into VM + +You can now SSH into the VM by running the output of the following command in your ssh client of choice + +```bash +echo az ssh vm --name $MY_VM_NAME --resource-group $MY_RESOURCE_GROUP_NAME +``` diff --git a/scenarios/ocd/CreateLinuxVMSecureWebServer/README.md b/scenarios/ocd/CreateLinuxVMSecureWebServer/README.md new file mode 100644 index 00000000..5b848c71 --- /dev/null +++ b/scenarios/ocd/CreateLinuxVMSecureWebServer/README.md @@ -0,0 +1,163 @@ +# Intro to Create a NGINX Webserver Secured via HTTPS +Welcome to this tutorial where we will create a VM. This tutorial assumes you are logged into Azure CLI already and have selected a subscription to use with the CLI. If you have not done this already. Press b and hit ctl c to exit the program. Following that you can enter + +'az login' followed by 'az account list --output table' and 'az account set --subscription "name of subscription to use"' + + +If you need to install Azure CLI run the following command - curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash + + +Assuming the pre requisites are met press space bar to proceed + +## Create a resource Group +The first thing we need to do is create a resource group. You can do this by running the following command + +'az group create --name $RESOURCE_GROUP_NAME --location $RESOURCE_LOCATION' + +```bash +az group create --name $RESOURCE_GROUP_NAME --location $RESOURCE_LOCATION +``` + +Results: +``` +{ + "id": "/subscriptions/8c487e6a-8bbb-42bb-81e6-3c122d1bb1c7/resourceGroups/$RESOURCE_GROUP_NAME", + "location": "eastus", + "managedBy": null, + "name": "$RESOURCE_GROUP_NAME", + "properties": { + "provisioningState": "Succeeded" + }, + "tags": null, + "type": "Microsoft.Resources/resourceGroups" +} + +``` + +## Create a Virtual Machine (VM) +You can do this by running the following command: + +'az vm create --resource-group $RESOURCE_GROUP_NAME --name $VM_NAME --image $VM_IMAGE --admin-username $VM_ADMIN_USERNAME --generate-ssh-keys' + +```bash +az vm create --resource-group $RESOURCE_GROUP_NAME --name $VM_NAME --image $VM_IMAGE --admin-username $VM_ADMIN_USERNAME --generate-ssh-keys +``` + +Results: + +``` +{ + "fqdns": "", + "id": "/subscriptions//resourceGroups/$RESOURCE_GROUP_NAME2/providers/Microsoft.Compute/virtualMachines/$VM_NAME", + "location": "eastus", + "macAddress": "00-0D-3A-23-9A-49", + "powerState": "VM running", + "privateIpAddress": "10.0.0.4", + "publicIpAddress": "52.174.34.95", + "resourceGroup": "$RESOURCE_GROUP_NAME" +} +``` + +Congrats you created a VM! Next we will open port 80 and install NGINX. + +## Store IP Address as environment Variable +The following command will store the IP Address as a environment variable that we can access later to do SSH + +'export IP_ADDRESS=$(az vm show --show-details --resource-group $RESOURCE_GROUP_NAME --name $VM_NAME --query publicIps --output tsv)' + +```bash +export IP_ADDRESS=$(az vm show --show-details --resource-group $RESOURCE_GROUP_NAME --name $VM_NAME --query publicIps --output tsv) +``` + +## Validate IP_ADDRESS +Let's make sure the IP Address is correctly stored + +```bash +echo $IP_ADDRESS +``` + +# Open Port 80 to allow web traffic +Open port 80 with the following command: + +'az vm open-port --port 80,443 --resource-group $RESOURCE_GROUP_NAME --name $VM_NAME' + +```bash +az vm open-port --port 80,443 --resource-group $RESOURCE_GROUP_NAME --name $VM_NAME +``` + +## Validate SSH Connection +To validate you are connected to your VM you can run the following command: + +'ssh -o StrictHostKeyChecking=no $VM_ADMIN_USERNAME@$IP_ADDRESS hostname' +Note - For the following commands we place ssh before hand as we must connect to the VM each time to run commands + +```bash +ssh -o StrictHostKeyChecking=no $VM_ADMIN_USERNAME@$IP_ADDRESS hostname +``` + +## Ensure VM is up to date +Ensure the VM is up to date by running the following command: + +'sudo apt update' +Note - This may take ~30 seconds to complete + +```bash +ssh $VM_ADMIN_USERNAME@$IP_ADDRESS sudo apt update +``` + +## Install NGINX +Run the following command to install the NGINX webserver + +'sudo apt install nginx' +This may take a few minutes... + +```bash +ssh $VM_ADMIN_USERNAME@$IP_ADDRESS sudo apt --yes --force-yes install nginx +``` + +## View Your webserver running + +```bash +echo $IP_ADDRESS +``` + +Congratulations you have now created a Virtual Machine and installed a webserver! + +Press 1 to end the tutorial and 2 to secure your webserver via https + +1. Quit the tutorial +2. Secure your webserver via https and add a custom domain + +## Select unique custom domain Name + +When prompted to enter a value for CUSTOM_DOMAIN_NAME enter a custom domain for your webserver Note - This must be unique on Azure + +```bash +echo $CUSTOM_DOMAIN_NAME +``` + +install Az CLI extension for Front Door in order to add HTTPS + +```bash +az extension add --name front-door +``` + +## Setting Up HTTPS Terminated EndPoint + +The following command will set up a custom domain secured via https. This may take a few minutes +```bash +az network front-door create --backend-address $IP_ADDRESS --name $CUSTOM_DOMAIN_NAME --resource-group $RESOURCE_GROUP_NAME --accepted-protocols Http Https --forwarding-protocol HttpOnly --protocol Http +``` + +## See your webserver running HTTPS + +Run the following command to see the url of your webserver. +NOTE - It may take ~5 minutes for backends to update appropriately and for your site to be secured via https. + +```bash +az network front-door show --name $CUSTOM_DOMAIN_NAME --resource-group $RESOURCE_GROUP_NAME --query frontendEndpoints[*].hostName --output tsv +``` + +## Conclusion + +You have completed the tutorial! View your resources on portal.azure.com From e912e1ade1157344e2d1fa347b73fba64d5cca97 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 17 Jul 2023 19:41:23 -0700 Subject: [PATCH 071/226] [update] release. --- .github/workflows/build-test-release.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-test-release.yaml b/.github/workflows/build-test-release.yaml index 0f3425f2..44756660 100644 --- a/.github/workflows/build-test-release.yaml +++ b/.github/workflows/build-test-release.yaml @@ -24,4 +24,6 @@ jobs: title: "IE" automatic_release_tag: "latest" prerelease: true - files: ./bin/ie + files: | + ./bin/ie + scenarios/* From 7f9a302d65020419e5327b8248d4ff285debbced Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 18 Jul 2023 11:38:10 -0700 Subject: [PATCH 072/226] [update] testing scripts to live within the testing scenarios directory. --- .github/workflows/build-test-release.yaml | 1 - {testScripts => scenarios/testing}/CommentTest.md | 0 {testScripts => scenarios/testing}/brokenMarkdown.md | 0 {testScripts => scenarios/testing}/createRG.md | 0 .../testing}/e2eAzureTestCommentVariables.md | 0 {testScripts => scenarios/testing}/fuzzyMatchTest.md | 0 {testScripts => scenarios/testing}/nonCLI.md | 0 {testScripts => scenarios/testing}/test.md | 0 {testScripts => scenarios/testing}/variableHierarchy.ini | 0 {testScripts => scenarios/testing}/variableHierarchy.md | 0 10 files changed, 1 deletion(-) rename {testScripts => scenarios/testing}/CommentTest.md (100%) rename {testScripts => scenarios/testing}/brokenMarkdown.md (100%) rename {testScripts => scenarios/testing}/createRG.md (100%) rename {testScripts => scenarios/testing}/e2eAzureTestCommentVariables.md (100%) rename {testScripts => scenarios/testing}/fuzzyMatchTest.md (100%) rename {testScripts => scenarios/testing}/nonCLI.md (100%) rename {testScripts => scenarios/testing}/test.md (100%) rename {testScripts => scenarios/testing}/variableHierarchy.ini (100%) rename {testScripts => scenarios/testing}/variableHierarchy.md (100%) diff --git a/.github/workflows/build-test-release.yaml b/.github/workflows/build-test-release.yaml index 44756660..e5ae9661 100644 --- a/.github/workflows/build-test-release.yaml +++ b/.github/workflows/build-test-release.yaml @@ -26,4 +26,3 @@ jobs: prerelease: true files: | ./bin/ie - scenarios/* diff --git a/testScripts/CommentTest.md b/scenarios/testing/CommentTest.md similarity index 100% rename from testScripts/CommentTest.md rename to scenarios/testing/CommentTest.md diff --git a/testScripts/brokenMarkdown.md b/scenarios/testing/brokenMarkdown.md similarity index 100% rename from testScripts/brokenMarkdown.md rename to scenarios/testing/brokenMarkdown.md diff --git a/testScripts/createRG.md b/scenarios/testing/createRG.md similarity index 100% rename from testScripts/createRG.md rename to scenarios/testing/createRG.md diff --git a/testScripts/e2eAzureTestCommentVariables.md b/scenarios/testing/e2eAzureTestCommentVariables.md similarity index 100% rename from testScripts/e2eAzureTestCommentVariables.md rename to scenarios/testing/e2eAzureTestCommentVariables.md diff --git a/testScripts/fuzzyMatchTest.md b/scenarios/testing/fuzzyMatchTest.md similarity index 100% rename from testScripts/fuzzyMatchTest.md rename to scenarios/testing/fuzzyMatchTest.md diff --git a/testScripts/nonCLI.md b/scenarios/testing/nonCLI.md similarity index 100% rename from testScripts/nonCLI.md rename to scenarios/testing/nonCLI.md diff --git a/testScripts/test.md b/scenarios/testing/test.md similarity index 100% rename from testScripts/test.md rename to scenarios/testing/test.md diff --git a/testScripts/variableHierarchy.ini b/scenarios/testing/variableHierarchy.ini similarity index 100% rename from testScripts/variableHierarchy.ini rename to scenarios/testing/variableHierarchy.ini diff --git a/testScripts/variableHierarchy.md b/scenarios/testing/variableHierarchy.md similarity index 100% rename from testScripts/variableHierarchy.md rename to scenarios/testing/variableHierarchy.md From ef699411234b229a6bfc55c5f200c968bf18e6db Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 18 Jul 2023 11:52:46 -0700 Subject: [PATCH 073/226] [update] pipeline to zip up scenarios and make them part of the release. --- .github/workflows/build-test-release.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/build-test-release.yaml b/.github/workflows/build-test-release.yaml index e5ae9661..38dd4b2c 100644 --- a/.github/workflows/build-test-release.yaml +++ b/.github/workflows/build-test-release.yaml @@ -17,6 +17,10 @@ jobs: - name: Run unit tests across all targets. run: | make test-all + - name: Prepare scenarios to be released. + run: | + sudo apt install zip + zip -r scenarios.zip scenarios - name: Release Innovation Engine uses: "marvinpinto/action-automatic-releases@latest" with: @@ -26,3 +30,4 @@ jobs: prerelease: true files: | ./bin/ie + ./scenarios.zip From 8e75ac5f6194061cf46cd730b6a0e0a3a5603166 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 18 Jul 2023 12:09:42 -0700 Subject: [PATCH 074/226] [update] installation script. --- scripts/install_from_release.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/scripts/install_from_release.sh b/scripts/install_from_release.sh index fe5001f0..5197cd4a 100644 --- a/scripts/install_from_release.sh +++ b/scripts/install_from_release.sh @@ -2,12 +2,17 @@ set -e # Download the binary from the latest wget -O ie https://github.com/Azure/InnovationEngine/releases/download/latest/ie +wget -O scenarios.zip https://github.com/Azure/InnovationEngine/releases/download/latest/scenarios.zip # Setup permissions & move to the local bin chmod +x ie mkdir -p ~/.local/bin mv ie ~/.local/bin +# Unzip the scenarios +unzip scenarios.zip -d ~ +rm scenarios.zip + # Export the path to IE if it's not already available if [[ !"$PATH" =~ "~/.local/bin" || !"$PATH" =~ "$HOME/.local/bin" ]]; then export PATH="$PATH:~/.local/bin" From c1fe63657e76ffc149586376ef2c2d435778bfa0 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 18 Jul 2023 12:54:52 -0700 Subject: [PATCH 075/226] [update] installation script to be more silent. --- scripts/install_from_release.sh | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/scripts/install_from_release.sh b/scripts/install_from_release.sh index 5197cd4a..3b8ac8ad 100644 --- a/scripts/install_from_release.sh +++ b/scripts/install_from_release.sh @@ -1,19 +1,22 @@ set -e # Download the binary from the latest -wget -O ie https://github.com/Azure/InnovationEngine/releases/download/latest/ie -wget -O scenarios.zip https://github.com/Azure/InnovationEngine/releases/download/latest/scenarios.zip +echo "Installing IE & scenarios from the latest release..." +wget -q -O ie https://github.com/Azure/InnovationEngine/releases/download/latest/ie > /dev/null +wget -q -O scenarios.zip https://github.com/Azure/InnovationEngine/releases/download/latest/scenarios.zip > /dev/null # Setup permissions & move to the local bin -chmod +x ie -mkdir -p ~/.local/bin -mv ie ~/.local/bin +chmod +x ie > /dev/null +mkdir -p ~/.local/bin > /dev/null +mv ie ~/.local/bin > /dev/null -# Unzip the scenarios -unzip scenarios.zip -d ~ -rm scenarios.zip +# Unzip the scenarios, overwrite if they already exist. +unzip -o scenarios.zip -d ~ > /dev/null +rm scenarios.zip > /dev/null # Export the path to IE if it's not already available if [[ !"$PATH" =~ "~/.local/bin" || !"$PATH" =~ "$HOME/.local/bin" ]]; then export PATH="$PATH:~/.local/bin" fi + +echo "Done." From b1ce3b71632f9deac52dfd184e682810aba0b398 Mon Sep 17 00:00:00 2001 From: jasonmesser7 Date: Wed, 1 Feb 2023 15:47:53 -0800 Subject: [PATCH 076/226] Added Quickstart for VMSS --- demoScripts/vmssQuickstart.md | 254 ++++++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 demoScripts/vmssQuickstart.md diff --git a/demoScripts/vmssQuickstart.md b/demoScripts/vmssQuickstart.md new file mode 100644 index 00000000..f147428d --- /dev/null +++ b/demoScripts/vmssQuickstart.md @@ -0,0 +1,254 @@ +--- +title: Quickstart - Create a Virtual Machine Scale Set with Azure CLI +description: Get started with your deployments by learning how to quickly create a Virtual Machine Scale Set with Azure CLI. +author: ju-shim +ms.author: jushiman +ms.topic: quickstart +ms.service: virtual-machine-scale-sets +ms.date: 11/22/2022 +ms.reviewer: mimckitt +ms.custom: mimckitt, devx-track-azurecli, mode-api +--- + +# Quickstart: Create a Virtual Machine Scale Set with the Azure CLI + +**Applies to:** :heavy_check_mark: Linux VMs :heavy_check_mark: Windows VMs :heavy_check_mark: Uniform scale sets + +> [!NOTE] +> The following article is for Uniform Virtual Machine Scale Sets. We recommend using Flexible Virtual Machine Scale Sets for new workloads. Learn more about this new orchestration mode in our [Flexible Virtual Machine Scale Sets overview](flexible-virtual-machine-scale-sets.md). + +A Virtual Machine Scale Set allows you to deploy and manage a set of auto-scaling virtual machines. You can scale the number of VMs in the scale set manually, or define rules to autoscale based on resource usage like CPU, memory demand, or network traffic. An Azure load balancer then distributes traffic to the VM instances in the scale set. In this quickstart, you create a Virtual Machine Scale Set and deploy a sample application with the Azure CLI. + +[!INCLUDE [quickstarts-free-trial-note](../../includes/quickstarts-free-trial-note.md)] + +[!INCLUDE [azure-cli-prepare-your-environment.md](~/articles/reusable-content/azure-cli/azure-cli-prepare-your-environment.md)] + +- This article requires version 2.0.29 or later of the Azure CLI. If using Azure Cloud Shell, the latest version is already installed. + + +## Define Environment Variables + +```azurecli-interactive +export RESOURCE_GROUP_NAME=myResourceGroup +export RESOURCE_LOCATION=eastus +export SCALE_SET_NAME=myScaleSet +export BASE_VM_IMAGE=UbuntuLTS +export ADMIN_USERNAME=azureuser +export LOAD_BALANCER_NAME=myScaleSetLB +export BACKEND_POOL_NAME=myScaleSetLBBEPool +export LOAD_BALANCER_RULE_NAME=myLoadBalancerRuleWeb +export FRONT_END_IP_NAME=loadBalancerFrontEnd +export CUSTOM_SCRIPT_NAME=customScript +export SCALE_SET_PUBLIC_IP=myScaleSetLBPublicIP +``` + +## Create a scale set +Before you can create a scale set, create a resource group with [az group create](/cli/azure/group). The following example creates a resource group named *myResourceGroup* in the *eastus* location: + +```azurecli-interactive +az group create --name $RESOURCE_GROUP_NAME --location $RESOURCE_LOCATION +``` + + +```Output +{ + "id": "/subscriptions//resourceGroups/myResourceGroup", + "location": "eastus", + "managedBy": null, + "name": "myResourceGroup", + "properties": { + "provisioningState": "Succeeded" + }, + "tags": null, + "type": "Microsoft.Resources/resourceGroups" +} +``` + +Now create a Virtual Machine Scale Set with [az vmss create](/cli/azure/vmss). The following example creates a scale set named *myScaleSet* that is set to automatically update as changes are applied, and generates SSH keys if they do not exist in *~/.ssh/id_rsa*. These SSH keys are used if you need to log in to the VM instances. To use an existing set of SSH keys, instead use the `--ssh-key-value` parameter and specify the location of your keys. + +```azurecli-interactive +az vmss create \ + --resource-group $RESOURCE_GROUP_NAME \ + --name $SCALE_SET_NAME \ + --image $BASE_VM_IMAGE \ + --upgrade-policy-mode automatic \ + --admin-username $ADMIN_USERNAME \ + --generate-ssh-keys +``` + +It takes a few minutes to create and configure all the scale set resources and VMs. + + +## Deploy sample application +To test your scale set, install a basic web application. The Azure Custom Script Extension is used to download and run a script that installs an application on the VM instances. This extension is useful for post deployment configuration, software installation, or any other configuration / management task. For more information, see the [Custom Script Extension overview](../virtual-machines/extensions/custom-script-linux.md). + +Use the Custom Script Extension to install a basic NGINX web server. Apply the Custom Script Extension that installs NGINX with [az vmss extension set](/cli/azure/vmss/extension) as follows: + +```azurecli-interactive +az vmss extension set \ + --publisher Microsoft.Azure.Extensions \ + --version 2.0 \ + --name $CUSTOM_SCRIPT_NAME \ + --resource-group $RESOURCE_GROUP_NAME \ + --vmss-name $SCALE_SET_NAME \ + --settings '{"fileUris":["https://raw.githubusercontent.com/Azure-Samples/compute-automation-configurations/master/automate_nginx.sh"],"commandToExecute":"./automate_nginx.sh"}' +``` + +```Output +{ + "vmss": { + "doNotRunExtensionsOnOverprovisionedVMs": false, + "orchestrationMode": "Uniform", + "overprovision": true, + "provisioningState": "Succeeded", + "singlePlacementGroup": true, + "timeCreated": "2023-02-01T22:17:20.1117742+00:00", + "uniqueId": "38328143-69e8-4a9b-9d55-8a404cdb6d8b", + "upgradePolicy": { + "mode": "Automatic", + "rollingUpgradePolicy": { + "maxBatchInstancePercent": 20, + "maxSurge": false, + "maxUnhealthyInstancePercent": 20, + "maxUnhealthyUpgradedInstancePercent": 20, + "pauseTimeBetweenBatches": "PT0S", + "rollbackFailedInstancesOnPolicyBreach": false + } + }, + "virtualMachineProfile": { + "networkProfile": { + "networkInterfaceConfigurations": [ + { + "name": "mysca2132Nic", + "properties": { + "disableTcpStateTracking": false, + "dnsSettings": { + "dnsServers": [] + }, + "enableAcceleratedNetworking": false, + "enableIPForwarding": false, + "ipConfigurations": [ + { + "name": "mysca2132IPConfig", + "properties": { + "loadBalancerBackendAddressPools": [ + { + "id": "/subscriptions/f7a60fca-9977-4899-b907-005a076adbb6/resourceGroups/myResourceGroup/providers/Microsoft.Network/loadBalancers/myScaleSetLB/backendAddressPools/myScaleSetLBBEPool", + "resourceGroup": "myResourceGroup" + } + ], + "loadBalancerInboundNatPools": [ + { + "id": "/subscriptions/f7a60fca-9977-4899-b907-005a076adbb6/resourceGroups/myResourceGroup/providers/Microsoft.Network/loadBalancers/myScaleSetLB/inboundNatPools/myScaleSetLBNatPool", + "resourceGroup": "myResourceGroup" + } + ], + "privateIPAddressVersion": "IPv4", + "subnet": { + "id": "/subscriptions/f7a60fca-9977-4899-b907-005a076adbb6/resourceGroups/myResourceGroup/providers/Microsoft.Network/virtualNetworks/myScaleSetVNET/subnets/myScaleSetSubnet", + "resourceGroup": "myResourceGroup" + } + } + } + ], + "primary": true + } + } + ] + }, + "osProfile": { + "adminUsername": "azureuser", + "allowExtensionOperations": true, + "computerNamePrefix": "mysca2132", + "linuxConfiguration": { + "disablePasswordAuthentication": true, + "enableVMAgentPlatformUpdates": false, + "provisionVMAgent": true, + "ssh": { + "publicKeys": [ + { + "keyData": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCvR1+fGFuVMWS2bAY0SgW4E9QzLZ77ETdbCBUVF46eAyL8JWsLynX214hNSK16l4UYZyC3E6jea5qw2rGHPP4eMp7iif50xqd6qGICS428mqc9Gz29J0LFanM7XpHwLnBiJ6hmKvqvHB5tsGKh44MddW0wv+KiiEHIV1ZdSSvBRJ5MMQhqZoUiqlChHourOhaZxvw2dpJhRCvAEKw1s5RoeoLJAdZ6Qr53ERSkJr3BF7uAoNlGx6gatBVkjV+w9CZXN/YN62b1QQiGnk5/BIXNqEIsyxsa84+GbyieRIN/wYjSEV7ASRxSj60qV7RPexvAI+4JGa9UELYMQDrBElgL", + "path": "/home/azureuser/.ssh/authorized_keys" + } + ] + } + }, + "requireGuestProvisionSignal": true, + "secrets": [] + }, + "storageProfile": { + "imageReference": { + "offer": "UbuntuServer", + "publisher": "Canonical", + "sku": "18.04-LTS", + "version": "latest" + }, + "osDisk": { + "caching": "ReadWrite", + "createOption": "FromImage", + "diskSizeGB": 30, + "managedDisk": { + "storageAccountType": "Premium_LRS" + }, + "osType": "Linux" + } + } + } + } +} +``` + +## Allow traffic to application +When the scale set was created, an Azure load balancer was automatically deployed. The load balancer distributes traffic to the VM instances in the scale set. To allow traffic to reach the sample web application, create a load balancer rule with [az network lb rule create](/cli/azure/network/lb/rule). The following example creates a rule named *myLoadBalancerRuleWeb*: + +```azurecli-interactive +az network lb rule create \ + --resource-group $RESOURCE_GROUP_NAME \ + --name $LOAD_BALANCER_RULE_NAME \ + --lb-name $LOAD_BALANCER_NAME \ + --backend-pool-name $BACKEND_POOL_NAME \ + --backend-port 80 \ + --frontend-ip-name $FRONT_END_IP_NAME \ + --frontend-port 80 \ + --protocol tcp +``` + +## Test your scale set +To see your scale set in action, access the sample web application in a web browser. Obtain the public IP address of your load balancer with [az network public-ip show](/cli/azure/network/public-ip). The following example obtains the IP address for *myScaleSetLBPublicIP* created as part of the scale set: + +```azurecli-interactive +az network public-ip show \ + --resource-group $RESOURCE_GROUP_NAME \ + --name $SCALE_SET_PUBLIC_IP \ + --query '[ipAddress]' \ + --output tsv +``` + +Enter the public IP address of the load balancer in to a web browser. The load balancer distributes traffic to one of your VM instances, as shown in the following example: + +![Default web page in NGINX](media/virtual-machine-scale-sets-create-cli/running-nginx-site.png) + +Or run the following command in a local shell to validate the scale set is set up properly + +```bash + curl $(az network public-ip show --resource-group $RESOURCE_GROUP_NAME --name $SCALE_SET_PUBLIC_IP --query '[ipAddress]' --output tsv) +``` + + +```HTML +Hello World from host myscabd00000000 ! +``` + +## Clean up resources +When no longer needed, you can use [az group delete](/cli/azure/group) to remove the resource group, scale set, and all related resources as follows. The `--no-wait` parameter returns control to the prompt without waiting for the operation to complete. The `--yes` parameter confirms that you wish to delete the resources without an additional prompt to do so. + +```azurecli-interactive +az group delete --name $RESOURCE_GROUP_NAME --yes --no-wait +``` + + +## Next steps +In this quickstart, you created a basic scale set and used the Custom Script Extension to install a basic NGINX web server on the VM instances. To learn more, continue to the tutorial for how to create and manage Azure Virtual Machine Scale Sets. + +> [!div class="nextstepaction"] +> [Create and manage Azure Virtual Machine Scale Sets](tutorial-create-and-manage-cli.md) \ No newline at end of file From 9b53b0325722ee04cd8956520a602ad9998b0208 Mon Sep 17 00:00:00 2001 From: Ross Gardler Date: Tue, 14 Feb 2023 01:05:20 +0000 Subject: [PATCH 077/226] Initial review comments addressed ahead of testing the execution impact comments. --- demoScripts/vmssQuickstart.md | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/demoScripts/vmssQuickstart.md b/demoScripts/vmssQuickstart.md index f147428d..c95b7156 100644 --- a/demoScripts/vmssQuickstart.md +++ b/demoScripts/vmssQuickstart.md @@ -28,18 +28,22 @@ A Virtual Machine Scale Set allows you to deploy and manage a set of auto-scalin ## Define Environment Variables +Throughout this document we use environment variables to facilitate cut and paste reuse. +The default values below will enable you to work through this document in most cases. The meaning of each +environment variable will be addressed as they are used in the steps below. + ```azurecli-interactive -export RESOURCE_GROUP_NAME=myResourceGroup +export RESOURCE_GROUP_NAME=vmssQuickstartRG export RESOURCE_LOCATION=eastus -export SCALE_SET_NAME=myScaleSet +export SCALE_SET_NAME=vmssQuickstart export BASE_VM_IMAGE=UbuntuLTS export ADMIN_USERNAME=azureuser -export LOAD_BALANCER_NAME=myScaleSetLB -export BACKEND_POOL_NAME=myScaleSetLBBEPool -export LOAD_BALANCER_RULE_NAME=myLoadBalancerRuleWeb -export FRONT_END_IP_NAME=loadBalancerFrontEnd -export CUSTOM_SCRIPT_NAME=customScript -export SCALE_SET_PUBLIC_IP=myScaleSetLBPublicIP +export LOAD_BALANCER_NAME=vmssQuickstartLB +export BACKEND_POOL_NAME=vmssQuickstartPool +export LOAD_BALANCER_RULE_NAME=vmssQuickstartRule +export FRONT_END_IP_NAME=vmssQuickstartLoadBalancerFrontEnd +export CUSTOM_SCRIPT_NAME=vmssQuickstartCustomScript +export SCALE_SET_PUBLIC_IP=vmssQuickstartPublicIP ``` ## Create a scale set From a3633401f5bb017db1e0a665f2dd00dfa214cd62 Mon Sep 17 00:00:00 2001 From: Ross Gardler Date: Thu, 20 Jul 2023 00:03:56 +0000 Subject: [PATCH 078/226] Add next steps to each of the OCD docs --- scenarios/ocd/CreateAKSDeployment/README.md | 7 +++++++ scenarios/ocd/CreateCloudNativePythonContainer/README.md | 7 +++++++ scenarios/ocd/CreateLinuxVMAndSSH/README.md | 7 +++++++ scenarios/ocd/CreateLinuxVMSecureWebServer/README.md | 7 +++++++ 4 files changed, 28 insertions(+) diff --git a/scenarios/ocd/CreateAKSDeployment/README.md b/scenarios/ocd/CreateAKSDeployment/README.md index b3f8e805..2f5e9935 100644 --- a/scenarios/ocd/CreateAKSDeployment/README.md +++ b/scenarios/ocd/CreateAKSDeployment/README.md @@ -349,3 +349,10 @@ Run the following command to get the HTTPS endpoint for your application: echo https://$FQDN ``` Paste this into the browser to validate your deployment. + +## Next Steps + +* [Azure Kubernetes Service Documentation](https://learn.microsoft.com/en-us/azure/aks/) +* [Create an Azure Container Registry](https://learn.microsoft.com/en-us/azure/aks/tutorial-kubernetes-prepare-acr?tabs=azure-cli) +* [Scale your Applciation in AKS](https://learn.microsoft.com/en-us/azure/aks/tutorial-kubernetes-scale?tabs=azure-cli) +* [Update your application in AKS](https://learn.microsoft.com/en-us/azure/aks/tutorial-kubernetes-app-update?tabs=azure-cli) diff --git a/scenarios/ocd/CreateCloudNativePythonContainer/README.md b/scenarios/ocd/CreateCloudNativePythonContainer/README.md index a14847fa..3165fe57 100644 --- a/scenarios/ocd/CreateCloudNativePythonContainer/README.md +++ b/scenarios/ocd/CreateCloudNativePythonContainer/README.md @@ -134,3 +134,10 @@ az group delete --name $MY_RESOURCE_GROUP_NAME ## Notes - The sample in section 1 is originally from , which is also referenced in step 1 of the video walkthrough. The updated sample is at: . + +## Next Steps + +* [Azure Container Apps Documentation](https://learn.microsoft.com/en-us/azure/container-apps/) +* [Scaling an Azure Container App](https://learn.microsoft.com/en-us/azure/container-apps/scale-app?pivots=azure-cli) +* [Manage Secrets in Container Apps](https://learn.microsoft.com/en-us/azure/container-apps/manage-secrets?tabs=azure-cli) +* [Health Probes in Azure Container Apps](https://learn.microsoft.com/en-us/azure/container-apps/health-probes?tabs=arm-cli) \ No newline at end of file diff --git a/scenarios/ocd/CreateLinuxVMAndSSH/README.md b/scenarios/ocd/CreateLinuxVMAndSSH/README.md index 67136eff..9e574f06 100644 --- a/scenarios/ocd/CreateLinuxVMAndSSH/README.md +++ b/scenarios/ocd/CreateLinuxVMAndSSH/README.md @@ -102,3 +102,10 @@ You can now SSH into the VM by running the output of the following command in yo ```bash echo az ssh vm --name $MY_VM_NAME --resource-group $MY_RESOURCE_GROUP_NAME ``` + +# Next Steps + +* [VM Documentation](https://learn.microsoft.com/en-us/azure/virtual-machines/) +* [Use Cloud-Init to initialize a Linux VM on first boot](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/tutorial-automate-vm-deployment) +* [Create custom VM images](https://learn.microsoft.com/en-us/azure/virtual-machines/linux/tutorial-custom-images) +* [Load Balance VMs](https://learn.microsoft.com/en-us/azure/load-balancer/quickstart-load-balancer-standard-public-cli) \ No newline at end of file diff --git a/scenarios/ocd/CreateLinuxVMSecureWebServer/README.md b/scenarios/ocd/CreateLinuxVMSecureWebServer/README.md index 5b848c71..8f09b2ad 100644 --- a/scenarios/ocd/CreateLinuxVMSecureWebServer/README.md +++ b/scenarios/ocd/CreateLinuxVMSecureWebServer/README.md @@ -161,3 +161,10 @@ az network front-door show --name $CUSTOM_DOMAIN_NAME --resource-group $RESOURCE ## Conclusion You have completed the tutorial! View your resources on portal.azure.com + +# Next Steps + +* [VM Documentation](https://learn.microsoft.com/en-us/azure/virtual-machines/) +* [Create Vm Scale Set](https://learn.microsoft.com/en-us/azure/virtual-machine-scale-sets/flexible-virtual-machine-scale-sets-cli) +* [Load Balance VMs](https://learn.microsoft.com/en-us/azure/load-balancer/quickstart-load-balancer-standard-public-cli) +* [Baclup VMs](https://learn.microsoft.com/en-us/azure/virtual-machines/backup-recovery) \ No newline at end of file From 70ebc6401d719a046db20d1a93964368c93f3748 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 20 Jul 2023 14:14:37 -0700 Subject: [PATCH 079/226] [update] error handling. --- internal/engine/execution.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 54bf3a18..e546056b 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -84,14 +84,14 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { // rendered while the command is executing. done := make(chan error) var commandOutput shells.CommandOutput + var err error + go func(block parsers.CodeBlock) { - output, err := shells.ExecuteBashCommand(block.Content, utils.CopyMap(env), true) - commandOutput = output + commandOutput, err = shells.ExecuteBashCommand(block.Content, utils.CopyMap(env), true) done <- err }(block) frame := 0 - var err error loop: // While the command is executing, render the spinner. From 5f275bbf73f273a20ae0447c56b70a5c0e4041eb Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 20 Jul 2023 14:16:10 -0700 Subject: [PATCH 080/226] [update] error handling & add trailing newline. --- internal/engine/engine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 5bfd139f..b6a97676 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -33,7 +33,7 @@ func NewEngine(configuration EngineConfiguration) *Engine { func (e *Engine) ExecuteScenario(scenario *Scenario) error { fmt.Println(titleStyle.Render(scenario.Name)) e.ExecuteAndRenderSteps(scenario.Steps, utils.CopyMap(scenario.Environment)) - fmt.Printf(scriptHeader.Render("# Generated bash to replicate what just happened:")+"\n%s", scriptText.Render(scenario.ToShellScript())) + fmt.Printf(scriptHeader.Render("# Generated bash to replicate what just happened:")+"\n%s\n", scriptText.Render(scenario.ToShellScript())) return nil } From 52e1c7374220b14b645f9e69bb5f76e680d2780e Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 20 Jul 2023 14:24:12 -0700 Subject: [PATCH 081/226] [add] logging to command execution for now. --- internal/engine/execution.go | 11 +++++------ internal/shells/bash.go | 5 +++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index e546056b..960e63c7 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -84,24 +84,23 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { // rendered while the command is executing. done := make(chan error) var commandOutput shells.CommandOutput - var err error go func(block parsers.CodeBlock) { - commandOutput, err = shells.ExecuteBashCommand(block.Content, utils.CopyMap(env), true) + output, err := shells.ExecuteBashCommand(block.Content, utils.CopyMap(env), true) + commandOutput = output done <- err }(block) - frame := 0 - + var commandErr error loop: // While the command is executing, render the spinner. for { select { - case err = <-done: + case commandErr = <-done: // Show the cursor, check the result of the command, and display the // final status. fmt.Print("\033[?25h") - if err == nil { + if commandErr == nil { fmt.Printf("\r %s \n", checkStyle.Render("✔")) fmt.Printf("\033[%dB\n", lines) if e.Configuration.Verbose { diff --git a/internal/shells/bash.go b/internal/shells/bash.go index 7907b9d2..28ca41fb 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -87,6 +87,11 @@ func ExecuteBashCommand(command string, env map[string]string, inherit_environme // Execute command, handle errors, and return output. err = commandToExecute.Run() standardOutput, standardError := stdout.String(), stderr.String() + + fmt.Println("Standard: ", standardOutput) + fmt.Println("Error: ", standardError) + fmt.Println("Error: ", err) + if err != nil { return CommandOutput{}, fmt.Errorf("command exited with '%w' and the message '%s'", err, standardError) } From 57b56fed1eba4e730286257f2dfd4ca55c25a7a4 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 20 Jul 2023 14:27:50 -0700 Subject: [PATCH 082/226] [fix] frame logic. --- internal/engine/execution.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 960e63c7..6cfd6669 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -92,6 +92,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { }(block) var commandErr error + var frame int loop: // While the command is executing, render the spinner. for { @@ -109,7 +110,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { } else { fmt.Printf("\r %s \n", errorStyle.Render("✗")) fmt.Printf("\033[%dB", lines) - fmt.Printf(" %s\n", errorMessageStyle.Render(err.Error())) + fmt.Printf(" %s\n", errorMessageStyle.Render(commandErr.Error())) } break loop From 130297e324c4e9c2d2cf45a21a0d3dfe1986c143 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 20 Jul 2023 14:28:44 -0700 Subject: [PATCH 083/226] [fix] frame logic. --- internal/engine/execution.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 6cfd6669..3d8fd208 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -92,7 +92,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { }(block) var commandErr error - var frame int + var frame int = 0 loop: // While the command is executing, render the spinner. for { From 130e7b3389a8e0e8a00ca8a57ae3e803aacf5d9f Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sun, 23 Jul 2023 14:42:06 -0700 Subject: [PATCH 084/226] [update] dependencies. --- go.mod | 2 ++ go.sum | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/go.mod b/go.mod index 5eb19c5e..685a36ae 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/creack/pty v1.1.18 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/go-logr/logr v1.2.3 // indirect @@ -28,6 +29,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/kr/pty v1.1.8 // indirect github.com/labstack/echo/v4 v4.10.2 // indirect github.com/labstack/gommon v0.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect diff --git a/go.sum b/go.sum index 72c076b4..a186dc38 100644 --- a/go.sum +++ b/go.sum @@ -44,7 +44,11 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -149,6 +153,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= +github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M= From 473cb78d5150c792df74c61e72e30f9595487728 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sun, 23 Jul 2023 14:42:44 -0700 Subject: [PATCH 085/226] [add] support for SSH commands, fix error handling for CLI commands, and change execution logic for processes that require stdin/stdout. --- internal/engine/execution.go | 115 +++++++++++++++++++++++------------ internal/engine/testing.go | 8 ++- internal/shells/bash.go | 37 ++++++++--- 3 files changed, 111 insertions(+), 49 deletions(-) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 3d8fd208..d6978a9b 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -16,13 +16,14 @@ const ( // TODO - Make this configurable for terminals that support it. // spinnerFrames = `⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏` spinnerFrames = `-\|/` - spinnerLength = 1 spinnerRefresh = 100 * time.Millisecond ) var azGroupDelete = regexp.MustCompile(`az group delete`) +var azCommand = regexp.MustCompile(`az\s+([a-z]+)\s+([a-z]+)`) +var sshCommand = regexp.MustCompile(`ssh\s+([a-z]+)`) -// If a scenario has an `az group delete` command and the `--preserve-resources` +// If a scenario has an `az group delete` command and the `--do-not-delete` // flag is set, we remove it from the steps. func filterDeletionCommands(steps []Step, preserveResources bool) []Step { filteredSteps := []Step{} @@ -49,6 +50,20 @@ func filterDeletionCommands(steps []Step, preserveResources bool) []Step { return filteredSteps } +// Check for errors from the Azure CLI. The Azure CLI doesn't return a non-zero +// exit code when an error occurs, so we have to check the output for errors. +func checkForAzCLIError(command string, output shells.CommandOutput) bool { + if !azCommand.MatchString(command) { + return false + } + + if output.StdOut == "" && output.StdErr != "" { + return true + } + + return false +} + // Executes the steps from a scenario and renders the output to the terminal. func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { @@ -71,54 +86,76 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { indentedBlock := indentMultiLineCommand(block.Content, 4) fmt.Print(" " + indentedBlock) - // Grab the number of lines it contains & set the cursor to the - // beginning of the block. - lines := strings.Count(block.Content, "\n") - fmt.Printf("\033[%dA", lines) - - // Render the spinner and hide the cursor. - fmt.Print(spinnerStyle.Render(" "+string(spinnerFrames[0])) + " ") - fmt.Print("\033[?25l") - // execute the command as a goroutine to allow for the spinner to be // rendered while the command is executing. done := make(chan error) var commandOutput shells.CommandOutput - go func(block parsers.CodeBlock) { - output, err := shells.ExecuteBashCommand(block.Content, utils.CopyMap(env), true) - commandOutput = output - done <- err - }(block) + // If the command is an SSH command, we need to forward the input and + // output + forward_input_output := false + if sshCommand.MatchString(block.Content) { + forward_input_output = true + } + + logging.GlobalLogger.WithField("forward_intput_output", forward_input_output).Info("Executing command: " + block.Content) var commandErr error var frame int = 0 - loop: - // While the command is executing, render the spinner. - for { - select { - case commandErr = <-done: - // Show the cursor, check the result of the command, and display the - // final status. - fmt.Print("\033[?25h") - if commandErr == nil { - fmt.Printf("\r %s \n", checkStyle.Render("✔")) - fmt.Printf("\033[%dB\n", lines) - if e.Configuration.Verbose { - fmt.Printf(" %s\n", verboseStyle.Render(commandOutput.StdOut)) + + // If forwarding input/output, don't render the spinner. + if !forward_input_output { + // Grab the number of lines it contains & set the cursor to the + // beginning of the block. + lines := strings.Count(block.Content, "\n") + fmt.Printf("\033[%dA", lines) + + // Render the spinner and hide the cursor. + fmt.Print(spinnerStyle.Render(" "+string(spinnerFrames[0])) + " ") + fmt.Print("\033[?25l") + + go func(block parsers.CodeBlock) { + output, err := shells.ExecuteBashCommand(block.Content, utils.CopyMap(env), true, forward_input_output) + commandOutput = output + done <- err + }(block) + loop: + // While the command is executing, render the spinner. + for { + select { + case commandErr = <-done: + // Show the cursor, check the result of the command, and display the + // final status. + fmt.Print("\033[?25h") + + if checkForAzCLIError(block.Content, commandOutput) { + commandErr = fmt.Errorf(commandOutput.StdErr) } - } else { - fmt.Printf("\r %s \n", errorStyle.Render("✗")) - fmt.Printf("\033[%dB", lines) - fmt.Printf(" %s\n", errorMessageStyle.Render(commandErr.Error())) - } - break loop - default: - frame = (frame + 1) % len(spinnerFrames) - fmt.Printf("\r %s", spinnerStyle.Render(string(spinnerFrames[frame]))) - time.Sleep(spinnerRefresh) + if commandErr == nil { + fmt.Printf("\r %s \n", checkStyle.Render("✔")) + + fmt.Printf("\033[%dB\n", lines) + if e.Configuration.Verbose { + fmt.Printf(" %s\n", verboseStyle.Render(commandOutput.StdOut)) + } + } else { + fmt.Printf("\r %s \n", errorStyle.Render("✗")) + fmt.Printf("\033[%dB", lines) + fmt.Printf(" %s\n", errorMessageStyle.Render(commandErr.Error())) + } + + break loop + default: + frame = (frame + 1) % len(spinnerFrames) + fmt.Printf("\r %s", spinnerStyle.Render(string(spinnerFrames[frame]))) + time.Sleep(spinnerRefresh) + } } + } else { + func(block parsers.CodeBlock) { + shells.ExecuteBashCommand(block.Content, utils.CopyMap(env), true, forward_input_output) + }(block) } } } diff --git a/internal/engine/testing.go b/internal/engine/testing.go index 0f960188..56a82c6e 100644 --- a/internal/engine/testing.go +++ b/internal/engine/testing.go @@ -34,7 +34,7 @@ func (e *Engine) TestSteps(steps []Step, env map[string]string) { done := make(chan error) var commandOutput shells.CommandOutput go func(block parsers.CodeBlock) { - output, err := shells.ExecuteBashCommand(block.Content, utils.CopyMap(env), true) + output, err := shells.ExecuteBashCommand(block.Content, utils.CopyMap(env), true, false) commandOutput = output done <- err }(block) @@ -50,6 +50,12 @@ func (e *Engine) TestSteps(steps []Step, env map[string]string) { // Show the cursor, check the result of the command, and display the // final status. fmt.Print("\033[?25h") + + // Handle the case where the command is an az cli command. + if checkForAzCLIError(block.Content, commandOutput) { + err = fmt.Errorf(commandOutput.StdErr) + } + if err == nil { actualOutput := commandOutput.StdOut expectedOutput := block.ExpectedOutput.Content diff --git a/internal/shells/bash.go b/internal/shells/bash.go index 28ca41fb..7f015c84 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -4,10 +4,14 @@ import ( "bufio" "bytes" "fmt" + // "io" "os" "os/exec" "strings" + // "github.com/creack/pty" + + "github.com/Azure/InnovationEngine/internal/logging" "github.com/Azure/InnovationEngine/internal/utils" ) @@ -51,17 +55,30 @@ type CommandOutput struct { } // Executes a bash command and returns the output or error. -func ExecuteBashCommand(command string, env map[string]string, inherit_environment_variables bool) (CommandOutput, error) { +func ExecuteBashCommand(command string, env map[string]string, inherit_environment_variables bool, forward_input_output bool) (CommandOutput, error) { var commandWithStateSaved = []string{ command, "env > /tmp/env.txt", } - commandToExecute := exec.Command("bash", "-c", strings.Join(commandWithStateSaved, "\n")) + var commandToExecute *exec.Cmd + + if !forward_input_output { + commandToExecute = exec.Command("bash", "-c", strings.Join(commandWithStateSaved, "\n")) + } else { + commandToExecute = exec.Command("bash", "-c", command) + } - // Capture std out and std err as separate buffers. var stdout, stderr bytes.Buffer - commandToExecute.Stdout = &stdout - commandToExecute.Stderr = &stderr + + if !forward_input_output { + // Capture std out and std err as separate buffers. + commandToExecute.Stdout = &stdout + commandToExecute.Stderr = &stderr + } else { + commandToExecute.Stdout = os.Stdout + commandToExecute.Stderr = os.Stderr + commandToExecute.Stdin = os.Stdin + } if inherit_environment_variables { commandToExecute.Env = os.Environ() @@ -84,13 +101,15 @@ func ExecuteBashCommand(command string, env map[string]string, inherit_environme } } + logging.GlobalLogger.Infof("Environment variables: %v", commandToExecute.Env) // Execute command, handle errors, and return output. + err = commandToExecute.Run() - standardOutput, standardError := stdout.String(), stderr.String() + if forward_input_output { + return CommandOutput{}, err + } - fmt.Println("Standard: ", standardOutput) - fmt.Println("Error: ", standardError) - fmt.Println("Error: ", err) + standardOutput, standardError := stdout.String(), stderr.String() if err != nil { return CommandOutput{}, fmt.Errorf("command exited with '%w' and the message '%s'", err, standardError) From fcec7fbe73a4be0ff7ce441cd4b4757ac0b61aba Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sun, 23 Jul 2023 14:43:34 -0700 Subject: [PATCH 086/226] [update] scenario to run ssh and not just output the SSH command. --- scenarios/ocd/CreateLinuxVMAndSSH/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenarios/ocd/CreateLinuxVMAndSSH/README.md b/scenarios/ocd/CreateLinuxVMAndSSH/README.md index 67136eff..94913706 100644 --- a/scenarios/ocd/CreateLinuxVMAndSSH/README.md +++ b/scenarios/ocd/CreateLinuxVMAndSSH/README.md @@ -100,5 +100,5 @@ az role assignment create --role "Virtual Machine Administrator Login" --assigne You can now SSH into the VM by running the output of the following command in your ssh client of choice ```bash -echo az ssh vm --name $MY_VM_NAME --resource-group $MY_RESOURCE_GROUP_NAME +az ssh vm --name $MY_VM_NAME --resource-group $MY_RESOURCE_GROUP_NAME ``` From 5d127bd748c79d1bc5e403f1bf7e44b09ac7f00e Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sun, 23 Jul 2023 14:50:19 -0700 Subject: [PATCH 087/226] [refactor] execution. --- internal/shells/bash.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/shells/bash.go b/internal/shells/bash.go index 7f015c84..a1304fbc 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -68,16 +68,16 @@ func ExecuteBashCommand(command string, env map[string]string, inherit_environme commandToExecute = exec.Command("bash", "-c", command) } - var stdout, stderr bytes.Buffer + var stdoutBuffer, stderrBuffer bytes.Buffer - if !forward_input_output { - // Capture std out and std err as separate buffers. - commandToExecute.Stdout = &stdout - commandToExecute.Stderr = &stderr - } else { + if forward_input_output { commandToExecute.Stdout = os.Stdout commandToExecute.Stderr = os.Stderr commandToExecute.Stdin = os.Stdin + } else { + // Capture std out and std err as separate buffers. + commandToExecute.Stdout = &stdoutBuffer + commandToExecute.Stderr = &stderrBuffer } if inherit_environment_variables { @@ -101,7 +101,7 @@ func ExecuteBashCommand(command string, env map[string]string, inherit_environme } } - logging.GlobalLogger.Infof("Environment variables: %v", commandToExecute.Env) + logging.GlobalLogger.Tracef("Environment variables: %v", commandToExecute.Env) // Execute command, handle errors, and return output. err = commandToExecute.Run() @@ -109,7 +109,7 @@ func ExecuteBashCommand(command string, env map[string]string, inherit_environme return CommandOutput{}, err } - standardOutput, standardError := stdout.String(), stderr.String() + standardOutput, standardError := stdoutBuffer.String(), stderrBuffer.String() if err != nil { return CommandOutput{}, fmt.Errorf("command exited with '%w' and the message '%s'", err, standardError) From b056c3ba409e28fcc70ca71cc57c1bcecdd2fe79 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sun, 23 Jul 2023 15:00:25 -0700 Subject: [PATCH 088/226] [refactor] command to execute setup. --- internal/shells/bash.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/internal/shells/bash.go b/internal/shells/bash.go index a1304fbc..d42ad2da 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -60,13 +60,7 @@ func ExecuteBashCommand(command string, env map[string]string, inherit_environme command, "env > /tmp/env.txt", } - var commandToExecute *exec.Cmd - - if !forward_input_output { - commandToExecute = exec.Command("bash", "-c", strings.Join(commandWithStateSaved, "\n")) - } else { - commandToExecute = exec.Command("bash", "-c", command) - } + commandToExecute := exec.Command("bash", "-c", strings.Join(commandWithStateSaved, "\n")) var stdoutBuffer, stderrBuffer bytes.Buffer From 48ba95f26c609c19e0486cc4c67fca0417791cc4 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 24 Jul 2023 11:25:18 -0700 Subject: [PATCH 089/226] [update] the scenario to use ssh without aad & fix the ssh regex. --- internal/engine/execution.go | 2 +- scenarios/ocd/CreateLinuxVMAndSSH/README.md | 41 ++++----------------- 2 files changed, 8 insertions(+), 35 deletions(-) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index d6978a9b..9b6e5c8c 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -21,7 +21,7 @@ const ( var azGroupDelete = regexp.MustCompile(`az group delete`) var azCommand = regexp.MustCompile(`az\s+([a-z]+)\s+([a-z]+)`) -var sshCommand = regexp.MustCompile(`ssh\s+([a-z]+)`) +var sshCommand = regexp.MustCompile(`(^|\s)\bssh\b\s`) // If a scenario has an `az group delete` command and the `--do-not-delete` // flag is set, we remove it from the steps. diff --git a/scenarios/ocd/CreateLinuxVMAndSSH/README.md b/scenarios/ocd/CreateLinuxVMAndSSH/README.md index 94913706..685bc9ac 100644 --- a/scenarios/ocd/CreateLinuxVMAndSSH/README.md +++ b/scenarios/ocd/CreateLinuxVMAndSSH/README.md @@ -26,8 +26,7 @@ az group create --name $MY_RESOURCE_GROUP_NAME --location $MY_LOCATION Results: -```json expected-similarity=0.7 -{ +```expected_similarity=0.3 "id": "/subscriptions/325e7c34-99fb-4190-aa87-1df746c67705/resourceGroups/myResourceGroup", "location": "eastus", "managedBy": null, @@ -37,7 +36,6 @@ Results: }, "tags": null, "type": "Microsoft.Resources/resourceGroups" -} ``` ## Create the Virtual Machine @@ -47,13 +45,12 @@ To create a VM in this resource group we need to run a simple command, here we h All other values are configured using environment variables. ```bash -az vm create --resource-group $MY_RESOURCE_GROUP_NAME --name $MY_VM_NAME --image $MY_VM_IMAGE --assign-identity --admin-username $MY_USERNAME --generate-ssh-keys --public-ip-sku Standard +az vm create --resource-group $MY_RESOURCE_GROUP_NAME --name $MY_VM_NAME --image $MY_VM_IMAGE --admin-username $MY_USERNAME --generate-ssh-keys --public-ip-sku Standard ``` Results: -```json expected-similarity=0.7 -{ +```expected_similarity=0.3 "fqdns": "", "id": "/subscriptions/325e7c34-99fb-4190-aa87-1df746c67705/resourceGroups/myResourceGroup/providers/Microsoft.Compute/virtualMachines/myVM", "location": "eastus", @@ -63,42 +60,18 @@ Results: "publicIpAddress": "52.147.208.85", "resourceGroup": "myResourceGroup", "zones": "" -} ``` -## Add VM AAD Extension - -In order to use Azure AD Login for a Linux VM an extension needs to be installed on the VM. VM extensions are small applications that provide post-deployment configuration and automation tasks on Azure virtual machines - -The following installs the extension to enable Azure AD Login on the recently deployed VM +# Store IP Address of VM in order to SSH +run the following command to get the IP Address of the VM and store it as an environment variable ```bash -az vm extension set --publisher Microsoft.Azure.ActiveDirectory --name AADSSHLoginForLinux --resource-group $MY_RESOURCE_GROUP_NAME --vm-name $MY_VM_NAME +export IP_ADDRESS=$(az vm show --show-details --resource-group $MY_RESOURCE_GROUP_NAME --name $MY_VM_NAME --query publicIps --output tsv) ``` -## Configure role assignments for the VM - -Now that you've created the VM, you need to configure an Azure RBAC policy to determine who can log in to the VM. Two Azure roles are used to authorize VM login: - -Virtual Machine Administrator Login: Users who have this role assigned can log in to an Azure virtual machine with administrator privileges. -Virtual Machine User Login: Users who have this role assigned can log in to an Azure virtual machine with regular user privileges. -To allow a user to log in to a VM over SSH, you must assign the Virtual Machine Administrator Login or Virtual Machine User Login role on the resource group that contains the VM and its associated virtual network, network interface, public IP address, or load balancer resources. - -An Azure user who has the Owner or Contributor role assigned for a VM doesn't automatically have privileges to Azure AD login to the VM over SSH. There's an intentional (and audited) separation between the set of people who control virtual machines and the set of people who can access virtual machines. - -The following example uses az role assignment create to assign the Virtual Machine Administrator Login role to the VM for your current Azure user. You obtain the username of your current Azure account by using az account show, and you set the scope to the VM created in a previous step by using az vm show. - -You can also assign the scope at a resource group or subscription level. Normal Azure RBAC inheritance permissions apply - -```bash -USERNAME=$(az account show --query user.name --output tsv) -RESOURCE_GROUP_ID=$(az group show --resource-group $MY_RESOURCE_GROUP_NAME --query id -o tsv) -az role assignment create --role "Virtual Machine Administrator Login" --assignee $USERNAME --scope $RESOURCE_GROUP_ID -``` - # SSH Into VM You can now SSH into the VM by running the output of the following command in your ssh client of choice ```bash -az ssh vm --name $MY_VM_NAME --resource-group $MY_RESOURCE_GROUP_NAME +ssh $MY_USERNAME@$IP_ADDRESS ``` From 5b777bb56aea9ea830efeb32992f97bf54347143 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 25 Jul 2023 10:50:07 -0700 Subject: [PATCH 090/226] [update] scenario to not check the host key. --- scenarios/ocd/CreateLinuxVMAndSSH/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenarios/ocd/CreateLinuxVMAndSSH/README.md b/scenarios/ocd/CreateLinuxVMAndSSH/README.md index 685bc9ac..57629ac6 100644 --- a/scenarios/ocd/CreateLinuxVMAndSSH/README.md +++ b/scenarios/ocd/CreateLinuxVMAndSSH/README.md @@ -73,5 +73,5 @@ export IP_ADDRESS=$(az vm show --show-details --resource-group $MY_RESOURCE_GROU You can now SSH into the VM by running the output of the following command in your ssh client of choice ```bash -ssh $MY_USERNAME@$IP_ADDRESS +ssh -o StrictHostKeyChecking=no $MY_USERNAME@$IP_ADDRESS ``` From 42e23f92ec793317d360e81fe7b498082e52bc5d Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 31 Jul 2023 11:30:24 -0700 Subject: [PATCH 091/226] [add] the ability to set the subscription and correlation IDs from the CLI. --- cmd/ie/commands/execute.go | 17 ++++++++++++----- cmd/ie/commands/test.go | 9 ++++++--- internal/engine/engine.go | 17 ++++++++++++++--- internal/engine/execution.go | 11 +++++------ 4 files changed, 37 insertions(+), 17 deletions(-) diff --git a/cmd/ie/commands/execute.go b/cmd/ie/commands/execute.go index 2a64389a..4c861a95 100644 --- a/cmd/ie/commands/execute.go +++ b/cmd/ie/commands/execute.go @@ -9,9 +9,13 @@ import ( func init() { rootCommand.AddCommand(executeCommand) + // Bool flags executeCommand.PersistentFlags().Bool("verbose", false, "Enable verbose logging & standard output.") - executeCommand.PersistentFlags().Bool("tracking", false, "Enable tracking for Azure resources created by the Azure CLI commands executed.") executeCommand.PersistentFlags().Bool("do-not-delete", false, "Do not delete the Azure resources created by the Azure CLI commands executed.") + + // String flags + executeCommand.PersistentFlags().String("correlation-id", "", "Adds a correlation ID to the user agent used by a scenarios azure-cli commands.") + executeCommand.PersistentFlags().String("subscription", "", "Sets the subscription ID used by a scenarios azure-cli commands. Will rely on the default subscription if not set.") } var executeCommand = &cobra.Command{ @@ -26,14 +30,17 @@ var executeCommand = &cobra.Command{ } verbose, _ := cmd.Flags().GetBool("verbose") - tracking, _ := cmd.Flags().GetBool("tracking") do_not_delete, _ := cmd.Flags().GetBool("do-not-delete") + subscription, _ := cmd.Flags().GetString("subscription") + correlation_id, _ := cmd.Flags().GetString("correlation-id") innovationEngine := engine.NewEngine(engine.EngineConfiguration{ - Verbose: verbose, - ResourceTracking: tracking, - DoNotDelete: do_not_delete, + Verbose: verbose, + DoNotDelete: do_not_delete, + Subscription: subscription, + CorrelationId: correlation_id, }) + scenario, err := engine.CreateScenarioFromMarkdown(markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}) if err != nil { panic(err) diff --git a/cmd/ie/commands/test.go b/cmd/ie/commands/test.go index 3b021e89..532bb700 100644 --- a/cmd/ie/commands/test.go +++ b/cmd/ie/commands/test.go @@ -9,6 +9,7 @@ import ( func init() { rootCommand.AddCommand(testCommand) testCommand.PersistentFlags().Bool("verbose", false, "Enable verbose logging & standard output.") + testCommand.PersistentFlags().String("subscription", "", "Sets the subscription ID used by a scenarios azure-cli commands. Will rely on the default subscription if not set.") } var testCommand = &cobra.Command{ @@ -24,11 +25,13 @@ var testCommand = &cobra.Command{ } verbose, _ := cmd.Flags().GetBool("verbose") + subscription, _ := cmd.Flags().GetString("subscription") innovationEngine := engine.NewEngine(engine.EngineConfiguration{ - Verbose: verbose, - ResourceTracking: false, - DoNotDelete: false, + Verbose: verbose, + DoNotDelete: false, + Subscription: subscription, + CorrelationId: "", }) scenario, err := engine.CreateScenarioFromMarkdown(markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index b6a97676..594e0c53 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -3,6 +3,7 @@ package engine import ( "fmt" + "github.com/Azure/InnovationEngine/internal/shells" "github.com/Azure/InnovationEngine/internal/utils" "github.com/charmbracelet/lipgloss" ) @@ -13,9 +14,10 @@ var ( ) type EngineConfiguration struct { - Verbose bool - ResourceTracking bool - DoNotDelete bool + Verbose bool + DoNotDelete bool + CorrelationId string + Subscription string } type Engine struct { @@ -31,6 +33,15 @@ func NewEngine(configuration EngineConfiguration) *Engine { // Executes a deployment scenario. func (e *Engine) ExecuteScenario(scenario *Scenario) error { + + if e.Configuration.Subscription != "" { + command := fmt.Sprintf("az account set --subscription %s", e.Configuration.Subscription) + _, err := shells.ExecuteBashCommand(command, map[string]string{}, true, false) + if err != nil { + return err + } + } + fmt.Println(titleStyle.Render(scenario.Name)) e.ExecuteAndRenderSteps(scenario.Steps, utils.CopyMap(scenario.Environment)) fmt.Printf(scriptHeader.Render("# Generated bash to replicate what just happened:")+"\n%s\n", scriptText.Render(scenario.ToShellScript())) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 9b6e5c8c..f46f3d08 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -67,14 +67,13 @@ func checkForAzCLIError(command string, output shells.CommandOutput) bool { // Executes the steps from a scenario and renders the output to the terminal. func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { - // Enable resource tracking for all of the deployments performed by the - // innovation engine. - if e.Configuration.ResourceTracking { - tracking_id := "6edbe7b9-4e03-4ab0-8213-230ba21aeaba" - env["AZURE_HTTP_USER_AGENT"] = fmt.Sprintf("pid-%s", tracking_id) + // If the correlation ID is set, we need to set the AZURE_HTTP_USER_AGENT + // environment variable so that the Azure CLI will send the correlation ID + // with Azure Resource Manager requests. + if e.Configuration.CorrelationId != "" { + env["AZURE_HTTP_USER_AGENT"] = fmt.Sprintf("innovation-engine-%s", e.Configuration.CorrelationId) if e.Configuration.Verbose { logging.GlobalLogger.Info("Resource tracking enabled. Tracking ID: " + env["AZURE_HTTP_USER_AGENT"]) - fmt.Println("Resource tracking enabled. Tracking ID: " + env["AZURE_HTTP_USER_AGENT"]) } } From 544f44e901156c064d3f8646181199242c79e02e Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 31 Jul 2023 11:37:43 -0700 Subject: [PATCH 092/226] [update] logging for setting the subscription. --- internal/engine/engine.go | 13 +++++++++++++ internal/engine/execution.go | 4 +--- internal/shells/bash.go | 7 ------- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 594e0c53..93066eba 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -3,6 +3,7 @@ package engine import ( "fmt" + "github.com/Azure/InnovationEngine/internal/logging" "github.com/Azure/InnovationEngine/internal/shells" "github.com/Azure/InnovationEngine/internal/utils" "github.com/charmbracelet/lipgloss" @@ -38,8 +39,10 @@ func (e *Engine) ExecuteScenario(scenario *Scenario) error { command := fmt.Sprintf("az account set --subscription %s", e.Configuration.Subscription) _, err := shells.ExecuteBashCommand(command, map[string]string{}, true, false) if err != nil { + logging.GlobalLogger.Error("Failed to set subscription", err) return err } + logging.GlobalLogger.Infof("Set subscription to %s", e.Configuration.Subscription) } fmt.Println(titleStyle.Render(scenario.Name)) @@ -50,6 +53,16 @@ func (e *Engine) ExecuteScenario(scenario *Scenario) error { // Validates a deployment scenario. func (e *Engine) TestScenario(scenario *Scenario) error { + if e.Configuration.Subscription != "" { + command := fmt.Sprintf("az account set --subscription %s", e.Configuration.Subscription) + _, err := shells.ExecuteBashCommand(command, map[string]string{}, true, false) + if err != nil { + logging.GlobalLogger.Error("Failed to set subscription", err) + return err + } + logging.GlobalLogger.Infof("Set subscription to %s", e.Configuration.Subscription) + } + fmt.Println(titleStyle.Render(scenario.Name)) e.TestSteps(scenario.Steps, utils.CopyMap(scenario.Environment)) return nil diff --git a/internal/engine/execution.go b/internal/engine/execution.go index f46f3d08..b64ae8c2 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -72,9 +72,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { // with Azure Resource Manager requests. if e.Configuration.CorrelationId != "" { env["AZURE_HTTP_USER_AGENT"] = fmt.Sprintf("innovation-engine-%s", e.Configuration.CorrelationId) - if e.Configuration.Verbose { - logging.GlobalLogger.Info("Resource tracking enabled. Tracking ID: " + env["AZURE_HTTP_USER_AGENT"]) - } + logging.GlobalLogger.Info("Resource tracking enabled. Tracking ID: " + env["AZURE_HTTP_USER_AGENT"]) } stepsToExecute := filterDeletionCommands(steps, e.Configuration.DoNotDelete) diff --git a/internal/shells/bash.go b/internal/shells/bash.go index d42ad2da..a5d2d9af 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -4,14 +4,10 @@ import ( "bufio" "bytes" "fmt" - // "io" "os" "os/exec" "strings" - // "github.com/creack/pty" - - "github.com/Azure/InnovationEngine/internal/logging" "github.com/Azure/InnovationEngine/internal/utils" ) @@ -95,9 +91,6 @@ func ExecuteBashCommand(command string, env map[string]string, inherit_environme } } - logging.GlobalLogger.Tracef("Environment variables: %v", commandToExecute.Env) - // Execute command, handle errors, and return output. - err = commandToExecute.Run() if forward_input_output { return CommandOutput{}, err From 6a128644dcbc728c01fbb0eb11946856f604c478 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 31 Jul 2023 11:39:50 -0700 Subject: [PATCH 093/226] [update] the default log level. --- cmd/ie/commands/root.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/ie/commands/root.go b/cmd/ie/commands/root.go index 691d3ddf..8f1998a2 100644 --- a/cmd/ie/commands/root.go +++ b/cmd/ie/commands/root.go @@ -24,7 +24,7 @@ var rootCommand = &cobra.Command{ // Entrypoint into the Innovation Engine CLI. func ExecuteCLI() { - rootCommand.PersistentFlags().String("log-level", string(logging.Error), "Configure the log level") + rootCommand.PersistentFlags().String("log-level", string(logging.Debug), "Configure the log level") if err := rootCommand.Execute(); err != nil { fmt.Println(err) From 05ab40f242e56d7187fbaa2abe9c2bee6cc7ae2d Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 31 Jul 2023 11:49:20 -0700 Subject: [PATCH 094/226] [update] variable names to be more consistent. --- cmd/ie/commands/execute.go | 8 ++++---- internal/engine/execution.go | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cmd/ie/commands/execute.go b/cmd/ie/commands/execute.go index 4c861a95..dbcd1f60 100644 --- a/cmd/ie/commands/execute.go +++ b/cmd/ie/commands/execute.go @@ -30,15 +30,15 @@ var executeCommand = &cobra.Command{ } verbose, _ := cmd.Flags().GetBool("verbose") - do_not_delete, _ := cmd.Flags().GetBool("do-not-delete") + doNotDelete, _ := cmd.Flags().GetBool("do-not-delete") subscription, _ := cmd.Flags().GetString("subscription") - correlation_id, _ := cmd.Flags().GetString("correlation-id") + correlationId, _ := cmd.Flags().GetString("correlation-id") innovationEngine := engine.NewEngine(engine.EngineConfiguration{ Verbose: verbose, - DoNotDelete: do_not_delete, + DoNotDelete: doNotDelete, Subscription: subscription, - CorrelationId: correlation_id, + CorrelationId: correlationId, }) scenario, err := engine.CreateScenarioFromMarkdown(markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index b64ae8c2..9643ae10 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -90,18 +90,18 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { // If the command is an SSH command, we need to forward the input and // output - forward_input_output := false + interactiveCommand := false if sshCommand.MatchString(block.Content) { - forward_input_output = true + interactiveCommand = true } - logging.GlobalLogger.WithField("forward_intput_output", forward_input_output).Info("Executing command: " + block.Content) + logging.GlobalLogger.WithField("forward_intput_output", interactiveCommand).Info("Executing command: " + block.Content) var commandErr error var frame int = 0 // If forwarding input/output, don't render the spinner. - if !forward_input_output { + if !interactiveCommand { // Grab the number of lines it contains & set the cursor to the // beginning of the block. lines := strings.Count(block.Content, "\n") @@ -112,7 +112,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { fmt.Print("\033[?25l") go func(block parsers.CodeBlock) { - output, err := shells.ExecuteBashCommand(block.Content, utils.CopyMap(env), true, forward_input_output) + output, err := shells.ExecuteBashCommand(block.Content, utils.CopyMap(env), true, interactiveCommand) commandOutput = output done <- err }(block) @@ -151,7 +151,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { } } else { func(block parsers.CodeBlock) { - shells.ExecuteBashCommand(block.Content, utils.CopyMap(env), true, forward_input_output) + shells.ExecuteBashCommand(block.Content, utils.CopyMap(env), true, interactiveCommand) }(block) } } From eec2a3b8534c4d3f505788b4116ef7f4c8d4f13c Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 31 Jul 2023 16:35:35 -0700 Subject: [PATCH 095/226] [add] environment determination, status updates for OCD, and fixes to execution. --- cmd/ie/commands/execute.go | 10 ++++- cmd/ie/commands/root.go | 17 +++++++- internal/engine/engine.go | 12 +++++- internal/engine/execution.go | 78 ++++++++++++++++++++++++++++++++++-- internal/ocd/status.go | 10 +++++ internal/shells/bash.go | 5 ++- 6 files changed, 122 insertions(+), 10 deletions(-) create mode 100644 internal/ocd/status.go diff --git a/cmd/ie/commands/execute.go b/cmd/ie/commands/execute.go index dbcd1f60..21522291 100644 --- a/cmd/ie/commands/execute.go +++ b/cmd/ie/commands/execute.go @@ -1,6 +1,9 @@ package commands import ( + "fmt" + "os" + "github.com/Azure/InnovationEngine/internal/engine" "github.com/spf13/cobra" ) @@ -26,24 +29,27 @@ var executeCommand = &cobra.Command{ markdownFile := args[0] if markdownFile == "" { cmd.Help() - return + os.Exit(1) } verbose, _ := cmd.Flags().GetBool("verbose") doNotDelete, _ := cmd.Flags().GetBool("do-not-delete") subscription, _ := cmd.Flags().GetString("subscription") correlationId, _ := cmd.Flags().GetString("correlation-id") + environment, _ := cmd.Flags().GetString("environment") innovationEngine := engine.NewEngine(engine.EngineConfiguration{ Verbose: verbose, DoNotDelete: doNotDelete, Subscription: subscription, CorrelationId: correlationId, + Environment: environment, }) scenario, err := engine.CreateScenarioFromMarkdown(markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}) if err != nil { - panic(err) + fmt.Printf("Error creating scenario: %s", err) + os.Exit(1) } innovationEngine.ExecuteScenario(scenario) diff --git a/cmd/ie/commands/root.go b/cmd/ie/commands/root.go index 8f1998a2..4e36fe46 100644 --- a/cmd/ie/commands/root.go +++ b/cmd/ie/commands/root.go @@ -4,6 +4,7 @@ import ( "fmt" "os" + "github.com/Azure/InnovationEngine/internal/engine" "github.com/Azure/InnovationEngine/internal/logging" "github.com/spf13/cobra" ) @@ -16,15 +17,29 @@ var rootCommand = &cobra.Command{ PersistentPreRun: func(cmd *cobra.Command, args []string) { logLevel, err := cmd.Flags().GetString("log-level") if err != nil { - panic(err) + fmt.Printf("Error getting log level: %s", err) + os.Exit(1) } logging.Init(logging.LevelFromString(logLevel)) + + // Check environment + environment, err := cmd.Flags().GetString("environment") + if err != nil { + fmt.Printf("Error getting environment: %s", err) + os.Exit(1) + } + + if !engine.IsValidEnvironment(environment) { + fmt.Printf("Invalid environment: %s", environment) + os.Exit(1) + } }, } // Entrypoint into the Innovation Engine CLI. func ExecuteCLI() { rootCommand.PersistentFlags().String("log-level", string(logging.Debug), "Configure the log level") + rootCommand.PersistentFlags().String("environment", engine.EnvironmentsLocal, "The environment that the CLI is running in. (local, ci, ocd)") if err := rootCommand.Execute(); err != nil { fmt.Println(err) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 93066eba..12fed3e1 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -14,11 +14,22 @@ var ( scriptText = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")) ) +const ( + EnvironmentsLocal = "local" + EnvironmentsCI = "ci" + EnvironmentsOCD = "ocd" +) + +func IsValidEnvironment(environment string) bool { + return environment == EnvironmentsLocal || environment == EnvironmentsCI || environment == EnvironmentsOCD +} + type EngineConfiguration struct { Verbose bool DoNotDelete bool CorrelationId string Subscription string + Environment string } type Engine struct { @@ -34,7 +45,6 @@ func NewEngine(configuration EngineConfiguration) *Engine { // Executes a deployment scenario. func (e *Engine) ExecuteScenario(scenario *Scenario) error { - if e.Configuration.Subscription != "" { command := fmt.Sprintf("az account set --subscription %s", e.Configuration.Subscription) _, err := shells.ExecuteBashCommand(command, map[string]string{}, true, false) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 9643ae10..ac1b66e2 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -1,12 +1,15 @@ package engine import ( + "encoding/json" "fmt" + "os" "regexp" "strings" "time" "github.com/Azure/InnovationEngine/internal/logging" + "github.com/Azure/InnovationEngine/internal/ocd" "github.com/Azure/InnovationEngine/internal/parsers" "github.com/Azure/InnovationEngine/internal/shells" "github.com/Azure/InnovationEngine/internal/utils" @@ -22,6 +25,7 @@ const ( var azGroupDelete = regexp.MustCompile(`az group delete`) var azCommand = regexp.MustCompile(`az\s+([a-z]+)\s+([a-z]+)`) var sshCommand = regexp.MustCompile(`(^|\s)\bssh\b\s`) +var azResourceURI = regexp.MustCompile(`\"id\": \"(/subscriptions/[^\"]+)\"`) // If a scenario has an `az group delete` command and the `--do-not-delete` // flag is set, we remove it from the steps. @@ -75,9 +79,21 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { logging.GlobalLogger.Info("Resource tracking enabled. Tracking ID: " + env["AZURE_HTTP_USER_AGENT"]) } + var ocdStatus = ocd.OneClickDeploymentStatus{} + stepsToExecute := filterDeletionCommands(steps, e.Configuration.DoNotDelete) + + if e.Configuration.Environment == EnvironmentsOCD { + for stepNumber, step := range stepsToExecute { + ocdStatus.Steps = append(ocdStatus.Steps, fmt.Sprintf("%d. %s", stepNumber+1, step.Name)) + } + ocdStatus.Status = "Executing" + } + for stepNumber, step := range stepsToExecute { fmt.Printf("%d. %s\n", stepNumber+1, step.Name) + ocdStatus.CurrentStep = stepNumber + 1 + for _, block := range step.CodeBlocks { // Render the codeblock. indentedBlock := indentMultiLineCommand(block.Content, 4) @@ -95,7 +111,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { interactiveCommand = true } - logging.GlobalLogger.WithField("forward_intput_output", interactiveCommand).Info("Executing command: " + block.Content) + logging.GlobalLogger.WithField("isInteractive", interactiveCommand).Infof("Executing command: %s", block.Content) var commandErr error var frame int = 0 @@ -136,10 +152,34 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { if e.Configuration.Verbose { fmt.Printf(" %s\n", verboseStyle.Render(commandOutput.StdOut)) } + + if e.Configuration.Environment == EnvironmentsOCD { + if azCommand.MatchString(block.Content) { + matches := azResourceURI.FindStringSubmatch(commandOutput.StdOut) + if len(matches) > 1 { + logging.GlobalLogger.Infof("Found resource URI: %s", matches[1]) + ocdStatus.ResourceURIs = append(ocdStatus.ResourceURIs, matches[1]) + } else { + logging.GlobalLogger.Warnf("Could not find resource URI in the output for the command: %s", block.Content) + } + } + ocdStatusJSON, _ := json.Marshal(ocdStatus) + fmt.Println(string(ocdStatusJSON)) + } + } else { fmt.Printf("\r %s \n", errorStyle.Render("✗")) fmt.Printf("\033[%dB", lines) fmt.Printf(" %s\n", errorMessageStyle.Render(commandErr.Error())) + + if e.Configuration.Environment == EnvironmentsOCD { + ocdStatus.Status = "Failed" + ocdStatus.Error = commandErr.Error() + ocdStatusJSON, _ := json.Marshal(ocdStatus) + fmt.Println(string(ocdStatusJSON)) + } + + os.Exit(1) } break loop @@ -150,11 +190,41 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { } } } else { - func(block parsers.CodeBlock) { - shells.ExecuteBashCommand(block.Content, utils.CopyMap(env), true, interactiveCommand) - }(block) + lines := strings.Count(block.Content, "\n") + output, err := shells.ExecuteBashCommand(block.Content, utils.CopyMap(env), true, interactiveCommand) + + if checkForAzCLIError(block.Content, output) { + err = fmt.Errorf(output.StdErr) + } + + if err == nil { + fmt.Print("\033[?25h") + fmt.Printf("\r %s \n", checkStyle.Render("✔")) + + fmt.Printf("\033[%dB\n", lines) + if e.Configuration.Verbose { + fmt.Printf(" %s\n", verboseStyle.Render(commandOutput.StdOut)) + } + } else { + fmt.Printf("\r %s \n", errorStyle.Render("✗")) + fmt.Printf("\033[%dB", lines) + fmt.Printf(" %s\n", errorMessageStyle.Render(err.Error())) + + if e.Configuration.Environment == EnvironmentsOCD { + ocdStatus.Status = "Failed" + ocdStatus.Error = err.Error() + fmt.Println(ocdStatus) + } + + os.Exit(1) + } } } } + + if e.Configuration.Environment == EnvironmentsOCD { + ocdStatus.Status = "Succeeded" + } + shells.ResetStoredEnvironmentVariables() } diff --git a/internal/ocd/status.go b/internal/ocd/status.go new file mode 100644 index 00000000..b0db9c61 --- /dev/null +++ b/internal/ocd/status.go @@ -0,0 +1,10 @@ +package ocd + +// / The status of a one-click deployment. +type OneClickDeploymentStatus struct { + Steps []string `json:"steps"` + CurrentStep int `json:"currentStep"` + Status string `json:"status"` + ResourceURIs []string `json:"resourceURIs"` + Error string `json:"error"` +} diff --git a/internal/shells/bash.go b/internal/shells/bash.go index a5d2d9af..cd303e92 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -46,8 +46,9 @@ func ResetStoredEnvironmentVariables() error { } type CommandOutput struct { - StdOut string - StdErr string + StdOut string + StdErr string + StatusCode int } // Executes a bash command and returns the output or error. From 2111e0dbd52480a4f9926042c5b8c660d3aa25e8 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 31 Jul 2023 16:37:36 -0700 Subject: [PATCH 096/226] [update] grouping for regex variables --- internal/engine/execution.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index ac1b66e2..f614e193 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -22,10 +22,12 @@ const ( spinnerRefresh = 100 * time.Millisecond ) -var azGroupDelete = regexp.MustCompile(`az group delete`) -var azCommand = regexp.MustCompile(`az\s+([a-z]+)\s+([a-z]+)`) -var sshCommand = regexp.MustCompile(`(^|\s)\bssh\b\s`) -var azResourceURI = regexp.MustCompile(`\"id\": \"(/subscriptions/[^\"]+)\"`) +var ( + azGroupDelete = regexp.MustCompile(`az group delete`) + azCommand = regexp.MustCompile(`az\s+([a-z]+)\s+([a-z]+)`) + sshCommand = regexp.MustCompile(`(^|\s)\bssh\b\s`) + azResourceURI = regexp.MustCompile(`\"id\": \"(/subscriptions/[^\"]+)\"`) +) // If a scenario has an `az group delete` command and the `--do-not-delete` // flag is set, we remove it from the steps. From bf2ff265b7c2148e6f28b11e94ceffa25584c99a Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 1 Aug 2023 12:08:02 -0700 Subject: [PATCH 097/226] [add] prefix & postfix for ocd status json. --- internal/engine/execution.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index f614e193..fa72ec53 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -166,7 +166,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { } } ocdStatusJSON, _ := json.Marshal(ocdStatus) - fmt.Println(string(ocdStatusJSON)) + fmt.Println("ie_us" + string(ocdStatusJSON) + "ie_ue") } } else { @@ -178,7 +178,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { ocdStatus.Status = "Failed" ocdStatus.Error = commandErr.Error() ocdStatusJSON, _ := json.Marshal(ocdStatus) - fmt.Println(string(ocdStatusJSON)) + fmt.Println("ie_us" + string(ocdStatusJSON) + "ie_ue") } os.Exit(1) @@ -207,6 +207,11 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { if e.Configuration.Verbose { fmt.Printf(" %s\n", verboseStyle.Render(commandOutput.StdOut)) } + + if e.Configuration.Environment == EnvironmentsOCD { + ocdStatusJSON, _ := json.Marshal(ocdStatus) + fmt.Println("ie_us" + string(ocdStatusJSON) + "ie_ue") + } } else { fmt.Printf("\r %s \n", errorStyle.Render("✗")) fmt.Printf("\033[%dB", lines) @@ -215,7 +220,9 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { if e.Configuration.Environment == EnvironmentsOCD { ocdStatus.Status = "Failed" ocdStatus.Error = err.Error() - fmt.Println(ocdStatus) + ocdStatusJSON, _ := json.Marshal(ocdStatus) + + fmt.Println("ie_us" + string(ocdStatusJSON) + "ie_ue") } os.Exit(1) From adaedb3f10532d7e1c374a3a5aec039dbd35af44 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 1 Aug 2023 14:05:58 -0700 Subject: [PATCH 098/226] [refactor] status reporting & instantation. Remove URI tracking for now. --- internal/engine/execution.go | 57 ++++++++++++++---------------------- internal/ocd/status.go | 40 +++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 35 deletions(-) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index fa72ec53..a64699d1 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -1,7 +1,6 @@ package engine import ( - "encoding/json" "fmt" "os" "regexp" @@ -70,6 +69,20 @@ func checkForAzCLIError(command string, output shells.CommandOutput) bool { return false } +// Print out the one click deployment status if in the correct environment. +func reportOCDStatus(status ocd.OneClickDeploymentStatus, environment string) { + if environment == EnvironmentsOCD { + statusJson, err := status.AsJsonString() + if err != nil { + logging.GlobalLogger.Error("Failed to marshal status", err) + } else { + // We add these strings to the output so that the portal can find and parse + // the JSON status. + fmt.Printf("ie_us%sie_ue\n", statusJson) + } + } +} + // Executes the steps from a scenario and renders the output to the terminal. func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { @@ -81,15 +94,14 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { logging.GlobalLogger.Info("Resource tracking enabled. Tracking ID: " + env["AZURE_HTTP_USER_AGENT"]) } - var ocdStatus = ocd.OneClickDeploymentStatus{} + var ocdStatus = ocd.NewOneClickDeploymentStatus() stepsToExecute := filterDeletionCommands(steps, e.Configuration.DoNotDelete) if e.Configuration.Environment == EnvironmentsOCD { for stepNumber, step := range stepsToExecute { - ocdStatus.Steps = append(ocdStatus.Steps, fmt.Sprintf("%d. %s", stepNumber+1, step.Name)) + ocdStatus.AddStep(fmt.Sprintf("%d. %s", stepNumber+1, step.Name)) } - ocdStatus.Status = "Executing" } for stepNumber, step := range stepsToExecute { @@ -155,31 +167,15 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { fmt.Printf(" %s\n", verboseStyle.Render(commandOutput.StdOut)) } - if e.Configuration.Environment == EnvironmentsOCD { - if azCommand.MatchString(block.Content) { - matches := azResourceURI.FindStringSubmatch(commandOutput.StdOut) - if len(matches) > 1 { - logging.GlobalLogger.Infof("Found resource URI: %s", matches[1]) - ocdStatus.ResourceURIs = append(ocdStatus.ResourceURIs, matches[1]) - } else { - logging.GlobalLogger.Warnf("Could not find resource URI in the output for the command: %s", block.Content) - } - } - ocdStatusJSON, _ := json.Marshal(ocdStatus) - fmt.Println("ie_us" + string(ocdStatusJSON) + "ie_ue") - } + reportOCDStatus(ocdStatus, e.Configuration.Environment) } else { fmt.Printf("\r %s \n", errorStyle.Render("✗")) fmt.Printf("\033[%dB", lines) fmt.Printf(" %s\n", errorMessageStyle.Render(commandErr.Error())) - if e.Configuration.Environment == EnvironmentsOCD { - ocdStatus.Status = "Failed" - ocdStatus.Error = commandErr.Error() - ocdStatusJSON, _ := json.Marshal(ocdStatus) - fmt.Println("ie_us" + string(ocdStatusJSON) + "ie_ue") - } + ocdStatus.SetError(commandErr) + reportOCDStatus(ocdStatus, e.Configuration.Environment) os.Exit(1) } @@ -208,23 +204,14 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { fmt.Printf(" %s\n", verboseStyle.Render(commandOutput.StdOut)) } - if e.Configuration.Environment == EnvironmentsOCD { - ocdStatusJSON, _ := json.Marshal(ocdStatus) - fmt.Println("ie_us" + string(ocdStatusJSON) + "ie_ue") - } + reportOCDStatus(ocdStatus, e.Configuration.Environment) } else { fmt.Printf("\r %s \n", errorStyle.Render("✗")) fmt.Printf("\033[%dB", lines) fmt.Printf(" %s\n", errorMessageStyle.Render(err.Error())) - if e.Configuration.Environment == EnvironmentsOCD { - ocdStatus.Status = "Failed" - ocdStatus.Error = err.Error() - ocdStatusJSON, _ := json.Marshal(ocdStatus) - - fmt.Println("ie_us" + string(ocdStatusJSON) + "ie_ue") - } - + ocdStatus.SetError(err) + reportOCDStatus(ocdStatus, e.Configuration.Environment) os.Exit(1) } } diff --git a/internal/ocd/status.go b/internal/ocd/status.go index b0db9c61..babeffe5 100644 --- a/internal/ocd/status.go +++ b/internal/ocd/status.go @@ -1,5 +1,11 @@ package ocd +import ( + "encoding/json" + + "github.com/Azure/InnovationEngine/internal/logging" +) + // / The status of a one-click deployment. type OneClickDeploymentStatus struct { Steps []string `json:"steps"` @@ -8,3 +14,37 @@ type OneClickDeploymentStatus struct { ResourceURIs []string `json:"resourceURIs"` Error string `json:"error"` } + +func NewOneClickDeploymentStatus() OneClickDeploymentStatus { + return OneClickDeploymentStatus{ + Steps: []string{}, + CurrentStep: 0, + Status: "Executing", + ResourceURIs: []string{}, + Error: "", + } +} + +// Get the status as a JSON string. +func (status *OneClickDeploymentStatus) AsJsonString() (string, error) { + json, err := json.Marshal(status) + if err != nil { + logging.GlobalLogger.Error("Failed to marshal status", err) + return "", err + } + + return string(json), nil +} + +func (status *OneClickDeploymentStatus) AddStep(step string) { + status.Steps = append(status.Steps, step) +} + +func (status *OneClickDeploymentStatus) AddResourceURI(uri string) { + status.ResourceURIs = append(status.ResourceURIs, uri) +} + +func (status *OneClickDeploymentStatus) SetError(err error) { + status.Status = "Failed" + status.Error = err.Error() +} From 4cf1ca0c2f358ad2f63f52bdf6144f0e3d9cef3b Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 1 Aug 2023 15:10:57 -0700 Subject: [PATCH 099/226] [update] resource URIs to be populated in the end after all of the deployments have completed. --- internal/engine/execution.go | 69 +++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index a64699d1..5a85cd1d 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -22,10 +22,15 @@ const ( ) var ( - azGroupDelete = regexp.MustCompile(`az group delete`) + sshCommand = regexp.MustCompile(`(^|\s)\bssh\b\s`) + + // Az cli command regex azCommand = regexp.MustCompile(`az\s+([a-z]+)\s+([a-z]+)`) - sshCommand = regexp.MustCompile(`(^|\s)\bssh\b\s`) - azResourceURI = regexp.MustCompile(`\"id\": \"(/subscriptions/[^\"]+)\"`) + azGroupDelete = regexp.MustCompile(`az group delete`) + + // ARM response regex + azResourceURI = regexp.MustCompile(`\"id\": \"(/subscriptions/[^\"]+)\"`) + azResourceGroupName = regexp.MustCompile(`resourceGroups/([^\"]+)`) ) // If a scenario has an `az group delete` command and the `--do-not-delete` @@ -83,6 +88,30 @@ func reportOCDStatus(status ocd.OneClickDeploymentStatus, environment string) { } } +func findResourceGroupName(commandOutput string) string { + matches := azResourceGroupName.FindStringSubmatch(commandOutput) + if len(matches) > 1 { + return matches[1] + } + return "" +} + +// Find all the deployed resources in a resource group. +func findAllDeployedResourceURIs(resourceGroup string) []string { + output, err := shells.ExecuteBashCommand("az resource list -g"+resourceGroup, map[string]string{}, true, false) + + if err != nil { + logging.GlobalLogger.Error("Failed to list deployments", err) + } + + matches := azResourceURI.FindAllStringSubmatch(output.StdOut, -1) + results := []string{} + for _, match := range matches { + results = append(results, match[1]) + } + return results +} + // Executes the steps from a scenario and renders the output to the terminal. func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { @@ -94,16 +123,17 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { logging.GlobalLogger.Info("Resource tracking enabled. Tracking ID: " + env["AZURE_HTTP_USER_AGENT"]) } + var resourceGroupName string var ocdStatus = ocd.NewOneClickDeploymentStatus() stepsToExecute := filterDeletionCommands(steps, e.Configuration.DoNotDelete) - if e.Configuration.Environment == EnvironmentsOCD { - for stepNumber, step := range stepsToExecute { - ocdStatus.AddStep(fmt.Sprintf("%d. %s", stepNumber+1, step.Name)) - } + for stepNumber, step := range stepsToExecute { + ocdStatus.AddStep(fmt.Sprintf("%d. %s", stepNumber+1, step.Name)) } + reportOCDStatus(ocdStatus, e.Configuration.Environment) + for stepNumber, step := range stepsToExecute { fmt.Printf("%d. %s\n", stepNumber+1, step.Name) ocdStatus.CurrentStep = stepNumber + 1 @@ -167,6 +197,16 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { fmt.Printf(" %s\n", verboseStyle.Render(commandOutput.StdOut)) } + // Extract the resource group name from the command output if + // it's not already set. + if resourceGroupName == "" && azCommand.MatchString(block.Content) { + tmpResourceGroup := findResourceGroupName(commandOutput.StdOut) + if tmpResourceGroup != "" { + logging.GlobalLogger.WithField("resourceGroup", tmpResourceGroup).Info("Found resource group") + resourceGroupName = tmpResourceGroup + } + } + reportOCDStatus(ocdStatus, e.Configuration.Environment) } else { @@ -218,9 +258,20 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { } } - if e.Configuration.Environment == EnvironmentsOCD { - ocdStatus.Status = "Succeeded" + ocdStatus.Status = "Succeeded" + + if resourceGroupName != "" { + resourceURIs := findAllDeployedResourceURIs(resourceGroupName) + + if len(resourceURIs) > 0 { + logging.GlobalLogger.WithField("resourceURIs", resourceURIs).Info("Found deployed resources.") + ocdStatus.ResourceURIs = resourceURIs + } + } else { + logging.GlobalLogger.Warn("No resource group name found. Unable to find deployed resources.") } + reportOCDStatus(ocdStatus, e.Configuration.Environment) + shells.ResetStoredEnvironmentVariables() } From 02e49f70b3ea0f13b755675d3ae0470c08cb6ac6 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 4 Aug 2023 10:17:38 -0700 Subject: [PATCH 100/226] [update] status code exits by capturing the command exit status --- internal/engine/execution.go | 2 +- internal/shells/bash.go | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 5a85cd1d..3943a6c0 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -28,7 +28,7 @@ var ( azCommand = regexp.MustCompile(`az\s+([a-z]+)\s+([a-z]+)`) azGroupDelete = regexp.MustCompile(`az group delete`) - // ARM response regex + // ARM regex azResourceURI = regexp.MustCompile(`\"id\": \"(/subscriptions/[^\"]+)\"`) azResourceGroupName = regexp.MustCompile(`resourceGroups/([^\"]+)`) ) diff --git a/internal/shells/bash.go b/internal/shells/bash.go index cd303e92..f3a2b5bb 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -55,8 +55,11 @@ type CommandOutput struct { func ExecuteBashCommand(command string, env map[string]string, inherit_environment_variables bool, forward_input_output bool) (CommandOutput, error) { var commandWithStateSaved = []string{ command, + "IE_LAST_COMMAND_EXIT_CODE=\"$?\"", "env > /tmp/env.txt", + "exit $IE_LAST_COMMAND_EXIT_CODE", } + commandToExecute := exec.Command("bash", "-c", strings.Join(commandWithStateSaved, "\n")) var stdoutBuffer, stderrBuffer bytes.Buffer From 4d6ee1b68ef1ee3cafc8a6571942dc4ccbefc944 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 8 Aug 2023 13:30:26 -0700 Subject: [PATCH 101/226] [remove] az cli error check now that exit codes are working, refactor name. --- internal/engine/execution.go | 24 +----------------------- internal/engine/testing.go | 5 ----- internal/shells/bash.go | 13 ++++++------- 3 files changed, 7 insertions(+), 35 deletions(-) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 3943a6c0..56fcaea9 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -60,20 +60,6 @@ func filterDeletionCommands(steps []Step, preserveResources bool) []Step { return filteredSteps } -// Check for errors from the Azure CLI. The Azure CLI doesn't return a non-zero -// exit code when an error occurs, so we have to check the output for errors. -func checkForAzCLIError(command string, output shells.CommandOutput) bool { - if !azCommand.MatchString(command) { - return false - } - - if output.StdOut == "" && output.StdErr != "" { - return true - } - - return false -} - // Print out the one click deployment status if in the correct environment. func reportOCDStatus(status ocd.OneClickDeploymentStatus, environment string) { if environment == EnvironmentsOCD { @@ -185,10 +171,6 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { // final status. fmt.Print("\033[?25h") - if checkForAzCLIError(block.Content, commandOutput) { - commandErr = fmt.Errorf(commandOutput.StdErr) - } - if commandErr == nil { fmt.Printf("\r %s \n", checkStyle.Render("✔")) @@ -231,17 +213,13 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { lines := strings.Count(block.Content, "\n") output, err := shells.ExecuteBashCommand(block.Content, utils.CopyMap(env), true, interactiveCommand) - if checkForAzCLIError(block.Content, output) { - err = fmt.Errorf(output.StdErr) - } - if err == nil { fmt.Print("\033[?25h") fmt.Printf("\r %s \n", checkStyle.Render("✔")) fmt.Printf("\033[%dB\n", lines) if e.Configuration.Verbose { - fmt.Printf(" %s\n", verboseStyle.Render(commandOutput.StdOut)) + fmt.Printf(" %s\n", verboseStyle.Render(output.StdOut)) } reportOCDStatus(ocdStatus, e.Configuration.Environment) diff --git a/internal/engine/testing.go b/internal/engine/testing.go index 56a82c6e..94864980 100644 --- a/internal/engine/testing.go +++ b/internal/engine/testing.go @@ -51,11 +51,6 @@ func (e *Engine) TestSteps(steps []Step, env map[string]string) { // final status. fmt.Print("\033[?25h") - // Handle the case where the command is an az cli command. - if checkForAzCLIError(block.Content, commandOutput) { - err = fmt.Errorf(commandOutput.StdErr) - } - if err == nil { actualOutput := commandOutput.StdOut expectedOutput := block.ExpectedOutput.Content diff --git a/internal/shells/bash.go b/internal/shells/bash.go index f3a2b5bb..e6392a2f 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -46,13 +46,12 @@ func ResetStoredEnvironmentVariables() error { } type CommandOutput struct { - StdOut string - StdErr string - StatusCode int + StdOut string + StdErr string } // Executes a bash command and returns the output or error. -func ExecuteBashCommand(command string, env map[string]string, inherit_environment_variables bool, forward_input_output bool) (CommandOutput, error) { +func ExecuteBashCommand(command string, env map[string]string, inheritEnvironment bool, forwardInputOutput bool) (CommandOutput, error) { var commandWithStateSaved = []string{ command, "IE_LAST_COMMAND_EXIT_CODE=\"$?\"", @@ -64,7 +63,7 @@ func ExecuteBashCommand(command string, env map[string]string, inherit_environme var stdoutBuffer, stderrBuffer bytes.Buffer - if forward_input_output { + if forwardInputOutput { commandToExecute.Stdout = os.Stdout commandToExecute.Stderr = os.Stderr commandToExecute.Stdin = os.Stdin @@ -74,7 +73,7 @@ func ExecuteBashCommand(command string, env map[string]string, inherit_environme commandToExecute.Stderr = &stderrBuffer } - if inherit_environment_variables { + if inheritEnvironment { commandToExecute.Env = os.Environ() } @@ -96,7 +95,7 @@ func ExecuteBashCommand(command string, env map[string]string, inherit_environme } err = commandToExecute.Run() - if forward_input_output { + if forwardInputOutput { return CommandOutput{}, err } From 73dc17ef56b32efc2452132d798c8706b2d9bdbb Mon Sep 17 00:00:00 2001 From: vmarcella Date: Wed, 9 Aug 2023 15:50:31 -0700 Subject: [PATCH 102/226] [remove] python modules no longer part of the project. --- MarkdownParser.py | 236 ------------------------------------ executor.py | 300 ---------------------------------------------- main.py | 37 ------ requirements.txt | 5 - 4 files changed, 578 deletions(-) delete mode 100644 MarkdownParser.py delete mode 100644 executor.py delete mode 100644 main.py delete mode 100644 requirements.txt diff --git a/MarkdownParser.py b/MarkdownParser.py deleted file mode 100644 index d7b109de..00000000 --- a/MarkdownParser.py +++ /dev/null @@ -1,236 +0,0 @@ -#Given a markdown file, this should break out the headings, paragraphs, executable commands etc. -import re - -class MarkdownParser: - - - def __init__(self, markdownFilepath): - self.markdownFilepath = markdownFilepath - self.markdownElements = [] - self.codeBlockType = '```' - self.headingType = '#' - self.paragraphType = 'p' - self.commentType = '' - endOfComment = True - continue - elif self.markdownFile.read(1) == '`': - if self.checkForCodeBlock(char): - subtype, command = self.processCodeSample(char) - self.createAndAppendElement(self.commentType, subtype, command) - comment += command - else: - self.markdownFile.seek(currentPosition) - comment += char - - char = self.markdownFile.read(1) - - if "expected_similarity" in comment: - results,subtype = self.processResultsBlock() - similarity = re.findall(r"\d*\.?\d+", comment)[0] - # outputblock = "```output\n" + results + "\n```" - # self.createAndAppendElement(self.paragraphType, 'paragraph', outputblock.strip()) - # Loops through elements starting at the end looking for the most recent - # Codeblock markdown item and adds the results - for i, markdownElement in reversed(list(enumerate(self.markdownElements))): - if markdownElement[0] == self.codeBlockType: - self.markdownElements[i][1].results = results - self.markdownElements[i][1].similarity = similarity - - break - self.createAndAppendElement(self.codeBlockType, subtype, results) - else: - self.createAndAppendElement(self.commentType, None, comment) - - - - - def createAndAppendElement(self, type, subtype, value): - element = MarkdownElement(subtype, value) - self.markdownElements.append((type, element)) - - # Helper function ran when we hit a backtick. It checks the next two characters to see - # if they are also backticks. If so, we enter a code block and return true - def checkForCodeBlock(self, char): - currentPosition = self.markdownFile.tell() - if self.markdownFile.read(1) == '`' and self.markdownFile.read(1) == '`': - self.markdownFile.seek(currentPosition) - return True - else: - self.markdownFile.seek(currentPosition) - return False - - def checkForComment(self): - currentPosition = self.markdownFile.tell() - if self.markdownFile.read(1) == '!' and self.markdownFile.read(1) == '-' and self.markdownFile.read(1) == '-': - self.markdownFile.seek(currentPosition) - return True - else: - self.markdownFile.seek(currentPosition) - return False - - def processResultsBlock(self): - results = "" - subtype = "" - endOfCodeBlock = False - - char = self.markdownFile.read(1) - - while char != '`': - char = self.markdownFile.read(1) - - self.markdownFile.read(2) - while char != '\n': - char = self.markdownFile.read(1) - subtype += char - - while not endOfCodeBlock: - if (char == '`'): - if self.checkForCodeBlock(char): - endOfCodeBlock = True - # Read the remaining bash ticks - self.markdownFile.read(2) - - else: - results += char - # Read all 3 back ticks - char = self.markdownFile.read(1) - - return results.strip(), subtype.strip() - # If we want to add process for bold and italicized text - def processBoldText(self,char): - pass - - # If want to process dashes for hidden titles etc. - def processDash(self, char): - pass - -class MarkdownElement: - - def __init__(self, subtype, value): - self.subtype = subtype - self.value = value - self.results = None - self.similarity = 1.0 - diff --git a/executor.py b/executor.py deleted file mode 100644 index 33d53850..00000000 --- a/executor.py +++ /dev/null @@ -1,300 +0,0 @@ - -# Class which will run the main loop of the program - -from unittest import result -import pexpect -import pexpect.replwrap -import time -from fuzzywuzzy import fuzz -from fuzzywuzzy import process -import re -import random -from os.path import exists - -PEXPECT_PROMPT = u'[PEXPECT_PROMPT>' -PEXPECT_CONTINUATION_PROMPT = u'[PEXPECT_PROMPT+' - -class Executor: - shell = None - markdownData = None - executableCodeList = None - - def __init__(self, markdownData, modeOfOperation, fileName): - self.markdownData = markdownData - self.fileName = fileName - self.executableCodeList = {"bash", "terraform", 'azurecli-interactive' , 'azurecli'} - self.modeOfOperation = modeOfOperation - self.numberOfTestsPassed = 0 - self.totalNumberOfTests = 0 - self.failedTests = [] - self.randomIdentifierSet = False - self.randomIdentifier = random.randint(100,10000) - self.shell = self.get_shell() - self.readEnvVariables() - - - # Fairly straight forward main loop. While markdownData is not empty - # Checks type for heading, code block, or paragrpah. - # If Heading it outputs the heading, pops the item and prompts input from user - # If paragraph it outputs paragraph and pops item from list and continues with no pause - # If Code block, it calls ExecuteCode helper function to print and execute the code block - - def runMainLoop(self): - if self.modeOfOperation == "interactive": - self.runMainLoopInteractive() - elif self.modeOfOperation == "test": - self.runMainLoopTest() - elif self.modeOfOperation == 'execute': - self.runMainLoopExecute() - else: - self.runMainLoopInteractive() - - - # This function loops through the markdown elements in an interactive manner. It pauses and - # requests input from the user to continue at every heading and code block - def runMainLoopInteractive(self): - beginningHeading = True - fromCodeBlock = False - - for markdownItem in self.markdownData: - if markdownItem[0] == '#': - if beginningHeading or fromCodeBlock: - print(markdownItem[1].value) - beginningHeading = False - fromCodeBlock = False - else: - beginningHeading = True - self.askForInput("Press any key to continue...") - - elif markdownItem[0] == 'p' and markdownItem[1].subtype == 'prerequisites': - print(markdownItem[1].value) - self.askForInput("Press any key to proceed and execute the prerequisites...") - self.executePrerequisites(markdownItem) - beginningHeading = True - - elif markdownItem[0] == 'p': - print(markdownItem[1].value) - beginningHeading = True - - elif markdownItem[0] == '```': - print('\n```' + markdownItem[1].subtype + '\n' + markdownItem[1].value + '\n```') - self.executeCode(markdownItem) - fromCodeBlock = True - - # This function runs through and only looks at code blocks. It executes them and then - # Looks at the output. It will automatically return exit code 1 if a test fails. - # Used in GitHub Actions and automated testing scenarios - def runMainLoopTest(self): - for markdownItem in self.markdownData: - if markdownItem[0] == '```': - print('\n```' + markdownItem[1].subtype + '\n' + markdownItem[1].value + '\n```') - if markdownItem[1].subtype in self.executableCodeList: - if not self.randomIdentifierSet and self.modeOfOperation == "test": - self.randomIdentifierSet = True - setRandomIdentifierCommand = 'export MY_RESOURCE_GROUP_NAME=testResourceGroup' + str(self.randomIdentifier) - self.shell.run_command(setRandomIdentifierCommand, 1200).strip() - - self.runCommand(markdownItem) - - elif markdownItem[0] == 'p' and markdownItem[1].subtype == 'prerequisites': - self.executePrerequisites(markdownItem) - - elif markdownItem[0] == 'p' and markdownItem[1].subtype == 'next steps': - self.executeNextSteps(markdownItem) - - - print("\n{} of {} Tests Passed!".format(str(self.numberOfTestsPassed), str(self.totalNumberOfTests))) - if self.numberOfTestsPassed < self.totalNumberOfTests: - print("---------FAILED CODE BLOCKS-------------- \n\n") - for failedTest in self.failedTests: - print(failedTest[0][1].value + '\n') - print(failedTest[1]) - - exit(1) - - # This function runs through and executes not pausing for any input or failing. - # The primary intention of this is for executing pre requisites - def runMainLoopExecute(self): - for markdownItem in self.markdownData: - print(markdownItem[1].value) - if markdownItem[0] == '```': - print('\n```' + markdownItem[1].subtype + '\n' + markdownItem[1].value + '\n```') - if markdownItem[1].subtype in self.executableCodeList: - self.runCommand(markdownItem) - - # Checks to see if code block is executable i.e, bash, terraform, azurecli-interactive, azurecli - # If it is it will wait for input and call run command which passes the command to the repl - def executeCode(self, markdownItem): - if markdownItem[1].subtype in self.executableCodeList: - self.askForInput("Press any key to execute the above code block...") - print("Executing Code...") - self.runCommand(markdownItem) - - else: - self.askForInput("Press any key to continue...") - - # Function takes a command and uses the shell which was instantiated at run time using the - # Local shell information to execute that command. If the user is logged into az cli on - # The authentication will carry over to this environment as well - def runCommand(self, markdownItem): - command = markdownItem[1].value - expectedResult = markdownItem[1].results - expectedSimilarity = markdownItem[1].similarity - - #print("debug", "Execute command: '" + command + "'\n") - startTime = time.time() - try: - # Setting a 20 minute timeout...Need a better way to discover broken commands - response = self.shell.run_command(command, 1200).strip() - except ValueError as ve: - print("Continuation prompt required for command " + command) - print(ex) - response = command + " failed to run" - except Exception as ex: - print("command timed out") - print(ex) - response = command + " failed to run" - - - timeToExecute = time.time() - startTime - print("\n" + response + "\n" + "Time to Execute - " + str(timeToExecute)) - - if expectedResult is not None: - print("Expected Results - " + expectedResult) - self.testResponse(response, expectedResult, expectedSimilarity, markdownItem) - - - def testResponse(self, response, expectedResult, expectedSimilarity, markdownItem): - # Todo... try to implement more than just fuzzy matching. Can we look and see if the command returned - # A warning or an error? Problem I am having is calls can return every type of response... I could - # Hard code something for Azure responses, but it wouldn't be extendible - #print("\n```output\n" + expectedResult + "\n```") - - if self.modeOfOperation == "interactive": - actualSimilarity = fuzz.ratio(response, expectedResult) / 100 - - if actualSimilarity < float(expectedSimilarity): - print("The output is NOT correct. The remainder of the document may not function properly") - print("The Actual similarity was {} \n The expected similarity was {}".format(str(actualSimilarity), expectedSimilarity)) - - - self.askForInput("Press any key to continue...") - - elif self.modeOfOperation == "test": - self.totalNumberOfTests += 1 - actualSimilarity = fuzz.ratio(response, expectedResult) / 100 - - if actualSimilarity < float(expectedSimilarity): - errorOutput = "Test Failed \n\nThe expected result was - \n" + expectedResult - errorOutput += "\nthe actual result - \n" + response - errorOutput += "\nThe Actual similarity was {} \nThe expected similarity was {} \n".format(str(actualSimilarity), expectedSimilarity) - print(errorOutput) - - self.failedTests.append((markdownItem, errorOutput)) - else: - self.numberOfTestsPassed += 1 - - - # todo: Create testing mode for execute which simply lets the user know if a test fails which one it is - - - - - def executePrerequisites(self, markdownItem): - results = re.findall(r'\]\(([^)]+)\)', markdownItem[1].value) - - for markdownFilepath in results: - if exists(markdownFilepath): - command = 'python3 main.py execute ' + markdownFilepath - print("this is the command to execute \n" + command) - response = self.shell.run_command(command).strip() - print(response) - else: - print("Could not find file named " + markdownFilepath + " Please locate and run this prerequisite manually") - - def executeNextSteps(self, markdownItem): - print("Found Next Steps....") - pass - - def askForInput(self, inputPrompt): - print("\n\n" + inputPrompt + " Press b to exit the program \n \n") - keyPressed = self.getInstructionKey() - if keyPressed == 'b': - print("Exiting program on b key press") - exit() - - def getInstructionKey(self): - """Waits for a single keypress on stdin. - This is a silly function to call if you need to do it a lot because it has - to store stdin's current setup, setup stdin for reading single keystrokes - then read the single keystroke then revert stdin back after reading the - keystroke. - Returns the character of the key that was pressed (zero on - KeyboardInterrupt which can happen when a signal gets handled) - This method is licensed under cc by-sa 3.0 - Thanks to mheyman http://stackoverflow.com/questions/983354/how-do-i-make-python-to-wait-for-a-pressed-key\ - """ - import termios, fcntl, sys, os - fd = sys.stdin.fileno() - # save old state - flags_save = fcntl.fcntl(fd, fcntl.F_GETFL) - attrs_save = termios.tcgetattr(fd) - # make raw - the way to do this comes from the termios(3) man page. - attrs = list(attrs_save) # copy the stored version to update - # iflag - attrs[0] &= ~(termios.IGNBRK | termios.BRKINT | termios.PARMRK - | termios.ISTRIP | termios.INLCR | termios. IGNCR - | termios.ICRNL | termios.IXON ) - # oflag - attrs[1] &= ~termios.OPOST - # cflag - attrs[2] &= ~(termios.CSIZE | termios. PARENB) - attrs[2] |= termios.CS8 - # lflag - attrs[3] &= ~(termios.ECHONL | termios.ECHO | termios.ICANON - | termios.ISIG | termios.IEXTEN) - termios.tcsetattr(fd, termios.TCSANOW, attrs) - # turn off non-blocking - fcntl.fcntl(fd, fcntl.F_SETFL, flags_save & ~os.O_NONBLOCK) - # read a single keystroke - try: - ret = sys.stdin.read(1) # returns a single character - except KeyboardInterrupt: - ret = 0 - finally: - # restore old state - termios.tcsetattr(fd, termios.TCSAFLUSH, attrs_save) - fcntl.fcntl(fd, fcntl.F_SETFL, flags_save) - return ret - - # Function looks for file named - def readEnvVariables(self): - - if exists(self.fileName[:-3] + '.ini'): - envFile = open(self.fileName[:-3] + '.ini') - lines = envFile.readlines() - - for line in lines: - variableName = line.split()[0] - value = line.split()[2] - command = variableName + '=' + value - self.shell.run_command(command).strip() - # Comment block variables goes after the .ini declaration and thus overrides - for markdownItem in self.markdownData: - if markdownItem[0] == " + ```json { "id": "/subscriptions/ab9d8365-2f65-47a4-8df4-7e40db70c8d2/resourceGroups/$MY_RESOURCE_GROUP_NAME", @@ -72,6 +74,8 @@ az storage account create --name $MY_STORAGE_ACCOUNT_NAME --resource-group $MY_R Results: + + ```json { "accessTier": "Hot", @@ -181,6 +185,8 @@ az storage container create --name images --account-name $MY_STORAGE_ACCOUNT_NAM Results: + + ```json { "created": true @@ -219,6 +225,8 @@ az postgres flexible-server create \ Results: + + ```json { "connectionString": "postgresql://$MY_DATABASE_USERNAME:$MY_DATABASE_PASSWORD@$MY_DATABASE_NAME.postgres.database.azure.com/flexibleserverdb?sslmode=require", @@ -260,6 +268,8 @@ az cognitiveservices account create \ Results: + + ```json { "etag": "\"090ac83c-0000-0700-0000-64d4fcd80000\"", @@ -436,6 +446,8 @@ az containerapp show --name $MY_CONTAINER_APP_NAME --resource-group $MY_RESOURCE Results: + + ```json { "id": "/subscriptions/eb9d8265-2f64-47a4-8df4-7e41db70c8d8/resourceGroups/cn-test3/providers/Microsoft.App/containerapps/cntestcontainerapp17", @@ -563,6 +575,8 @@ az postgres flexible-server firewall-rule create \ Results: + + ```json { "endIpAddress": "20.237.221.47", From 7d9eb51abf4571782856467f784b04cdd24fe5e7 Mon Sep 17 00:00:00 2001 From: Ralph Rouhana Date: Sat, 19 Aug 2023 11:39:17 -0400 Subject: [PATCH 119/226] Fix export command typo, add note about accepting AI terms on portal --- .../README.md | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md b/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md index ae0fcbe2..fd36bbf9 100644 --- a/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md +++ b/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md @@ -7,21 +7,23 @@ In this guide, we'll be walking through deploying the necessary resources for a - **Azure Computer Vision** to analyze the images for cats or dogs - **Azure Container App** to deploy our code +Note: If you've never created a Computer Vision resource before, you will not be able to create one using the Azure CLI. You must create your first Computer Vision resource from the Azure portal to review and acknowledge the Responsible AI terms and conditions. You can do so here: [Create a Computer Vision Resource](https://portal.azure.com/#create/Microsoft.CognitiveServicesComputerVision). After that, you can create subsequent resources using any deployment tool (SDK, CLI, or ARM template, etc) under the same Azure subscription. + ## Define Environment Variables -The first step in this tutorial is to define environment variables. Replace the values with your own. +The first step in this tutorial is to define environment variables. **Replace the values on the right with your own unique values.** These values will be used throughout the tutorial to create resources and configure the application. Use lowercase and no special characters for the storage account name. ```plaintext -export MY_RESOURCE_GROUP_NAME=myresourcegroup +export MY_RESOURCE_GROUP_NAME= export MY_LOCATION=westus -export MY_STORAGE_ACCOUNT_NAME=mystorageaccount -export $MY_DATABASE_SERVER_NAME=mydatabaseserver -export $MY_DATABASE_NAME=mydatabase -export MY_DATABASE_USERNAME=mydatabaseusername -export MY_DATABASE_PASSWORD=mydatabasepassword -export MY_COMPUTER_VISION_NAME=mycomputervisionname -export MY_CONTAINER_APP_NAME=mycontainerapp -export MY_CONTAINER_APP_ENV_NAME=mycontainerappenv +export MY_STORAGE_ACCOUNT_NAME= +export MY_DATABASE_SERVER_NAME= +export MY_DATABASE_NAME= +export MY_DATABASE_USERNAME= +export MY_DATABASE_PASSWORD= +export MY_COMPUTER_VISION_NAME= +export MY_CONTAINER_APP_NAME= +export MY_CONTAINER_APP_ENV_NAME= ``` ## Clone the sample repository From bd06316182cec9d400de3a1392d372dad6ec4a15 Mon Sep 17 00:00:00 2001 From: Ralph Rouhana Date: Sat, 19 Aug 2023 12:00:42 -0400 Subject: [PATCH 120/226] Remove lb after similarity comments --- .../ocd/CreateContainerAppDeploymentFromSource/README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md b/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md index fd36bbf9..d44bfa75 100644 --- a/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md +++ b/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md @@ -51,7 +51,6 @@ az group create --name $MY_RESOURCE_GROUP_NAME --location $MY_LOCATION Results: - ```json { "id": "/subscriptions/ab9d8365-2f65-47a4-8df4-7e40db70c8d2/resourceGroups/$MY_RESOURCE_GROUP_NAME", @@ -77,7 +76,6 @@ az storage account create --name $MY_STORAGE_ACCOUNT_NAME --resource-group $MY_R Results: - ```json { "accessTier": "Hot", @@ -188,7 +186,6 @@ az storage container create --name images --account-name $MY_STORAGE_ACCOUNT_NAM Results: - ```json { "created": true @@ -228,7 +225,6 @@ az postgres flexible-server create \ Results: - ```json { "connectionString": "postgresql://$MY_DATABASE_USERNAME:$MY_DATABASE_PASSWORD@$MY_DATABASE_NAME.postgres.database.azure.com/flexibleserverdb?sslmode=require", @@ -271,7 +267,6 @@ az cognitiveservices account create \ Results: - ```json { "etag": "\"090ac83c-0000-0700-0000-64d4fcd80000\"", @@ -449,7 +444,6 @@ az containerapp show --name $MY_CONTAINER_APP_NAME --resource-group $MY_RESOURCE Results: - ```json { "id": "/subscriptions/eb9d8265-2f64-47a4-8df4-7e41db70c8d8/resourceGroups/cn-test3/providers/Microsoft.App/containerapps/cntestcontainerapp17", @@ -578,7 +572,6 @@ az postgres flexible-server firewall-rule create \ Results: - ```json { "endIpAddress": "20.237.221.47", From f93a313fcbe073111e766e1e5514111dd9ea9f03 Mon Sep 17 00:00:00 2001 From: Ralph Rouhana Date: Sat, 19 Aug 2023 13:43:22 -0400 Subject: [PATCH 121/226] Add instruction to install containerapp extension --- .../README.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md b/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md index d44bfa75..06f175ad 100644 --- a/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md +++ b/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md @@ -413,7 +413,13 @@ export COMPUTER_VISION_KEY=$(az cognitiveservices account keys list --name $MY_C ## Deploy the code into a Container App -Now that we've got our storage, database, and Computer Vision resources all set up, we are ready to deploy the application code. To do this, we're going to use Azure Container Apps to host a containerized build of our Next.js app. The `Dockerfile` is already created at the root of the repository, so all we need to do is run a single command to deploy the code. This command will create an Azure Container Registry resource to host our Docker image, an Azure Container App resource which runs the image, and an Azure Container App Environment resource for our image. Let's break down what we're passing into the command. +Now that we've got our storage, database, and Computer Vision resources all set up, we are ready to deploy the application code. To do this, we're going to use Azure Container Apps to host a containerized build of our Next.js app. The `Dockerfile` is already created at the root of the repository, so all we need to do is run a single command to deploy the code. Before running this command, we first need to install the containerapp extension for the Azure CLI. + +```bash +az extension add --upgrade -n containerapp +``` + +This command will create an Azure Container Registry resource to host our Docker image, an Azure Container App resource which runs the image, and an Azure Container App Environment resource for our image. Let's break down what we're passing into the command. - The basics: resource name, resource group, and the region - The name of the Azure Container App Environment resource to use or create @@ -427,6 +433,8 @@ az containerapp up \ --environment $MY_CONTAINER_APP_ENV_NAME \ --context-path . \ --source . \ + --target-port 3000 \ + --ingress external \ --env-vars \ AZURE_DATABASE_URL=$DATABASE_URL \ AZURE_COMPUTER_VISION_KEY=$COMPUTER_VISION_KEY \ @@ -612,7 +620,11 @@ az storage cors add \ --account-key $STORAGE_ACCOUNT_KEY ``` -That's it! Feel free to access the newly deployed web app in your browser using the $CONTAINER_APP_URL environment variable. +That's it! Feel free to access the newly deployed web app in your browser printing the CONTAINER_APP_URL environment variable we added earlier. + +```bash +echo $CONTAINER_APP_URL +``` ## Next Steps From af0639314a4c152bc4285f803e4eeee3d72d82c7 Mon Sep 17 00:00:00 2001 From: Ralph Rouhana Date: Sat, 19 Aug 2023 15:58:41 -0400 Subject: [PATCH 122/226] Added cd instruction after cloning repo --- .../CreateContainerAppDeploymentFromSource/README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md b/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md index 06f175ad..0962bafd 100644 --- a/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md +++ b/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md @@ -34,7 +34,13 @@ First, we're going to clone this repository onto our local machines. This will p git clone https://github.com/Azure/computer-vision-nextjs-webapp.git ``` -Once cloned, navigate to the root of the repo in your terminal. To preserve saved environment variables, it's important that this terminal window stays open for the duration of the deployment. +Once cloned, navigate to the root of the repo in your terminal. + +```bash +cd computer-vision-nextjs-webapp +``` + +To preserve saved environment variables, it's important that this terminal window stays open for the duration of the deployment. ## Login to Azure using the CLI @@ -443,7 +449,7 @@ az containerapp up \ AZURE_STORAGE_ACCOUNT_KEY=$STORAGE_ACCOUNT_KEY ``` -We can verify that the command was successfull by using: +We can verify that the command was successful by using: ```bash az containerapp show --name $MY_CONTAINER_APP_NAME --resource-group $MY_RESOURCE_GROUP_NAME From 063baaf8377ebdc8d75957fb6e0c4f5f97e0e09e Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 22 Aug 2023 12:51:07 -0700 Subject: [PATCH 123/226] [add] test for markdown title parsing. --- internal/parsers/markdown_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 internal/parsers/markdown_test.go diff --git a/internal/parsers/markdown_test.go b/internal/parsers/markdown_test.go new file mode 100644 index 00000000..e3d7240f --- /dev/null +++ b/internal/parsers/markdown_test.go @@ -0,0 +1,19 @@ +package parsers + +import ( + "testing" +) + +func TestParsingTitle(t *testing.T) { + markdown := []byte(`# Hello World`) + document := ParseMarkdownIntoAst(markdown) + title, err := ExtractScenarioTitleFromAst(document, markdown) + + if err != nil { + t.Errorf("Error parsing title: %s", err) + } + + if title != "Hello World" { + t.Errorf("Title is wrong: %s", title) + } +} From d58465082001230bfc95045b21915ebcf57c33e0 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 22 Aug 2023 12:54:57 -0700 Subject: [PATCH 124/226] [add] first tests for markdown parsing. --- internal/parsers/test_markdown.go | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 internal/parsers/test_markdown.go diff --git a/internal/parsers/test_markdown.go b/internal/parsers/test_markdown.go new file mode 100644 index 00000000..434b8b25 --- /dev/null +++ b/internal/parsers/test_markdown.go @@ -0,0 +1,35 @@ +package parsers + +import ( + "testing" +) + +func TestParsingMarkdownTitle(t *testing.T) { + // Handle when title is present + markdown := []byte(`# Hello World`) + document := ParseMarkdownIntoAst(markdown) + title, err := ExtractScenarioTitleFromAst(document, markdown) + + if err != nil { + t.Errorf("Error parsing title: %s", err) + } + + if title != "Hello World" { + t.Errorf("Title is wrong: %s", title) + } + + // Handle when title is not present + markdown = []byte(``) + + document = ParseMarkdownIntoAst(markdown) + title, err = ExtractScenarioTitleFromAst(document, markdown) + + if err == nil { + t.Errorf("Error should have been thrown") + } + + if title != "" { + t.Errorf("Title should be empty") + } + +} From 5529e3febe1cd2626dd0f6b61795702670efca8e Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 22 Aug 2023 13:24:04 -0700 Subject: [PATCH 125/226] [rm] invalid test file. --- internal/parsers/test_markdown.go | 35 ------------------------------- 1 file changed, 35 deletions(-) delete mode 100644 internal/parsers/test_markdown.go diff --git a/internal/parsers/test_markdown.go b/internal/parsers/test_markdown.go deleted file mode 100644 index 434b8b25..00000000 --- a/internal/parsers/test_markdown.go +++ /dev/null @@ -1,35 +0,0 @@ -package parsers - -import ( - "testing" -) - -func TestParsingMarkdownTitle(t *testing.T) { - // Handle when title is present - markdown := []byte(`# Hello World`) - document := ParseMarkdownIntoAst(markdown) - title, err := ExtractScenarioTitleFromAst(document, markdown) - - if err != nil { - t.Errorf("Error parsing title: %s", err) - } - - if title != "Hello World" { - t.Errorf("Title is wrong: %s", title) - } - - // Handle when title is not present - markdown = []byte(``) - - document = ParseMarkdownIntoAst(markdown) - title, err = ExtractScenarioTitleFromAst(document, markdown) - - if err == nil { - t.Errorf("Error should have been thrown") - } - - if title != "" { - t.Errorf("Title should be empty") - } - -} From 4a5deac4f877dce56dc6622ef600342d5a3854e6 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 22 Aug 2023 13:29:06 -0700 Subject: [PATCH 126/226] [update] deps. --- go.mod | 18 ++++++++---------- go.sum | 17 +++++++++-------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/go.mod b/go.mod index 685a36ae..e2a5b65d 100644 --- a/go.mod +++ b/go.mod @@ -4,14 +4,21 @@ go 1.20 require ( github.com/charmbracelet/lipgloss v0.7.1 + github.com/google/uuid v1.3.0 + github.com/labstack/echo/v4 v4.10.2 + github.com/sergi/go-diff v1.3.1 + github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.7.0 + github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 github.com/yuin/goldmark v1.5.4 gopkg.in/ini.v1 v1.67.0 + k8s.io/api v0.27.1 + k8s.io/apimachinery v0.27.1 + k8s.io/client-go v0.27.1 ) require ( github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/creack/pty v1.1.18 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/go-logr/logr v1.2.3 // indirect @@ -24,13 +31,10 @@ require ( github.com/google/gnostic v0.5.7-v3refs // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/google/gofuzz v1.1.0 // indirect - github.com/google/uuid v1.3.0 // indirect github.com/imdario/mergo v0.3.6 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/kr/pty v1.1.8 // indirect - github.com/labstack/echo/v4 v4.10.2 // indirect github.com/labstack/gommon v0.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect @@ -43,13 +47,10 @@ require ( github.com/muesli/termenv v0.15.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/rivo/uniseg v0.4.4 // indirect - github.com/sergi/go-diff v1.3.1 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.8.2 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect golang.org/x/crypto v0.6.0 // indirect golang.org/x/net v0.8.0 // indirect golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect @@ -62,9 +63,6 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.27.1 // indirect - k8s.io/apimachinery v0.27.1 // indirect - k8s.io/client-go v0.27.1 // indirect k8s.io/klog/v2 v2.90.1 // indirect k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a // indirect k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect diff --git a/go.sum b/go.sum index a186dc38..0fdfcfb1 100644 --- a/go.sum +++ b/go.sum @@ -44,11 +44,7 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -71,6 +67,7 @@ github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTr github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= @@ -129,6 +126,7 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -152,10 +150,10 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/labstack/echo/v4 v4.10.2 h1:n1jAhnq/elIFTHr1EYpiYtyKgx4RW9ccVgkqByZaN2M= github.com/labstack/echo/v4 v4.10.2/go.mod h1:OEyqf2//K1DFdE57vw2DRgWY0M7s65IVQO2FzvI4J5k= @@ -186,6 +184,8 @@ github.com/muesli/termenv v0.15.1 h1:UzuTb/+hhlBugQz28rpzey4ZuKcZ03MeKsoG7IJZIxs github.com/muesli/termenv v0.15.1/go.mod h1:HeAQPTzpfs016yGtA4g00CsdYnVLJvxsS4ANqrZs2sQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk= +github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -194,6 +194,7 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= @@ -367,8 +368,6 @@ golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 h1:vVKdlvoWBphwdxWKrFZEuM0kGgGLxUOYcY4U/2Vjg44= -golang.org/x/time v0.0.0-20220210224613-90d013bbcef8/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -413,6 +412,7 @@ golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -500,6 +500,7 @@ google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= From adade90c1f510715aeab56181127ec06d49007b0 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 22 Aug 2023 13:29:19 -0700 Subject: [PATCH 127/226] [update] test runner and add test for parsing a basic codeblock. --- Makefile | 3 ++- internal/parsers/markdown_test.go | 38 ++++++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 01445a23..f54a5949 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build-ie build-api build-all run-ie run-api clean +.PHONY: build-ie build-api build-all run-ie run-api clean test-all BINARY_DIR := bin IE_BINARY := $(BINARY_DIR)/ie @@ -24,6 +24,7 @@ build-all: build-ie build-api build-runner test-all: @echo "Running all tests..." + @go clean -testcache @go test -v ./... # ------------------------------- Run targets ---------------------------------- diff --git a/internal/parsers/markdown_test.go b/internal/parsers/markdown_test.go index e3d7240f..013abf95 100644 --- a/internal/parsers/markdown_test.go +++ b/internal/parsers/markdown_test.go @@ -1,10 +1,12 @@ package parsers import ( + "fmt" "testing" ) -func TestParsingTitle(t *testing.T) { +func TestParsingMarkdownTitle(t *testing.T) { + // Handle when title is present markdown := []byte(`# Hello World`) document := ParseMarkdownIntoAst(markdown) title, err := ExtractScenarioTitleFromAst(document, markdown) @@ -16,4 +18,38 @@ func TestParsingTitle(t *testing.T) { if title != "Hello World" { t.Errorf("Title is wrong: %s", title) } + + // Handle when title is not present + markdown = []byte(``) + + document = ParseMarkdownIntoAst(markdown) + title, err = ExtractScenarioTitleFromAst(document, markdown) + + if err == nil { + t.Errorf("Error should have been thrown") + } + + if title != "" { + t.Errorf("Title should be empty") + } + +} + +func TestParsingMarkdownCodeBlocks(t *testing.T) { + markdown := []byte(fmt.Sprintf("# Hello World\n ```bash\n%s\n```", "echo Hello")) + + document := ParseMarkdownIntoAst(markdown) + codeBlocks := ExtractCodeBlocksFromAst(document, markdown, []string{"bash"}) + + if len(codeBlocks) != 1 { + t.Errorf("Code block count is wrong: %d", len(codeBlocks)) + } + + if codeBlocks[0].Language != "bash" { + t.Errorf("Code block language is wrong: %s", codeBlocks[0].Language) + } + + if codeBlocks[0].Content != "echo Hello\n" { + t.Errorf("Code block code is wrong. Expected: %s, Got %s", "echo Hello\\n", codeBlocks[0].Content) + } } From 1536c66bc2a487b8a3b0dd2e352d308da2257a63 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 22 Aug 2023 17:05:00 -0700 Subject: [PATCH 128/226] [update] test structure and add a new test. --- internal/parsers/markdown_test.go | 58 ++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/internal/parsers/markdown_test.go b/internal/parsers/markdown_test.go index 013abf95..042dd0e0 100644 --- a/internal/parsers/markdown_test.go +++ b/internal/parsers/markdown_test.go @@ -5,34 +5,50 @@ import ( "testing" ) -func TestParsingMarkdownTitle(t *testing.T) { - // Handle when title is present - markdown := []byte(`# Hello World`) - document := ParseMarkdownIntoAst(markdown) - title, err := ExtractScenarioTitleFromAst(document, markdown) +func TestParsingScenarioTitles(t *testing.T) { - if err != nil { - t.Errorf("Error parsing title: %s", err) - } + t.Run("scenario with valid title", func(t *testing.T) { + markdown := []byte(`# Hello World`) + document := ParseMarkdownIntoAst(markdown) + title, err := ExtractScenarioTitleFromAst(document, markdown) - if title != "Hello World" { - t.Errorf("Title is wrong: %s", title) - } + if err != nil { + t.Errorf("Error parsing title: %s", err) + } - // Handle when title is not present - markdown = []byte(``) + if title != "Hello World" { + t.Errorf("Title is wrong: %s", title) + } + }) - document = ParseMarkdownIntoAst(markdown) - title, err = ExtractScenarioTitleFromAst(document, markdown) + t.Run("scenario with multiple titles", func(t *testing.T) { + markdown := []byte("# Hello World \n # Hello again") + document := ParseMarkdownIntoAst(markdown) + title, err := ExtractScenarioTitleFromAst(document, markdown) - if err == nil { - t.Errorf("Error should have been thrown") - } + if err != nil { + t.Errorf("Error parsing title: %s", err) + } - if title != "" { - t.Errorf("Title should be empty") - } + if title != "Hello World" { + t.Errorf("Title is wrong: %s", title) + } + }) + + t.Run("scenario without a title", func(t *testing.T) { + markdown := []byte(``) + + document := ParseMarkdownIntoAst(markdown) + title, err := ExtractScenarioTitleFromAst(document, markdown) + + if err == nil { + t.Errorf("Error should have been thrown") + } + if title != "" { + t.Errorf("Title should be empty") + } + }) } func TestParsingMarkdownCodeBlocks(t *testing.T) { From a313ba2f8cf2d1bafa1a7011ff5a17c8d5a14bb3 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 22 Aug 2023 17:07:59 -0700 Subject: [PATCH 129/226] [update] test names. --- internal/parsers/markdown_test.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/internal/parsers/markdown_test.go b/internal/parsers/markdown_test.go index 042dd0e0..de9142e8 100644 --- a/internal/parsers/markdown_test.go +++ b/internal/parsers/markdown_test.go @@ -5,9 +5,8 @@ import ( "testing" ) -func TestParsingScenarioTitles(t *testing.T) { - - t.Run("scenario with valid title", func(t *testing.T) { +func TestParsingMarkdownHeaders(t *testing.T) { + t.Run("Markdown with a valid title", func(t *testing.T) { markdown := []byte(`# Hello World`) document := ParseMarkdownIntoAst(markdown) title, err := ExtractScenarioTitleFromAst(document, markdown) @@ -21,7 +20,7 @@ func TestParsingScenarioTitles(t *testing.T) { } }) - t.Run("scenario with multiple titles", func(t *testing.T) { + t.Run("Markdown with multiple titles", func(t *testing.T) { markdown := []byte("# Hello World \n # Hello again") document := ParseMarkdownIntoAst(markdown) title, err := ExtractScenarioTitleFromAst(document, markdown) @@ -35,7 +34,7 @@ func TestParsingScenarioTitles(t *testing.T) { } }) - t.Run("scenario without a title", func(t *testing.T) { + t.Run("Markdown without a title", func(t *testing.T) { markdown := []byte(``) document := ParseMarkdownIntoAst(markdown) From 0028f4f385a01c17276342da46a22af90181cbf9 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 22 Aug 2023 18:06:08 -0700 Subject: [PATCH 130/226] [update] INI parsing to return an error instead of panicking, add a test for valid INI files. --- internal/engine/scenario.go | 6 +++++- internal/parsers/ini.go | 10 +++++---- internal/parsers/ini_test.go | 41 ++++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 internal/parsers/ini_test.go diff --git a/internal/engine/scenario.go b/internal/engine/scenario.go index 635de18b..24dce6f2 100644 --- a/internal/engine/scenario.go +++ b/internal/engine/scenario.go @@ -74,7 +74,11 @@ func CreateScenarioFromMarkdown(path string, languagesToExecute []string) (*Scen logging.GlobalLogger.Infof("INI file '%s' does not exist, skipping...", markdownINI) } else { logging.GlobalLogger.Infof("INI file '%s' exists, loading...", markdownINI) - environmentVariables = parsers.ParseINIFile(markdownINI) + environmentVariables, err = parsers.ParseINIFile(markdownINI) + + if err != nil { + return nil, err + } for key, value := range environmentVariables { logging.GlobalLogger.Debugf("Setting %s=%s\n", key, value) diff --git a/internal/parsers/ini.go b/internal/parsers/ini.go index 020d278c..4c30f31a 100644 --- a/internal/parsers/ini.go +++ b/internal/parsers/ini.go @@ -1,24 +1,26 @@ package parsers import ( - "github.com/Azure/InnovationEngine/internal/logging" + "fmt" "gopkg.in/ini.v1" ) // Parses an INI file into a flat map of keys mapped to values. This reduces // the complexity of the INI file to a simple key/value store and ignores the // sections. -func ParseINIFile(filePath string) map[string]string { +func ParseINIFile(filePath string) (map[string]string, error) { iniFile, err := ini.Load(filePath) + if err != nil { - logging.GlobalLogger.Fatalf("Failed to read the INI file %s because %v", filePath, err) + return nil, fmt.Errorf("Failed to read the INI file %s because %v", filePath, err) } + data := make(map[string]string) for _, section := range iniFile.Sections() { for key, value := range section.KeysHash() { data[key] = value } } - return data + return data, nil } diff --git a/internal/parsers/ini_test.go b/internal/parsers/ini_test.go new file mode 100644 index 00000000..9cdcaa38 --- /dev/null +++ b/internal/parsers/ini_test.go @@ -0,0 +1,41 @@ +package parsers + +import ( + "os" + "testing" +) + +func TestParsingINIFiles(t *testing.T) { + + t.Run("INI with valid contents", func(t *testing.T) { + tempFile, err := os.CreateTemp("", "test") + + if err != nil { + t.Errorf("Error creating temp file: %s", err) + } + + defer os.Remove(tempFile.Name()) + + contents := []byte(`[section] + key=value`) + + if _, err := tempFile.Write(contents); err != nil { + t.Errorf("Error writing to temp file: %s", err) + } + + data, err := ParseINIFile(tempFile.Name()) + + if err != nil { + t.Errorf("Error parsing INI file: %s", err) + } + + if len(data) != 1 { + t.Errorf("Data length is wrong: %d", len(data)) + } + + if data["key"] != "value" { + t.Errorf("Data is wrong: %s", data["key"]) + } + }) + +} From db8bd65b248d6db4c9d00dfd638ed84519ebfb3e Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 22 Aug 2023 18:11:15 -0700 Subject: [PATCH 131/226] [add] tests for map utilities. --- internal/utils/maps_test.go | 46 +++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 internal/utils/maps_test.go diff --git a/internal/utils/maps_test.go b/internal/utils/maps_test.go new file mode 100644 index 00000000..f313160e --- /dev/null +++ b/internal/utils/maps_test.go @@ -0,0 +1,46 @@ +package utils + +import ( + "testing" +) + +func TestMapUtilities(t *testing.T) { + + t.Run("Copying maps", func(t *testing.T) { + original := make(map[string]string) + original["key"] = "value" + + copy := CopyMap(original) + + if len(copy) != 1 { + t.Errorf("Copy length is wrong: %d", len(copy)) + } + + if copy["key"] != "value" { + t.Errorf("Copy is wrong: %s", copy["key"]) + } + }) + + t.Run("Merging maps", func(t *testing.T) { + original := make(map[string]string) + original["key"] = "value" + + merge := make(map[string]string) + merge["key2"] = "value2" + + merged := MergeMaps(original, merge) + + if len(merged) != 2 { + t.Errorf("Merged length is wrong: %d", len(merged)) + } + + if merged["key"] != "value" { + t.Errorf("Merged is wrong: %s", merged["key"]) + } + + if merged["key2"] != "value2" { + t.Errorf("Merged is wrong: %s", merged["key2"]) + } + }) + +} From ab6048fe42602d7b4d373c36cd73f534ade3fd7a Mon Sep 17 00:00:00 2001 From: Ralph Rouhana Date: Wed, 23 Aug 2023 13:01:00 -0400 Subject: [PATCH 132/226] Env vars are auto generated, go bash only splits on first = --- internal/shells/bash.go | 2 +- .../README.md | 57 ++++++++++--------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/internal/shells/bash.go b/internal/shells/bash.go index e6392a2f..7413c32e 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -33,7 +33,7 @@ func loadEnvFile(path string) (map[string]string, error) { for scanner.Scan() { line := scanner.Text() if strings.Contains(line, "=") { - parts := strings.Split(line, "=") + parts := strings.SplitN(line, "=", 2) // Split at the first "=" only env[parts[0]] = parts[1] } } diff --git a/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md b/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md index 0962bafd..7205ac83 100644 --- a/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md +++ b/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md @@ -13,17 +13,18 @@ Note: If you've never created a Computer Vision resource before, you will not be The first step in this tutorial is to define environment variables. **Replace the values on the right with your own unique values.** These values will be used throughout the tutorial to create resources and configure the application. Use lowercase and no special characters for the storage account name. -```plaintext -export MY_RESOURCE_GROUP_NAME= +```bash +export SUFFIX=$(cat /dev/urandom | LC_ALL=C tr -dc 'a-z0-9' | fold -w 8 | head -n 1) +export MY_RESOURCE_GROUP_NAME=rg$SUFFIX export MY_LOCATION=westus -export MY_STORAGE_ACCOUNT_NAME= -export MY_DATABASE_SERVER_NAME= -export MY_DATABASE_NAME= -export MY_DATABASE_USERNAME= -export MY_DATABASE_PASSWORD= -export MY_COMPUTER_VISION_NAME= -export MY_CONTAINER_APP_NAME= -export MY_CONTAINER_APP_ENV_NAME= +export MY_STORAGE_ACCOUNT_NAME=storage$SUFFIX +export MY_DATABASE_SERVER_NAME=dbserver$SUFFIX +export MY_DATABASE_NAME=db$SUFFIX +export MY_DATABASE_USERNAME=dbuser$SUFFIX +export MY_DATABASE_PASSWORD=dbpass$SUFFIX +export MY_COMPUTER_VISION_NAME=computervision$SUFFIX +export MY_CONTAINER_APP_NAME=containerapp$SUFFIX +export MY_CONTAINER_APP_ENV_NAME=containerappenv$SUFFIX ``` ## Clone the sample repository @@ -460,12 +461,12 @@ Results: ```json { - "id": "/subscriptions/eb9d8265-2f64-47a4-8df4-7e41db70c8d8/resourceGroups/cn-test3/providers/Microsoft.App/containerapps/cntestcontainerapp17", + "id": "/subscriptions/fake3265-2f64-47a4-8df4-7e41ab70c8dh/resourceGroups/$MY_RESOURCE_GROUP_NAME/providers/Microsoft.App/containerapps/$MY_CONTAINER_APP_NAME", "identity": { "type": "None" }, "location": "West US", - "name": "cntestcontainerapp17", + "name": "$MY_CONTAINER_APP_NAME", "properties": { "configuration": { "activeRevisionsMode": "Single", @@ -477,7 +478,7 @@ Results: "customDomains": null, "exposedPort": 0, "external": true, - "fqdn": "cntestcontainerapp17.kindocean-a506af76.westus.azurecontainerapps.io", + "fqdn": "$MY_CONTAINER_APP_NAME.kindocean-a506af76.$MY_LOCATION.azurecontainerapps.io", "ipSecurityRestrictions": null, "stickySessions": null, "targetPort": 3000, @@ -495,12 +496,12 @@ Results: "service": null }, "customDomainVerificationId": "06C64CD176439F8B6CCBBE1B531758828A5CACEABFB30B4DC9750641532924F6", - "environmentId": "/subscriptions/eb9d8265-2f64-47a4-8df4-7e41db70c8d8/resourceGroups/cn-test3/providers/Microsoft.App/managedEnvironments/cndbcomputervisionenv11", - "eventStreamEndpoint": "https://westus.azurecontainerapps.dev/subscriptions/eb9d8265-2f64-47a4-8df4-7e41db70c8d8/resourceGroups/cn-test3/containerApps/cntestcontainerapp17/eventstream", - "latestReadyRevisionName": "cntestcontainerapp17--jl6fh75", - "latestRevisionFqdn": "cntestcontainerapp17--jl6fh75.kindocean-a506af76.westus.azurecontainerapps.io", - "latestRevisionName": "cntestcontainerapp17--jl6fh75", - "managedEnvironmentId": "/subscriptions/eb9d8265-2f64-47a4-8df4-7e41db70c8d8/resourceGroups/cn-test3/providers/Microsoft.App/managedEnvironments/cndbcomputervisionenv11", + "environmentId": "/subscriptions/fake3265-2f64-47a4-8df4-7e41ab70c8dh/resourceGroups/$MY_RESOURCE_GROUP_NAME/providers/Microsoft.App/managedEnvironments/$MY_CONTAINER_APP_ENV_NAME", + "eventStreamEndpoint": "https://$MY_LOCATION.azurecontainerapps.dev/subscriptions/eb9d8265-2f64-47a4-8df4-7e41db70c8d8/resourceGroups/$MY_RESOURCE_GROUP_NAME/containerApps/$MY_CONTAINER_APP_NAME/eventstream", + "latestReadyRevisionName": "$MY_CONTAINER_APP_NAME--jl6fh75", + "latestRevisionFqdn": "$MY_CONTAINER_APP_NAME--jl6fh75.kindocean-a506af76.$MY_LOCATION.azurecontainerapps.io", + "latestRevisionName": "$MY_CONTAINER_APP_NAME--jl6fh75", + "managedEnvironmentId": "/subscriptions/eb9d8265-2f64-47a4-8df4-7e41db70c8d8/resourceGroups/$MY_RESOURCE_GROUP_NAME/providers/Microsoft.App/managedEnvironments/$MY_CONTAINER_APP_ENV_NAME", "outboundIpAddresses": ["20.237.221.47"], "provisioningState": "Succeeded", "runningStatus": "Running", @@ -510,27 +511,27 @@ Results: "env": [ { "name": "AZURE_DATABASE_URL", - "value": "postgres://cndbadmin11:cndbscret11@cntestdb11.postgres.database.azure.com/flexibleserverdb" + "value": "$DATABASE_URL" }, { "name": "AZURE_COMPUTER_VISION_KEY", - "value": "949ad147794046baa8fc22af832c954f" + "value": "$COMPUTER_VISION_KEY" }, { "name": "AZURE_COMPUTER_VISION_ENDPOINT", - "value": "https://westus.api.cognitive.microsoft.com/" + "value": "$COMPUTER_VISION_ENDPOINT" }, { "name": "AZURE_STORAGE_ACCOUNT_NAME", - "value": "cnteststorage14" + "value": "$MY_STORAGE_ACCOUNT_NAME" }, { "name": "AZURE_STORAGE_ACCOUNT_KEY", - "value": "+b/zI8I35ZCrRgPHYsNSc2A1QiLId3aZyAoUlQboDmfVC22wPQUvus2qxqdnLcjq2+SJ7t1DxCLX+AStkCmi3Q==" + "value": "$STORAGE_ACCOUNT_KEY" } ], "image": "ralphr123/cn-app", - "name": "cntestcontainerapp17", + "name": "$MY_CONTAINER_APP_NAME", "resources": { "cpu": 0.5, "ephemeralStorage": "2Gi", @@ -551,13 +552,13 @@ Results: }, "workloadProfileName": null }, - "resourceGroup": "cn-test3", + "resourceGroup": "$MY_RESOURCE_GROUP_NAME", "systemData": { "createdAt": "2023-08-10T21:50:07.2125698", - "createdBy": "ralph.rouhana@gmail.com", + "createdBy": "username@domain.com", "createdByType": "User", "lastModifiedAt": "2023-08-10T21:50:07.2125698", - "lastModifiedBy": "ralph.rouhana@gmail.com", + "lastModifiedBy": "username@domain.com", "lastModifiedByType": "User" }, "type": "Microsoft.App/containerApps" From ee824958aef1b22b4ca83864b1eefc77a50b7929 Mon Sep 17 00:00:00 2001 From: Ralph Rouhana Date: Wed, 23 Aug 2023 13:52:56 -0400 Subject: [PATCH 133/226] Update source path for containerapp up --- .../CreateContainerAppDeploymentFromSource/README.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md b/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md index 7205ac83..632f4147 100644 --- a/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md +++ b/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md @@ -35,12 +35,6 @@ First, we're going to clone this repository onto our local machines. This will p git clone https://github.com/Azure/computer-vision-nextjs-webapp.git ``` -Once cloned, navigate to the root of the repo in your terminal. - -```bash -cd computer-vision-nextjs-webapp -``` - To preserve saved environment variables, it's important that this terminal window stays open for the duration of the deployment. ## Login to Azure using the CLI @@ -438,8 +432,8 @@ az containerapp up \ --resource-group $MY_RESOURCE_GROUP_NAME \ --location $MY_LOCATION \ --environment $MY_CONTAINER_APP_ENV_NAME \ - --context-path . \ - --source . \ + --context-path computer-vision-nextjs-webapp \ + --source computer-vision-nextjs-webapp \ --target-port 3000 \ --ingress external \ --env-vars \ From a9ee7abc51266551051e7f1779f248ed914b963a Mon Sep 17 00:00:00 2001 From: vmarcella Date: Wed, 23 Aug 2023 14:19:25 -0700 Subject: [PATCH 134/226] [update] execution to compare outputs & refactor output comparisons into the common module. --- internal/engine/common.go | 34 ++++++++++++++++++++++++++ internal/engine/execution.go | 18 +++++++++++++- internal/engine/testing.go | 40 ++++++------------------------- internal/parsers/markdown_test.go | 28 ++++++++++++---------- 4 files changed, 74 insertions(+), 46 deletions(-) diff --git a/internal/engine/common.go b/internal/engine/common.go index 04d5d6c2..98403791 100644 --- a/internal/engine/common.go +++ b/internal/engine/common.go @@ -4,7 +4,10 @@ import ( "fmt" "strings" + "github.com/Azure/InnovationEngine/internal/logging" + "github.com/Azure/InnovationEngine/internal/utils" "github.com/charmbracelet/lipgloss" + "github.com/xrash/smetrics" ) // Styles used for rendering output to the terminal. @@ -53,3 +56,34 @@ func indentMultiLineCommand(content string, indentation int) string { } return strings.Join(lines, "\n") } + +// Compares the actual output of a command to the expected output of a command. +func compareCommandOutputs(actualOutput string, expectedOutput string, expectedSimilarity float64, expectedOutputLanguage string) error { + if expectedOutputLanguage == "json" { + logging.GlobalLogger.Debugf("Comparing JSON strings:\nExpected: %s\nActual%s", expectedOutput, actualOutput) + meetsThreshold, err := utils.CompareJsonStrings(actualOutput, expectedOutput, expectedSimilarity) + + if err != nil { + return err + } + + if !meetsThreshold { + return fmt.Errorf(errorMessageStyle.Render("Expected output does not match actual output.")) + } + + score, _ := utils.ComputeJsonStringSimilarity(actualOutput, expectedOutput) + + actual, _ := utils.OrderJsonFields(actualOutput) + expected, _ := utils.OrderJsonFields(expectedOutput) + + logging.GlobalLogger.WithField("actual", actual).WithField("expected", expected).Debugf("Jaro score: %f Expected Similarity: %f", score, expectedSimilarity) + } else { + score := smetrics.JaroWinkler(expectedOutput, actualOutput, 0.7, 4) + + if expectedSimilarity > score { + return fmt.Errorf(errorMessageStyle.Render("Expected output does not match actual output.")) + } + } + + return nil +} diff --git a/internal/engine/execution.go b/internal/engine/execution.go index a3673f6f..4159a6e2 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -201,9 +201,25 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { showCursor() if commandErr == nil { + + actualOutput := commandOutput.StdOut + expectedOutput := block.ExpectedOutput.Content + expectedSimilarity := block.ExpectedOutput.ExpectedSimilarity + expectedOutputLanguage := block.ExpectedOutput.Language + + err := compareCommandOutputs(actualOutput, expectedOutput, expectedSimilarity, expectedOutputLanguage) + + if err != nil { + logging.GlobalLogger.Errorf("Error comparing command outputs: %s", err.Error()) + fmt.Printf("\r %s \n", errorStyle.Render("✗")) + moveCursorPositionDown(lines) + fmt.Printf(" %s\n", errorMessageStyle.Render(err.Error())) + fmt.Printf(" %s\n", utils.GetDifferenceBetweenStrings(block.ExpectedOutput.Content, commandOutput.StdOut)) + break loop + } + fmt.Printf("\r %s \n", checkStyle.Render("✔")) - moveCursorPositionDown(lines) if e.Configuration.Verbose { fmt.Printf(" %s\n", verboseStyle.Render(commandOutput.StdOut)) } diff --git a/internal/engine/testing.go b/internal/engine/testing.go index 94864980..4e0aa1dd 100644 --- a/internal/engine/testing.go +++ b/internal/engine/testing.go @@ -9,7 +9,6 @@ import ( "github.com/Azure/InnovationEngine/internal/parsers" "github.com/Azure/InnovationEngine/internal/shells" "github.com/Azure/InnovationEngine/internal/utils" - "github.com/xrash/smetrics" ) func (e *Engine) TestSteps(steps []Step, env map[string]string) { @@ -55,41 +54,16 @@ func (e *Engine) TestSteps(steps []Step, env map[string]string) { actualOutput := commandOutput.StdOut expectedOutput := block.ExpectedOutput.Content expectedSimilarity := block.ExpectedOutput.ExpectedSimilarity + expectedOutputLanguage := block.ExpectedOutput.Language - if block.ExpectedOutput.Language == "json" { - logging.GlobalLogger.Debugf("Comparing JSON strings:\nExpected: %s\nActual%s", expectedOutput, actualOutput) - meetsThreshold, err := utils.CompareJsonStrings(actualOutput, expectedOutput, expectedSimilarity) - if err != nil { - fmt.Printf("\r %s \n", errorStyle.Render("✗")) - fmt.Printf("\033[%dB", lines) - fmt.Printf(" %s\n", errorMessageStyle.Render(err.Error())) - break loop - } + err := compareCommandOutputs(actualOutput, expectedOutput, expectedSimilarity, expectedOutputLanguage) - if !meetsThreshold { - fmt.Printf("\r %s \n", errorStyle.Render("✗")) - fmt.Printf("\033[%dB", lines) - fmt.Printf(" %s\n", errorMessageStyle.Render("Expected output does not match actual output.")) - fmt.Printf(" %s\n", utils.GetDifferenceBetweenStrings(expectedOutput, actualOutput)) - break loop - } + if err != nil { + logging.GlobalLogger.Errorf("Error comparing command outputs: %s", err.Error()) + fmt.Printf("\r %s \n", errorStyle.Render("✗")) + moveCursorPositionDown(lines) + fmt.Printf(" %s\n", errorMessageStyle.Render(err.Error())) - if e.Configuration.Verbose { - score, _ := utils.ComputeJsonStringSimilarity(actualOutput, expectedOutput) - - actual, _ := utils.OrderJsonFields(actualOutput) - expected, _ := utils.OrderJsonFields(expectedOutput) - - logging.GlobalLogger.WithField("actual", actual).WithField("expected", expected).Debugf("JaroWinkler score: %f Expected Similarity: %f", score, expectedSimilarity) - } - } else { - score := smetrics.JaroWinkler(block.ExpectedOutput.Content, commandOutput.StdOut, 0.7, 4) - if block.ExpectedOutput.ExpectedSimilarity > score { - fmt.Printf("\r %s \n", errorStyle.Render("✗")) - fmt.Printf("\033[%dB", lines) - fmt.Printf(" %s\n", errorMessageStyle.Render("Expected output does not match actual output.")) - fmt.Printf(" %s\n", utils.GetDifferenceBetweenStrings(block.ExpectedOutput.Content, commandOutput.StdOut)) - } } fmt.Printf("\r %s \n", checkStyle.Render("✔")) diff --git a/internal/parsers/markdown_test.go b/internal/parsers/markdown_test.go index de9142e8..ad546454 100644 --- a/internal/parsers/markdown_test.go +++ b/internal/parsers/markdown_test.go @@ -51,20 +51,24 @@ func TestParsingMarkdownHeaders(t *testing.T) { } func TestParsingMarkdownCodeBlocks(t *testing.T) { - markdown := []byte(fmt.Sprintf("# Hello World\n ```bash\n%s\n```", "echo Hello")) - document := ParseMarkdownIntoAst(markdown) - codeBlocks := ExtractCodeBlocksFromAst(document, markdown, []string{"bash"}) + t.Run("Markdown with a valid bash code block", func(t *testing.T) { + markdown := []byte(fmt.Sprintf("# Hello World\n ```bash\n%s\n```", "echo Hello")) - if len(codeBlocks) != 1 { - t.Errorf("Code block count is wrong: %d", len(codeBlocks)) - } + document := ParseMarkdownIntoAst(markdown) + codeBlocks := ExtractCodeBlocksFromAst(document, markdown, []string{"bash"}) + + if len(codeBlocks) != 1 { + t.Errorf("Code block count is wrong: %d", len(codeBlocks)) + } - if codeBlocks[0].Language != "bash" { - t.Errorf("Code block language is wrong: %s", codeBlocks[0].Language) - } + if codeBlocks[0].Language != "bash" { + t.Errorf("Code block language is wrong: %s", codeBlocks[0].Language) + } + + if codeBlocks[0].Content != "echo Hello\n" { + t.Errorf("Code block code is wrong. Expected: %s, Got %s", "echo Hello\\n", codeBlocks[0].Content) + } + }) - if codeBlocks[0].Content != "echo Hello\n" { - t.Errorf("Code block code is wrong. Expected: %s, Got %s", "echo Hello\\n", codeBlocks[0].Content) - } } From 59ff2bf3cca5fe84a6c7d2dbd1f1bfc7726c166d Mon Sep 17 00:00:00 2001 From: vmarcella Date: Wed, 23 Aug 2023 14:20:24 -0700 Subject: [PATCH 135/226] [add] expected similarity blocks and format outputs. --- scenarios/ocd/CreateLinuxVMAndSSH/README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/scenarios/ocd/CreateLinuxVMAndSSH/README.md b/scenarios/ocd/CreateLinuxVMAndSSH/README.md index ab89c534..3517de77 100644 --- a/scenarios/ocd/CreateLinuxVMAndSSH/README.md +++ b/scenarios/ocd/CreateLinuxVMAndSSH/README.md @@ -27,7 +27,9 @@ az group create --name $MY_RESOURCE_GROUP_NAME --location $MY_LOCATION Results: -```expected_similarity=0.3 + +```json +{ "id": "/subscriptions/325e7c34-99fb-4190-aa87-1df746c67705/resourceGroups/myResourceGroup", "location": "eastus", "managedBy": null, @@ -37,6 +39,7 @@ Results: }, "tags": null, "type": "Microsoft.Resources/resourceGroups" +} ``` ## Create the Virtual Machine @@ -51,7 +54,9 @@ az vm create --resource-group $MY_RESOURCE_GROUP_NAME --name $MY_VM_NAME --image Results: -```expected_similarity=0.3 + +```json +{ "fqdns": "", "id": "/subscriptions/325e7c34-99fb-4190-aa87-1df746c67705/resourceGroups/myResourceGroup/providers/Microsoft.Compute/virtualMachines/myVM", "location": "eastus", @@ -61,6 +66,7 @@ Results: "publicIpAddress": "52.147.208.85", "resourceGroup": "myResourceGroup", "zones": "" +} ``` # Store IP Address of VM in order to SSH From 3f75cb70fffea253b9462795b12889655c62a365 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 24 Aug 2023 13:32:58 -0700 Subject: [PATCH 136/226] [update] logging to not include the caller for now, as logs were too verbose. --- internal/engine/execution.go | 1 + internal/logging/logging.go | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 4159a6e2..c30f9848 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -296,6 +296,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { } } + // Report the final status of the deployment (Only applies to one click deployments). ocdStatus.Status = "Succeeded" attachResourceURIsToOCDStatus(&ocdStatus, resourceGroupName, e.Configuration.Environment) reportOCDStatus(ocdStatus, e.Configuration.Environment) diff --git a/internal/logging/logging.go b/internal/logging/logging.go index 44647638..e8fd9c45 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -66,7 +66,7 @@ func Init(level Level) { DisableQuote: true, }) - GlobalLogger.SetReportCaller(true) + GlobalLogger.SetReportCaller(false) GlobalLogger.SetLevel(level.Integer()) file, err := os.OpenFile("ie.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) @@ -75,6 +75,6 @@ func Init(level Level) { GlobalLogger.SetOutput(file) } else { GlobalLogger.SetOutput(os.Stdout) - GlobalLogger.Info("Failed to log to file, using default stderr") + GlobalLogger.Warn("Failed to log to file, using default stderr") } } From b2366abda8183fd908070dffb73b2b62705c3843 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 24 Aug 2023 14:06:15 -0700 Subject: [PATCH 137/226] [add] cursor adjustment back. --- internal/engine/execution.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index c30f9848..3c79d0ab 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -219,6 +219,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { } fmt.Printf("\r %s \n", checkStyle.Render("✔")) + moveCursorPositionDown(lines) if e.Configuration.Verbose { fmt.Printf(" %s\n", verboseStyle.Render(commandOutput.StdOut)) From 12118ebb020e1a55bc85e8d47b7fb129f7e376e4 Mon Sep 17 00:00:00 2001 From: Adrian Joian <6505576+naioja@users.noreply.github.com> Date: Fri, 25 Aug 2023 20:45:58 +0200 Subject: [PATCH 138/226] AKS Best practice setup and NGINX Ingress Controller (#21) * Fix string quotes * AKS best practices * Visit the application URL by using curl * Fixing Public IP creation Correctling DNS label to match Azure regex Adding zones and label to the create command * Fix variable names * Removing AGIC from the docs * Switched to a differen random ID * Fixing codeblocks and annotations * Fixing extra space * Making sure workload is up before running curl * Fixing quotes --- scenarios/ocd/CreateAKSDeployment/README.md | 301 ++++++++++-------- .../azure-vote-agic-ssl.yml | 112 ------- .../CreateAKSDeployment/azure-vote-agic.yml | 104 ------ .../azure-vote-nginx-ssl.yml | 28 ++ .../cluster-issuer-prod.yml | 8 +- 5 files changed, 204 insertions(+), 349 deletions(-) delete mode 100644 scenarios/ocd/CreateAKSDeployment/azure-vote-agic-ssl.yml delete mode 100644 scenarios/ocd/CreateAKSDeployment/azure-vote-agic.yml create mode 100644 scenarios/ocd/CreateAKSDeployment/azure-vote-nginx-ssl.yml diff --git a/scenarios/ocd/CreateAKSDeployment/README.md b/scenarios/ocd/CreateAKSDeployment/README.md index ddd7f3c5..41160f6c 100644 --- a/scenarios/ocd/CreateAKSDeployment/README.md +++ b/scenarios/ocd/CreateAKSDeployment/README.md @@ -6,12 +6,18 @@ Welcome to this tutorial where we will take you step by step in creating an Azur The First step in this tutorial is to define environment variables ```bash -export UNIQUE_POSTFIX="$(($RANDOM % 1000 + 1))" -export MY_RESOURCE_GROUP_NAME="myResourceGroup$UNIQUE_POSTFIX" -export MY_LOCATION=eastus -export MY_AKS_CLUSTER_NAME="myAKSCluster$UNIQUE_POSTFIX" -export MY_PUBLIC_IP_NAME="myPublicIP$UNIQUE_POSTFIX" -export MY_DNS_LABEL="myAKSCluster$UNIQUE_POSTFIX" +export NETWORK_PREFIX="$(($RANDOM % 254 + 1))" +export RANDOM_ID="$(openssl rand -hex 3)" +export MY_RESOURCE_GROUP_NAME="myResourceGroup$RANDOM_ID" +export MY_LOCATION="eastus" +export MY_AKS_CLUSTER_NAME="myAKSCluster$RANDOM_ID" +export MY_PUBLIC_IP_NAME="myPublicIP$RANDOM_ID" +export MY_DNS_LABEL="mydnslabel$RANDOM_ID" +export MY_VNET_NAME="myVNet$RANDOM_ID" +export MY_VNET_PREFIX="10.$NETWORK_PREFIX.0.0/16" +export MY_SN_NAME="mySN$RANDOM_ID" +export MY_SN_PREFIX="10.$NETWORK_PREFIX.0.0/22" +export FQDN="${MY_DNS_LABEL}.${MY_LOCATION}.cloudapp.azure.com" ``` # Create a resource group @@ -23,9 +29,10 @@ az group create --name $MY_RESOURCE_GROUP_NAME --location $MY_LOCATION ``` Results: -```expected_similarity=0.3 + +```JSON { - "id": "/subscriptions/bb318642-28fd-482d-8d07-79182df07999/resourceGroups/testResourceGroup24763", + "id": "/subscriptions/bb318642-28fd-482d-8d07-79182df07999/resourceGroups/myResourceGroup210", "location": "eastus", "managedBy": null, "name": "testResourceGroup", @@ -37,6 +44,58 @@ Results: } ``` +## Create a virtual network and subnet + +A virtual network is the fundamental building block for private networks in Azure. Azure Virtual Network enables Azure resources like VMs to securely communicate with each other and the internet. + +```bash +az network vnet create \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --location $MY_LOCATION \ + --name $MY_VNET_NAME \ + --address-prefix $MY_VNET_PREFIX \ + --subnet-name $MY_SN_NAME \ + --subnet-prefixes $MY_SN_PREFIX +``` +Results: + + +```JSON +{ + "newVNet": { + "addressSpace": { + "addressPrefixes": [ + "10.210.0.0/16" + ] + }, + "enableDdosProtection": false, + "etag": "W/\"1e065114-2ae3-4dee-91eb-c69667e60afb\"", + "id": "/subscriptions/bb318642-28fd-482d-8d07-79182df07999/myResourceGroup210/providers/Microsoft.Network/virtualNetworks/myVNet210", + "location": "eastus", + "name": "myVNet210", + "provisioningState": "Succeeded", + "resourceGroup": "myResourceGroup210", + "resourceGuid": "3e54a2e8-32fa-4157-b817-f4e4507dbac9", + "subnets": [ + { + "addressPrefix": "10.210.0.0/22", + "delegations": [], + "etag": "W/\"1e065114-2ae3-4dee-91eb-c69667e60afb\"", + "id": "/subscriptions/bb318642-28fd-482d-8d07-79182df07999/myResourceGroup210/providers/Microsoft.Network/virtualNetworks/myVNet210/subnets/mySN210", + "name": "mySN210", + "privateEndpointNetworkPolicies": "Disabled", + "privateLinkServiceNetworkPolicies": "Enabled", + "provisioningState": "Succeeded", + "resourceGroup": "myResourceGroup210", + "type": "Microsoft.Network/virtualNetworks/subnets" + } + ], + "type": "Microsoft.Network/virtualNetworks", + "virtualNetworkPeerings": [] + } +} +``` + ## Register to AKS Azure Resource Providers Verify Microsoft.OperationsManagement and Microsoft.OperationalInsights providers are registered on your subscription. These are Azure resource providers required to support [Container insights](https://docs.microsoft.com/en-us/azure/azure-monitor/containers/container-insights-overview). To check the registration status, run the following commands @@ -46,11 +105,28 @@ az provider register --namespace Microsoft.OperationalInsights ``` ## Create AKS Cluster -Create an AKS cluster using the az aks create command with the --enable-addons monitoring parameter to enable Container insights. The following example creates a cluster named myAKSCluster with one node: +Create an AKS cluster using the az aks create command with the --enable-addons monitoring parameter to enable Container insights. The following example creates an autoscaling, availability zone enabled cluster named myAKSCluster: This will take a few minutes ```bash -az aks create --resource-group $MY_RESOURCE_GROUP_NAME --name $MY_AKS_CLUSTER_NAME --node-count 1 --enable-addons monitoring --generate-ssh-keys +export MY_SN_ID=$(az network vnet subnet list --resource-group $MY_RESOURCE_GROUP_NAME --vnet-name $MY_VNET_NAME --query "[0].id" --output tsv) + +az aks create \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --name $MY_AKS_CLUSTER_NAME \ + --auto-upgrade-channel stable \ + --enable-cluster-autoscaler \ + --enable-addons monitoring \ + --location $MY_LOCATION \ + --node-count 1 \ + --min-count 1 \ + --max-count 3 \ + --network-plugin azure \ + --network-policy azure \ + --vnet-subnet-id $MY_SN_ID \ + --no-ssh-key \ + --node-vm-size Standard_DS2_v2 \ + --zones 1 2 3 ``` ## Connect to the cluster @@ -70,7 +146,7 @@ if ! [ -x "$(command -v kubectl)" ]; then az aks install-cli; fi > This will overwrite any existing credentials with the same entry ```bash -az aks get-credentials --resource-group $RESOURCE_GROUP_NAME --name $AKS_CLUSTER_NAME --overwrite-existing +az aks get-credentials --resource-group $MY_RESOURCE_GROUP_NAME --name $MY_AKS_CLUSTER_NAME --overwrite-existing ``` 3. Verify the connection to your cluster using the kubectl get command. This command returns a list of the cluster nodes. @@ -82,7 +158,8 @@ kubectl get nodes ## Install NGINX Ingress Controller ```bash -export MY_STATIC_IP=$(az network public-ip create --resource-group MC_${MY_RESOURCE_GROUP_NAME}_${MY_AKS_CLUSTER_NAME}_${MY_LOCATION} --name ${MY_PUBLIC_IP_NAME} --sku Standard --allocation-method static --query publicIp.ipAddress -o tsv) +export MY_STATIC_IP=$(az network public-ip create --resource-group MC_${MY_RESOURCE_GROUP_NAME}_${MY_AKS_CLUSTER_NAME}_${MY_LOCATION} --location ${MY_LOCATION} --name ${MY_PUBLIC_IP_NAME} --dns-name ${MY_DNS_LABEL} --sku Standard --allocation-method static --version IPv4 --zone 1 2 3 --query publicIp.ipAddress -o tsv) + helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx helm repo update helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \ @@ -116,124 +193,51 @@ kubectl apply -f azure-vote-start.yml ## Test The Application Validate that the application is running by either visiting the public ip or the application url. The application url can be found by running the following command: -```bash -echo "http://${MY_DNS_LABEL}.${MY_LOCATION}.cloudapp.azure.com" -``` - -# Add Application Gateway Ingress Controller -The Application Gateway Ingress Controller (AGIC) is a Kubernetes application, which makes it possible for Azure Kubernetes Service (AKS) customers to leverage Azure's native Application Gateway L7 load-balancer to expose cloud software to the Internet. AGIC monitors the Kubernetes cluster it is hosted on and continuously updates an Application Gateway, so that selected services are exposed to the Internet - -AGIC helps eliminate the need to have another load balancer/public IP in front of the AKS cluster and avoids multiple hops in your datapath before requests reach the AKS cluster. Application Gateway talks to pods using their private IP directly and does not require NodePort or KubeProxy services. This also brings better performance to your deployments. - -## Deploy a new Application Gateway -1. Create a Public IP for Application Gateway by running the following: -```bash -az network public-ip create --name $PUBLIC_IP_NAME --resource-group $RESOURCE_GROUP_NAME --allocation-method Static --sku Standard -``` - -2. Create a Virtual Network(Vnet) for Application Gateway by running the following: -```bash -az network vnet create --name $VNET_NAME --resource-group $RESOURCE_GROUP_NAME --address-prefix 11.0.0.0/8 --subnet-name $SUBNET_NAME --subnet-prefix 11.1.0.0/16 -``` - -3. Create Application Gateway by running the following: - -> [!NOTE] -> This will take around 5 minutes -```bash -az network application-gateway create --name $APPLICATION_GATEWAY_NAME --location $RESOURCE_LOCATION --resource-group $RESOURCE_GROUP_NAME --sku Standard_v2 --public-ip-address $PUBLIC_IP_NAME --vnet-name $VNET_NAME --subnet $SUBNET_NAME --priority 100 -``` - -## Enable the AGIC add-on in existing AKS cluster - -1. Store Application Gateway ID by running the following: -```bash -APPLICATION_GATEWAY_ID=$(az network application-gateway show --name $APPLICATION_GATEWAY_NAME --resource-group $RESOURCE_GROUP_NAME --output tsv --query "id") -``` - -2. Enable Application Gateway Ingress Addon by running the following: - -> [!NOTE] -> This will take a few minutes -```bash -az aks enable-addons --name $AKS_CLUSTER_NAME --resource-group $RESOURCE_GROUP_NAME --addon ingress-appgw --appgw-id $APPLICATION_GATEWAY_ID -``` - -3. Store the node resource as an environment variable group by running the following: -```bash -NODE_RESOURCE_GROUP=$(az aks show --name myAKSCluster --resource-group $RESOURCE_GROUP_NAME --output tsv --query "nodeResourceGroup") -``` -4. Store the Vnet name as an environment variable by running the following: -```bash -AKS_VNET_NAME=$(az network vnet list --resource-group $NODE_RESOURCE_GROUP --output tsv --query "[0].name") -``` -5. Store the Vnet ID as an environment variable by running the following: +>[!Note] +>It often takes 2-3 minutes for the PODs to be created and the site to be reachable via http ```bash -AKS_VNET_ID=$(az network vnet show --name $AKS_VNET_NAME --resource-group $NODE_RESOURCE_GROUP --output tsv --query "id") -``` -## Peer the two virtual networks together -Since we deployed the AKS cluster in its own virtual network and the Application Gateway in another virtual network, you'll need to peer the two virtual networks together in order for traffic to flow from the Application Gateway to the pods in the cluster. Peering the two virtual networks requires running the Azure CLI command two separate times, to ensure that the connection is bi-directional. The first command will create a peering connection from the Application Gateway virtual network to the AKS virtual network; the second command will create a peering connection in the other direction. - -1. Create the peering from Application Gateway to AKS by runnig the following: -```bash -az network vnet peering create --name $APPGW_TO_AKS_PEERING_NAME --resource-group $RESOURCE_GROUP_NAME --vnet-name $VNET_NAME --remote-vnet $AKS_VNET_ID --allow-vnet-access -``` +runtime="5 minute"; endtime=$(date -ud "$runtime" +%s); while [[ $(date -u +%s) -le $endtime ]]; do STATUS=$(kubectl get pods -l app=azure-vote-front -o 'jsonpath={..status.conditions[?(@.type=="Ready")].status}'); echo $STATUS; if [ "$STATUS" = 'True' ]; then break; else sleep 10; fi; done -2. Store Id of Application Gateway Vnet As enviornment variable by running the following: -```bash -APPLICATION_GATEWAY_VNET_ID=$(az network vnet show --name $VNET_NAME --resource-group $RESOURCE_GROUP_NAME --output tsv --query "id") -``` -3. Create Vnet Peering from AKS to Application Gateway -```bash -az network vnet peering create --name $AKS_TO_APPGW_PEERING_NAME --resource-group $NODE_RESOURCE_GROUP --vnet-name $AKS_VNET_NAME --remote-vnet $APPLICATION_GATEWAY_VNET_ID --allow-vnet-access -``` -4. Store New IP address as environment variable by running the following command: -```bash -runtime="2 minute"; endtime=$(date -ud "$runtime" +%s); while [[ $(date -u +%s) -le $endtime ]]; do export IP_ADDRESS=$(az network public-ip show --resource-group $RESOURCE_GROUP_NAME --name $PUBLIC_IP_NAME --query ipAddress --output tsv); if ! [ -z $IP_ADDRESS ]; then break; else sleep 10; fi; done -``` - -## Apply updated application YAML complete with AGIC -In order to use the Application Gateway Ingress Controller we deployed we need to re-deploy an update Voting App YML file. The following command will update the application: - -The full updated YML file can be viewed at `azure-vote-agic-yml` -```bash -kubectl apply -f azure-vote-agic.yml -``` - -## Check that the application is reachable -Now that the Application Gateway is set up to serve traffic to the AKS cluster, let's verify that your application is reachable. - -Check that the sample application you created is up and running by either visiting the IP address of the Application Gateway that get from running the following command or check with curl. It may take Application Gateway a minute to get the update, so if the Application Gateway is still in an "Updating" state on Portal, then let it finish before trying to reach the IP address. Run the following to check the status: -```bash -kubectl get ingress -``` - -## Add custom subdomain to AGIC -Now Application Gateway Ingress has been added to the application gateway the next step is to add a custom domain. This will allow the endpoint to be reached by a human readable URL as well as allow for SSL Termination at the endpoint. - -1. Store Unique ID of the Public IP Address as an environment variable by running the following: -```bash -export PUBLIC_IP_ID=$(az network public-ip list --query "[?ipAddress!=null]|[?contains(ipAddress, '$IP_ADDRESS')].[id]" --output tsv) -``` - -2. Update public IP to respond to custom domain requests by running the following: -```bash -az network public-ip update --ids $PUBLIC_IP_ID --dns-name $CUSTOM_DOMAIN_NAME -``` - -3. Validate the resource is reachable via the custom domain. -```bash -az network public-ip show --ids $PUBLIC_IP_ID --query "[dnsSettings.fqdn]" --output tsv +curl "http://$FQDN" ``` +Results: -4. Store the custom domain as en enviornment variable. This will be used later when setting up https termination. -```bash -export FQDN=$(az network public-ip show --ids $PUBLIC_IP_ID --query "[dnsSettings.fqdn]" --output tsv) + +```HTML + + + + + Azure Voting App + + + + + +
+
+ +
+
+ + + +
+
+
Cats - 0 | Dogs - 0
+ +
+
+ + ``` # Add HTTPS termination to custom domain -At this point in the tutorial you have an AKS web app with Application Gateway as the Ingress controller and a custom domain you can use to access your application. The next step is to add an SSL certificate to the domain so that users can reach your application securely via https. +At this point in the tutorial you have an AKS web app with NGINX as the Ingress controller and a custom domain you can use to access your application. The next step is to add an SSL certificate to the domain so that users can reach your application securely via https. ## Set Up Cert Manager In order to add HTTPS we are going to use Cert Manager. Cert Manager is an open source tool used to obtain and manage SSL certificate for Kubernetes deployments. Cert Manager will obtain certificates from a variety of Issuers, both popular public Issuers as well as private Issuers, and ensure the certificates are valid and up-to-date, and will attempt to renew certificates at a configured time before expiry. @@ -248,7 +252,6 @@ kubectl create namespace cert-manager kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.7.0/cert-manager.crds.yaml ``` - 3. Add the certmanager.k8s.io/disable-validation: "true" label to the cert-manager namespace by running the following. This will allow the system resources that cert-manager requires to bootstrap TLS to be created in its own namespace. ```bash kubectl label namespace cert-manager certmanager.k8s.io/disable-validation=true @@ -279,16 +282,16 @@ helm install cert-manager jetstack/cert-manager --namespace cert-manager --versi ClusterIssuers are Kubernetes resources that represent certificate authorities (CAs) that are able to generate signed certificates by honoring certificate signing requests. All cert-manager certificates require a referenced issuer that is in a ready condition to attempt to honor the request. - The issuer we are using can be found in the `cluster-issuer-prod.yaml file` + The issuer we are using can be found in the `cluster-issuer-prod.yml file` ```bash -envsubst < cluster-issuer-prod.yaml | kubectl apply -f - +envsubst < cluster-issuer-prod.yml | kubectl apply -f - ``` 5. Upate Voting App Application to use Cert-Manager to obtain an SSL Certificate. - The full YAML file can be found in `azure-vote-agic-ssl-yml` + The full YAML file can be found in `azure-vote-nginx-ssl.yml` ```bash -envsubst < azure-vote-agic-ssl.yml | kubectl apply -f - +envsubst < azure-vote-nginx-ssl.yml | kubectl apply -f - ``` ## Validate application is working @@ -302,10 +305,10 @@ Validate SSL certificate is True by running the follow command: ```bash kubectl get certificate --output jsonpath={..status.conditions[0].status} ``` - Results: -```expected_similarity=0.8 + +```ASCII True ``` @@ -315,9 +318,45 @@ Run the following command to get the HTTPS endpoint for your application: >[!Note] > It often takes 2-3 minutes for the SSL certificate to propogate and the site to be reachable via https ```bash -echo https://$FQDN +runtime="5 minute"; endtime=$(date -ud "$runtime" +%s); while [[ $(date -u +%s) -le $endtime ]]; do STATUS=$(kubectl get svc --namespace=ingress-nginx ingress-nginx-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}'); echo $STATUS; if [ "$STATUS" = "$MY_STATIC_IP" ]; then break; else sleep 10; fi; done + +curl https://$FQDN ``` -Paste this into the browser to validate your deployment. +Results: + + +```HTML + + + + + Azure Voting App + + + + + +
+
+ +
+
+ + + +
+
+
Cats - 0 | Dogs - 0
+ +
+
+ + +``` + ## Next Steps diff --git a/scenarios/ocd/CreateAKSDeployment/azure-vote-agic-ssl.yml b/scenarios/ocd/CreateAKSDeployment/azure-vote-agic-ssl.yml deleted file mode 100644 index 3c595a31..00000000 --- a/scenarios/ocd/CreateAKSDeployment/azure-vote-agic-ssl.yml +++ /dev/null @@ -1,112 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: azure-vote-back -spec: - replicas: 1 - selector: - matchLabels: - app: azure-vote-back - template: - metadata: - labels: - app: azure-vote-back - spec: - nodeSelector: - "kubernetes.io/os": linux - containers: - - name: azure-vote-back - image: mcr.microsoft.com/oss/bitnami/redis:6.0.8 - env: - - name: ALLOW_EMPTY_PASSWORD - value: "yes" - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 250m - memory: 256Mi - ports: - - containerPort: 6379 - name: redis ---- -apiVersion: v1 -kind: Service -metadata: - name: azure-vote-back -spec: - ports: - - port: 6379 - selector: - app: azure-vote-back ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: azure-vote-front -spec: - replicas: 1 - selector: - matchLabels: - app: azure-vote-front - template: - metadata: - labels: - app: azure-vote-front - spec: - nodeSelector: - "kubernetes.io/os": linux - containers: - - name: azure-vote-front - image: mcr.microsoft.com/azuredocs/azure-vote-front:v1 - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 250m - memory: 256Mi - ports: - - containerPort: 80 - env: - - name: REDIS - value: "azure-vote-back" ---- -apiVersion: v1 -kind: Service -metadata: - name: azure-vote-front -spec: - type: - ports: - - port: 80 - selector: - app: azure-vote-front ---- -# INGRESS WITH SSL PROD -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: azure-vote-ingress-agic-ssl - annotations: - kubernetes.io/ingress.class: azure/application-gateway - kubernetes.io/tls-acme: 'true' - appgw.ingress.kubernetes.io/ssl-redirect: "true" - cert-manager.io/cluster-issuer: letsencrypt-prod -spec: - tls: - - hosts: - - $FQDN - secretName: azure-vote-agic-secret - rules: - - host: $FQDN - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: azure-vote-front - port: - number: 80 diff --git a/scenarios/ocd/CreateAKSDeployment/azure-vote-agic.yml b/scenarios/ocd/CreateAKSDeployment/azure-vote-agic.yml deleted file mode 100644 index f93cdcdc..00000000 --- a/scenarios/ocd/CreateAKSDeployment/azure-vote-agic.yml +++ /dev/null @@ -1,104 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: azure-vote-back -spec: - replicas: 1 - selector: - matchLabels: - app: azure-vote-back - template: - metadata: - labels: - app: azure-vote-back - spec: - nodeSelector: - "kubernetes.io/os": linux - containers: - - name: azure-vote-back - image: mcr.microsoft.com/oss/bitnami/redis:6.0.8 - env: - - name: ALLOW_EMPTY_PASSWORD - value: "yes" - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 250m - memory: 256Mi - ports: - - containerPort: 6379 - name: redis ---- -apiVersion: v1 -kind: Service -metadata: - name: azure-vote-back -spec: - ports: - - port: 6379 - selector: - app: azure-vote-back ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: azure-vote-front -spec: - replicas: 1 - selector: - matchLabels: - app: azure-vote-front - template: - metadata: - labels: - app: azure-vote-front - spec: - nodeSelector: - "kubernetes.io/os": linux - containers: - - name: azure-vote-front - image: mcr.microsoft.com/azuredocs/azure-vote-front:v1 - resources: - requests: - cpu: 100m - memory: 128Mi - limits: - cpu: 250m - memory: 256Mi - ports: - - containerPort: 80 - env: - - name: REDIS - value: "azure-vote-back" ---- -apiVersion: v1 -kind: Service -metadata: - name: azure-vote-front -spec: - type: - ports: - - port: 80 - selector: - app: azure-vote-front ---- -#Application Gateway Ingress -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: azure-vote-front - annotations: - kubernetes.io/ingress.class: azure/application-gateway -spec: - rules: - - http: - paths: - - path: / - backend: - service: - name: azure-vote-front - port: - number: 80 - pathType: Exact diff --git a/scenarios/ocd/CreateAKSDeployment/azure-vote-nginx-ssl.yml b/scenarios/ocd/CreateAKSDeployment/azure-vote-nginx-ssl.yml new file mode 100644 index 00000000..070e2014 --- /dev/null +++ b/scenarios/ocd/CreateAKSDeployment/azure-vote-nginx-ssl.yml @@ -0,0 +1,28 @@ +--- +# INGRESS WITH SSL PROD +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: vote-ingress + namespace: default + annotations: + kubernetes.io/tls-acme: "true" + nginx.ingress.kubernetes.io/ssl-redirect: "true" + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + ingressClassName: nginx + tls: + - hosts: + - $FQDN + secretName: azure-vote-nginx-secret + rules: + - host: $FQDN + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: azure-vote-front + port: + number: 80 diff --git a/scenarios/ocd/CreateAKSDeployment/cluster-issuer-prod.yml b/scenarios/ocd/CreateAKSDeployment/cluster-issuer-prod.yml index 3f4a9661..e4a37bab 100644 --- a/scenarios/ocd/CreateAKSDeployment/cluster-issuer-prod.yml +++ b/scenarios/ocd/CreateAKSDeployment/cluster-issuer-prod.yml @@ -17,14 +17,18 @@ spec: server: https://acme-v02.api.letsencrypt.org/directory # Secret resource used to store the account's private key. privateKeySecretRef: - name: example-issuer-account-key + name: letsencrypt # Enable the HTTP-01 challenge provider # you prove ownership of a domain by ensuring that a particular # file is present at the domain solvers: - http01: ingress: - class: azure/application-gateway + class: nginx + podTemplate: + spec: + nodeSelector: + "kubernetes.io/os": linux #EOF # References: From 4abcf43eaa5fe0f2bdb3395634d6b10921765114 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 25 Aug 2023 17:51:20 -0700 Subject: [PATCH 139/226] [add] the logic to write the command execution to the bash history file in the users home directory. --- internal/engine/engine.go | 64 ++++++++++++++++++++---------------- internal/engine/execution.go | 6 ++-- internal/engine/testing.go | 2 +- internal/shells/bash.go | 63 +++++++++++++++++++++++++++++++---- internal/utils/user.go | 30 +++++++++++++++++ 5 files changed, 126 insertions(+), 39 deletions(-) create mode 100644 internal/utils/user.go diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 85175b84..8f3051f9 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -45,61 +45,67 @@ func NewEngine(configuration EngineConfiguration) *Engine { } } -// Executes a deployment scenario. -func (e *Engine) ExecuteScenario(scenario *Scenario) error { - if e.Configuration.Subscription != "" { - command := fmt.Sprintf("az account set --subscription %s", e.Configuration.Subscription) - _, err := shells.ExecuteBashCommand(command, map[string]string{}, true, false) +func setSubscription(subscription string) error { + if subscription != "" { + command := fmt.Sprintf("az account set --subscription %s", subscription) + _, err := shells.ExecuteBashCommand(command, shells.BashCommandConfiguration{EnvironmentVariables: map[string]string{}, InteractiveCommand: false, WriteToHistory: false, InheritEnvironment: false}) + if err != nil { logging.GlobalLogger.Error("Failed to set subscription", err) return err } - logging.GlobalLogger.Infof("Set subscription to %s", e.Configuration.Subscription) - } - // Store the current directory so we can restore it later - currentDirectory, err := os.Getwd() - if err != nil { - logging.GlobalLogger.Error("Failed to get current directory", err) - return err + logging.GlobalLogger.Infof("Set subscription to %s", subscription) } + return nil +} + +func setWorkingDirectory(directory string) error { // Change working directory if specified - if e.Configuration.WorkingDirectory != "" { - err = os.Chdir(e.Configuration.WorkingDirectory) + if directory != "" { + err := os.Chdir(directory) if err != nil { logging.GlobalLogger.Error("Failed to change working directory", err) return err } - logging.GlobalLogger.Infof("Changed working directory to %s", e.Configuration.WorkingDirectory) + logging.GlobalLogger.Infof("Changed directory to %s", directory) } + return nil +} + +// Executes a deployment scenario. +func (e *Engine) ExecuteScenario(scenario *Scenario) error { + err := setSubscription(e.Configuration.Subscription) + if err != nil { + return err + } + + // Store the current directory so we can restore it later + originalDirectory, err := os.Getwd() + if err != nil { + logging.GlobalLogger.Error("Failed to get current directory", err) + return err + } + + setWorkingDirectory(e.Configuration.WorkingDirectory) // Execute the steps fmt.Println(scenarioTitleStyle.Render(scenario.Name)) e.ExecuteAndRenderSteps(scenario.Steps, utils.CopyMap(scenario.Environment)) fmt.Printf(scriptHeader.Render("# Generated bash to replicate what just happened:")+"\n%s\n", scriptText.Render(scenario.ToShellScript())) - // Restore original directory - err = os.Chdir(currentDirectory) - if err != nil { - logging.GlobalLogger.Error("Failed to restore the original working directory", err) - return err - } + setWorkingDirectory(originalDirectory) return nil } // Validates a deployment scenario. func (e *Engine) TestScenario(scenario *Scenario) error { - if e.Configuration.Subscription != "" { - command := fmt.Sprintf("az account set --subscription %s", e.Configuration.Subscription) - _, err := shells.ExecuteBashCommand(command, map[string]string{}, true, false) - if err != nil { - logging.GlobalLogger.Error("Failed to set subscription", err) - return err - } - logging.GlobalLogger.Infof("Set subscription to %s", e.Configuration.Subscription) + err := setSubscription(e.Configuration.Subscription) + if err != nil { + return err } fmt.Println(scenarioTitleStyle.Render(scenario.Name)) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 3c79d0ab..16a44b24 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -112,7 +112,7 @@ func findResourceGroupName(commandOutput string) string { // Find all the deployed resources in a resource group. func findAllDeployedResourceURIs(resourceGroup string) []string { - output, err := shells.ExecuteBashCommand("az resource list -g"+resourceGroup, map[string]string{}, true, false) + output, err := shells.ExecuteBashCommand("az resource list -g"+resourceGroup, shells.BashCommandConfiguration{EnvironmentVariables: map[string]string{}, InheritEnvironment: true, InteractiveCommand: false, WriteToHistory: true}) if err != nil { logging.GlobalLogger.Error("Failed to list deployments", err) @@ -187,7 +187,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { hideCursor() go func(block parsers.CodeBlock) { - output, err := shells.ExecuteBashCommand(block.Content, utils.CopyMap(env), true, interactiveCommand) + output, err := shells.ExecuteBashCommand(block.Content, shells.BashCommandConfiguration{EnvironmentVariables: utils.CopyMap(env), InheritEnvironment: true, InteractiveCommand: false, WriteToHistory: true}) commandOutput = output done <- err }(block) @@ -270,7 +270,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { reportOCDStatus(ocdStatus, e.Configuration.Environment) } - output, err := shells.ExecuteBashCommand(block.Content, utils.CopyMap(env), true, interactiveCommand) + output, err := shells.ExecuteBashCommand(block.Content, shells.BashCommandConfiguration{EnvironmentVariables: utils.CopyMap(env), InheritEnvironment: true, InteractiveCommand: true, WriteToHistory: true}) if err == nil { showCursor() diff --git a/internal/engine/testing.go b/internal/engine/testing.go index 4e0aa1dd..13a0b1f0 100644 --- a/internal/engine/testing.go +++ b/internal/engine/testing.go @@ -33,7 +33,7 @@ func (e *Engine) TestSteps(steps []Step, env map[string]string) { done := make(chan error) var commandOutput shells.CommandOutput go func(block parsers.CodeBlock) { - output, err := shells.ExecuteBashCommand(block.Content, utils.CopyMap(env), true, false) + output, err := shells.ExecuteBashCommand(block.Content, shells.BashCommandConfiguration{EnvironmentVariables: utils.CopyMap(env), InheritEnvironment: true, InteractiveCommand: false, WriteToHistory: true}) commandOutput = output done <- err }(block) diff --git a/internal/shells/bash.go b/internal/shells/bash.go index 7413c32e..2708cb3d 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -8,6 +8,8 @@ import ( "os/exec" "strings" + "golang.org/x/sys/unix" + "github.com/Azure/InnovationEngine/internal/utils" ) @@ -40,6 +42,29 @@ func loadEnvFile(path string) (map[string]string, error) { return env, nil } +func appendToBashHistory(command string, filePath string) error { + file, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) + if err != nil { + return fmt.Errorf("failed to open file: %w", err) + } + defer file.Close() + + // Lock the file to prevent other processes from writing to it concurrently + if err := unix.Flock(int(file.Fd()), unix.LOCK_EX); err != nil { + return fmt.Errorf("failed to lock file: %w", err) + } + defer unix.Flock(int(file.Fd()), unix.LOCK_UN) // Unlock the file when done + + // Append the command and a newline to the file + _, err = file.WriteString(command + "\n") + if err != nil { + return fmt.Errorf("failed to write to file: %w", err) + } + + return nil + +} + // Resets the stored environment variables file. func ResetStoredEnvironmentVariables() error { return os.Remove(environmentStateFile) @@ -50,8 +75,15 @@ type CommandOutput struct { StdErr string } +type BashCommandConfiguration struct { + EnvironmentVariables map[string]string + InheritEnvironment bool + InteractiveCommand bool + WriteToHistory bool +} + // Executes a bash command and returns the output or error. -func ExecuteBashCommand(command string, env map[string]string, inheritEnvironment bool, forwardInputOutput bool) (CommandOutput, error) { +func ExecuteBashCommand(command string, config BashCommandConfiguration) (CommandOutput, error) { var commandWithStateSaved = []string{ command, "IE_LAST_COMMAND_EXIT_CODE=\"$?\"", @@ -63,7 +95,7 @@ func ExecuteBashCommand(command string, env map[string]string, inheritEnvironmen var stdoutBuffer, stderrBuffer bytes.Buffer - if forwardInputOutput { + if config.InteractiveCommand { commandToExecute.Stdout = os.Stdout commandToExecute.Stderr = os.Stderr commandToExecute.Stdin = os.Stdin @@ -73,7 +105,7 @@ func ExecuteBashCommand(command string, env map[string]string, inheritEnvironmen commandToExecute.Stderr = &stderrBuffer } - if inheritEnvironment { + if config.InheritEnvironment { commandToExecute.Env = os.Environ() } @@ -84,19 +116,38 @@ func ExecuteBashCommand(command string, env map[string]string, inheritEnvironmen // isolated command calls. envFromPreviousStep, err := loadEnvFile(environmentStateFile) if err == nil { - merged := utils.MergeMaps(env, envFromPreviousStep) + merged := utils.MergeMaps(config.EnvironmentVariables, envFromPreviousStep) for k, v := range merged { commandToExecute.Env = append(commandToExecute.Env, fmt.Sprintf("%s=%s", k, v)) } } else { - for k, v := range env { + for k, v := range config.EnvironmentVariables { commandToExecute.Env = append(commandToExecute.Env, fmt.Sprintf("%s=%s", k, v)) } } + if config.WriteToHistory { + + homeDir, err := utils.GetHomeDirectory() + + if err != nil { + return CommandOutput{}, fmt.Errorf("failed to get home directory: %w", err) + } + + err = appendToBashHistory(command, homeDir+"/.bash_history") + + if err != nil { + return CommandOutput{}, fmt.Errorf("failed to write command to history: %w", err) + } + } + err = commandToExecute.Run() - if forwardInputOutput { + + // TODO(vmarcella): Find a better way to handle this. + if config.InteractiveCommand && !config.WriteToHistory { return CommandOutput{}, err + } else if config.InteractiveCommand && config.WriteToHistory { + return CommandOutput{}, fmt.Errorf("interactive commands cannot be written to history") } standardOutput, standardError := stdoutBuffer.String(), stderrBuffer.String() diff --git a/internal/utils/user.go b/internal/utils/user.go new file mode 100644 index 00000000..309d408d --- /dev/null +++ b/internal/utils/user.go @@ -0,0 +1,30 @@ +package utils + +import ( + "fmt" + "os" + "os/user" +) + +func GetHomeDirectory() (string, error) { + // Try to get home directory from user.Current() + usr, err := user.Current() + if err == nil { + return usr.HomeDir, nil + } + + // Fallback to environment variable + home, exists := os.LookupEnv("HOME") + if exists && home != "" { + return home, nil + } + + // Fallback for Windows + homeDrive, driveExists := os.LookupEnv("HOMEDRIVE") + homePath, pathExists := os.LookupEnv("HOMEPATH") + if driveExists && pathExists { + return homeDrive + homePath, nil + } + + return "", fmt.Errorf("Home directory cannot be determined") +} From c1841b975a3ef8f7cd49716c13aa19a61e68ca3d Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 25 Aug 2023 18:10:42 -0700 Subject: [PATCH 140/226] [fix] bug with interactive command error reporting. --- internal/shells/bash.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/internal/shells/bash.go b/internal/shells/bash.go index 2708cb3d..eca54b91 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -50,10 +50,11 @@ func appendToBashHistory(command string, filePath string) error { defer file.Close() // Lock the file to prevent other processes from writing to it concurrently + // and then unlock after we're done writing to it. if err := unix.Flock(int(file.Fd()), unix.LOCK_EX); err != nil { return fmt.Errorf("failed to lock file: %w", err) } - defer unix.Flock(int(file.Fd()), unix.LOCK_UN) // Unlock the file when done + defer unix.Flock(int(file.Fd()), unix.LOCK_UN) // Append the command and a newline to the file _, err = file.WriteString(command + "\n") @@ -144,10 +145,8 @@ func ExecuteBashCommand(command string, config BashCommandConfiguration) (Comman err = commandToExecute.Run() // TODO(vmarcella): Find a better way to handle this. - if config.InteractiveCommand && !config.WriteToHistory { + if config.InteractiveCommand { return CommandOutput{}, err - } else if config.InteractiveCommand && config.WriteToHistory { - return CommandOutput{}, fmt.Errorf("interactive commands cannot be written to history") } standardOutput, standardError := stdoutBuffer.String(), stderrBuffer.String() From 017b75dc9579fbe03c3b8bc5cf5254051e5a770c Mon Sep 17 00:00:00 2001 From: Rahul Gupta Date: Mon, 28 Aug 2023 15:44:01 -0700 Subject: [PATCH 141/226] [update] do not write interactive commands to history --- internal/engine/execution.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 16a44b24..0b3f072f 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -270,7 +270,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { reportOCDStatus(ocdStatus, e.Configuration.Environment) } - output, err := shells.ExecuteBashCommand(block.Content, shells.BashCommandConfiguration{EnvironmentVariables: utils.CopyMap(env), InheritEnvironment: true, InteractiveCommand: true, WriteToHistory: true}) + output, err := shells.ExecuteBashCommand(block.Content, shells.BashCommandConfiguration{EnvironmentVariables: utils.CopyMap(env), InheritEnvironment: true, InteractiveCommand: true, WriteToHistory: false}) if err == nil { showCursor() From 95e7f4cd29d283387bd96063fb74fbf7ebdd7b3e Mon Sep 17 00:00:00 2001 From: Adrian Joian <6505576+naioja@users.noreply.github.com> Date: Wed, 30 Aug 2023 17:37:18 +0200 Subject: [PATCH 142/226] Fix SSL user email address (#25) --- scenarios/ocd/CreateAKSDeployment/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/scenarios/ocd/CreateAKSDeployment/README.md b/scenarios/ocd/CreateAKSDeployment/README.md index 41160f6c..e3e3b91d 100644 --- a/scenarios/ocd/CreateAKSDeployment/README.md +++ b/scenarios/ocd/CreateAKSDeployment/README.md @@ -6,6 +6,7 @@ Welcome to this tutorial where we will take you step by step in creating an Azur The First step in this tutorial is to define environment variables ```bash +export SSL_EMAIL_ADDRESS="$(az account show --query user.name --output tsv)" export NETWORK_PREFIX="$(($RANDOM % 254 + 1))" export RANDOM_ID="$(openssl rand -hex 3)" export MY_RESOURCE_GROUP_NAME="myResourceGroup$RANDOM_ID" From 5cb2d3b55adb6d0fa6235b6c528d6992fcab6038 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 31 Aug 2023 12:17:45 -0700 Subject: [PATCH 143/226] [add] contributing guide. --- CONTRIBUTING.md | 175 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..cc3b00c3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,175 @@ + +# Contributing to InnovationEngine + +First off, thanks for taking the time to contribute! ❤️ + +All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉 + +> And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: +> - Star the project +> - Tweet about it +> - Refer this project in your project's readme +> - Mention the project at local meetups and tell your friends/colleagues + + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [I Have a Question](#i-have-a-question) +- [I Want To Contribute](#i-want-to-contribute) +- [Reporting Bugs](#reporting-bugs) +- [Suggesting Enhancements](#suggesting-enhancements) +- [Your First Code Contribution](#your-first-code-contribution) +- [Improving The Documentation](#improving-the-documentation) +- [Styleguides](#styleguides) +- [Commit Messages](#commit-messages) +- [Join The Project Team](#join-the-project-team) + + +## Code of Conduct + +This project and everyone participating in it is governed by the +[InnovationEngine Code of Conduct](https://github.com/Azure/InnovationEngine/blob/main/CODE_OF_CONDUCT.md). +By participating, you are expected to uphold this code. Please report unacceptable behavior +to mbifeld@microsoft.com. + + +## I Have a Question + +> If you want to ask a question, we assume that you have read the available [Documentation](). + +Before you ask a question, it is best to search for existing [Issues](https://github.com/Azure/InnovationEngine/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first. + +If you then still feel the need to ask a question and need clarification, we recommend the following: + +- Open an [Issue](https://github.com/Azure/InnovationEngine/issues/new). +- Provide as much context as you can about what you're running into. +- Provide project and platform versions (golang version, operating system, etc), depending on what seems relevant. + +We will then take care of the issue as soon as possible. + +## I Want To Contribute + +> ### Legal Notice +> When contributing to this project, you must agree that you have authored 100% of the content, that you have the necessary rights to the content and that the content you contribute may be provided under the project license. + +### Reporting Bugs + + +#### Before Submitting a Bug Report + +A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. + +- Make sure that you are using the latest version. +- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](). If you are looking for support, you might want to check [this section](#i-have-a-question)). +- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/Azure/InnovationEngineissues?q=label%3Abug). +- Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue. +- Collect information about the bug: +- Stack trace (Traceback) +- OS, Platform and Version (Windows, Linux, macOS, x86, ARM, etc) +- Version of the golang, make, etc depending on what seems relevant. +- Possibly your input and the output +- Can you reliably reproduce the issue? And can you also reproduce it with older versions? + + +#### How Do I Submit a Good Bug Report? + +> You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to mbifeld@microsoft.com. + + +We use GitHub issues to track bugs and errors. If you run into an issue with the project: + +- Open an [Issue](https://github.com/Azure/InnovationEngine/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.) +- Explain the behavior you would expect and the actual behavior. +- Please provide as much context as possible and describe the *reproduction steps* that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case. +- Provide the information you collected in the previous section. + +Once it's filed: + +- The project team will label the issue accordingly. +- A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced. +- If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution). + + + + +### Suggesting Enhancements + +This section guides you through submitting an enhancement suggestion for InnovationEngine, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions. + + +#### Before Submitting an Enhancement + +- Make sure that you are using the latest version. +- Read the [documentation]() carefully and find out if the functionality is already covered, maybe by an individual configuration. +- Perform a [search](https://github.com/Azure/InnovationEngine/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. +- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library. + + +#### How Do I Submit a Good Enhancement Suggestion? + +Enhancement suggestions are tracked as [GitHub issues](https://github.com/Azure/InnovationEngine/issues). + +- Use a **clear and descriptive title** for the issue to identify the suggestion. +- Provide a **step-by-step description of the suggested enhancement** in as many details as possible. +- **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you. +- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. +- **Explain why this enhancement would be useful** to most InnovationEngine users. You may also want to point out the other projects that solved it better and which could serve as inspiration. + +### Your First Code Contribution +#### Innovation Engine +To get started with developing features for the innovation engine itself, you +will need `make` & `go`. Once you have those installed and the project cloned +to a local repository, you can attempt to build the project using: + +```bash +make build-all +``` + +If the build completes, you should be able to start adding features/fixes +to the Innovation Engine codebase. Once you've added new changes, you can test +for regressions using: + +```bash +make test-all +``` + +If implementing a new feature, it is expected to add & update any necessary +tests for the changes introduced by the feature. + +If you're still looking for more information about how to build & run innovation engine, +[README](./README.md) has a more comprehensive guide for how to get started with project +development. + +#### Innovation Engine markdown scenarios + +If you are contributing to one of the markdown scenarios (Executable documents) +for innovation engine, you are expected to follow the installation steps before +updating/adding your document. This is needed because once you've made changes +or have added a new scenario, you should test your executable document by +using the innovation engine: + +```bash +ie execute +``` + +This will attempt to parse your document into an executable scenario, make sure +that the commands extracted from codeblocks execute successfully, and that +their corresponding result blocks (if any) also line up with what the command +returned. Once you get your scenario executes successfully, you should go ahead +and make a PR for it! + +### Improving The Documentation +TODO + +## Styleguides +For working on the innovation engine, `go fmt` is what is used to format the +code for the project. + +The commit style for individual commits doesn't necessarily matter as +all commits from a PR branch will be squashed and merged into the main +branch when PRs are completed. + + +## Attribution +This guide is based on the **contributing-gen**. [Make your own](https://github.com/bttger/contributing-gen)! From 192fd64cfcc9bb2d0cd8200dd68bcd71b22f5ba6 Mon Sep 17 00:00:00 2001 From: Adrian Joian <6505576+naioja@users.noreply.github.com> Date: Tue, 5 Sep 2023 19:45:43 +0200 Subject: [PATCH 144/226] anonymizing resource groups and other ids (#26) --- scenarios/ocd/CreateAKSDeployment/README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/scenarios/ocd/CreateAKSDeployment/README.md b/scenarios/ocd/CreateAKSDeployment/README.md index e3e3b91d..01d8c336 100644 --- a/scenarios/ocd/CreateAKSDeployment/README.md +++ b/scenarios/ocd/CreateAKSDeployment/README.md @@ -33,7 +33,7 @@ Results: ```JSON { - "id": "/subscriptions/bb318642-28fd-482d-8d07-79182df07999/resourceGroups/myResourceGroup210", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup210", "location": "eastus", "managedBy": null, "name": "testResourceGroup", @@ -70,19 +70,16 @@ Results: ] }, "enableDdosProtection": false, - "etag": "W/\"1e065114-2ae3-4dee-91eb-c69667e60afb\"", - "id": "/subscriptions/bb318642-28fd-482d-8d07-79182df07999/myResourceGroup210/providers/Microsoft.Network/virtualNetworks/myVNet210", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/myResourceGroup210/providers/Microsoft.Network/virtualNetworks/myVNet210", "location": "eastus", "name": "myVNet210", "provisioningState": "Succeeded", "resourceGroup": "myResourceGroup210", - "resourceGuid": "3e54a2e8-32fa-4157-b817-f4e4507dbac9", "subnets": [ { "addressPrefix": "10.210.0.0/22", "delegations": [], - "etag": "W/\"1e065114-2ae3-4dee-91eb-c69667e60afb\"", - "id": "/subscriptions/bb318642-28fd-482d-8d07-79182df07999/myResourceGroup210/providers/Microsoft.Network/virtualNetworks/myVNet210/subnets/mySN210", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/myResourceGroup210/providers/Microsoft.Network/virtualNetworks/myVNet210/subnets/mySN210", "name": "mySN210", "privateEndpointNetworkPolicies": "Disabled", "privateLinkServiceNetworkPolicies": "Enabled", From 94461a4fa2f1c5daeac4d6f6932b5744964da2dc Mon Sep 17 00:00:00 2001 From: Adrian Joian <6505576+naioja@users.noreply.github.com> Date: Tue, 5 Sep 2023 23:39:53 +0200 Subject: [PATCH 145/226] Using Ubuntu 22.04 and AAD extension for SSH (#28) --- scenarios/ocd/CreateLinuxVMAndSSH/README.md | 43 +++++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/scenarios/ocd/CreateLinuxVMAndSSH/README.md b/scenarios/ocd/CreateLinuxVMAndSSH/README.md index 3517de77..cb664bcf 100644 --- a/scenarios/ocd/CreateLinuxVMAndSSH/README.md +++ b/scenarios/ocd/CreateLinuxVMAndSSH/README.md @@ -10,7 +10,7 @@ export MY_RESOURCE_GROUP_NAME="myResourceGroup$UNIQUE_POSTFIX" export MY_LOCATION=EastUS export MY_VM_NAME="myVM$UNIQUE_POSTFIX" export MY_USERNAME=azureuser -export MY_VM_IMAGE=UbuntuLTS +export MY_VM_IMAGE="Canonical:0001-com-ubuntu-minimal-jammy:minimal-22_04-lts-gen2:latest" ``` # Login to Azure using the CLI @@ -44,12 +44,18 @@ Results: ## Create the Virtual Machine -To create a VM in this resource group we need to run a simple command, here we have provided the `--generate-ssh-keys` flag, this will cause the CLI to look for an avialable ssh key in `~/.ssh`, if one is found it will be used, otherwise one will be generated and stored in `~/.ssh`. We also provide the `--public-ip-sku Standard` flag to ensure that the machine is accessible via a public IP. Finally, we are deploying an `UbuntuLTS` image. +To create a VM in this resource group we need to run a simple command, here we have provided the `--generate-ssh-keys` flag, this will cause the CLI to look for an avialable ssh key in `~/.ssh`, if one is found it will be used, otherwise one will be generated and stored in `~/.ssh`. We also provide the `--public-ip-sku Standard` flag to ensure that the machine is accessible via a public IP. Finally, we are deploying the latest `Ubuntu 22.04` image. All other values are configured using environment variables. ```bash -az vm create --resource-group $MY_RESOURCE_GROUP_NAME --name $MY_VM_NAME --image $MY_VM_IMAGE --admin-username $MY_USERNAME --generate-ssh-keys --public-ip-sku Standard +az vm create \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --name $MY_VM_NAME \ + --image $MY_VM_IMAGE \ + --admin-username $MY_USERNAME \ + --generate-ssh-keys \ + --public-ip-sku Standard ``` Results: @@ -69,6 +75,18 @@ Results: } ``` +### Enable Azure AD login for a Linux Virtual Machine in Azure + +The following example has deploys a Linux VM and then installs the extension to enable Azure AD login for a Linux VM. VM extensions are small applications that provide post-deployment configuration and automation tasks on Azure virtual machines. + +```bash +az vm extension set \ + --publisher Microsoft.Azure.ActiveDirectory \ + --name AADSSHLoginForLinux \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --vm-name $MY_VM_NAME +``` + # Store IP Address of VM in order to SSH run the following command to get the IP Address of the VM and store it as an environment variable @@ -77,6 +95,25 @@ export IP_ADDRESS=$(az vm show --show-details --resource-group $MY_RESOURCE_GROU ``` # SSH Into VM +The following example uses [az role assignment create](https://learn.microsoft.com/cli/azure/role/assignment#az-role-assignment-create) to assign the Virtual Machine Administrator Login role to the VM for your current Azure user. + +```bash +export MY_AZURE_USER=$(az account show --query user.name --output tsv) +export MY_RESOURCE_GROUP_ID=$(az group show --resource-group $MY_RESOURCE_GROUP_NAME --query id -o tsv) + +az role assignment create \ + --role "Virtual Machine Administrator Login" \ + --assignee $MY_AZURE_USER \ + --scope $MY_RESOURCE_GROUP_ID +``` + +## Export the SSH configuration for use with SSH clients that support OpenSSH +Login to Azure Linux VMs with Azure AD supports exporting the OpenSSH certificate and configuration. That means you can use any SSH clients that support OpenSSH-based certificates to sign in through Azure AD. The following example exports the configuration for all IP addresses assigned to the VM: + +```bash +az ssh config --file ~/.ssh/config --name $MY_VM_NAME --resource-group $MY_RESOURCE_GROUP_NAME +``` + You can now SSH into the VM by running the output of the following command in your ssh client of choice ```bash From 26042cb612d9b094657ab17e9e78e04c81f3deb2 Mon Sep 17 00:00:00 2001 From: Mitchell Bifeld <55719566+mbifeld@users.noreply.github.com> Date: Tue, 5 Sep 2023 14:40:34 -0700 Subject: [PATCH 146/226] Update CONTRIBUTING.md (#27) Changed small phrases and updated links. Added Microsoft Open Source contribution guide. Added instructions for creating a PR. Commented out unfinished sections. --- CONTRIBUTING.md | 61 ++++++++++++++++++++++++++----------------------- 1 file changed, 33 insertions(+), 28 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cc3b00c3..16811904 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,7 +3,7 @@ First off, thanks for taking the time to contribute! ❤️ -All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉 +All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles contributions. Please make sure to read the relevant section before making your contribution. It will make it a lot easier for us maintainers and smooth out the experience for all involved. The community looks forward to your contributions. 🎉 > And if you like the project, but just don't have time to contribute, that's fine. There are other easy ways to support the project and show your appreciation, which we would also be very happy about: > - Star the project @@ -15,15 +15,14 @@ All types of contributions are encouraged and valued. See the [Table of Contents ## Table of Contents - [Code of Conduct](#code-of-conduct) +- [Microsoft Open Source Contribution Guide](#microsoft-open-source-contribution-guide) - [I Have a Question](#i-have-a-question) - [I Want To Contribute](#i-want-to-contribute) - [Reporting Bugs](#reporting-bugs) - [Suggesting Enhancements](#suggesting-enhancements) - [Your First Code Contribution](#your-first-code-contribution) -- [Improving The Documentation](#improving-the-documentation) + - [Styleguides](#styleguides) -- [Commit Messages](#commit-messages) -- [Join The Project Team](#join-the-project-team) ## Code of Conduct @@ -33,12 +32,14 @@ This project and everyone participating in it is governed by the By participating, you are expected to uphold this code. Please report unacceptable behavior to mbifeld@microsoft.com. +## Microsoft Open Source Contribution Guide -## I Have a Question +This is a Microsoft Open Source project. Please reference to the [Microsoft Open Source Contribtution Guide](https://docs.opensource.microsoft.com/contributing/) for FAQs and general information on contributing to Microsoft Open Source. -> If you want to ask a question, we assume that you have read the available [Documentation](). +## I Have a Question -Before you ask a question, it is best to search for existing [Issues](https://github.com/Azure/InnovationEngine/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first. + +Before you ask a question, it is best to search for existing [Issues](https://github.com/Azure/InnovationEngine/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. If you then still feel the need to ask a question and need clarification, we recommend the following: @@ -46,7 +47,7 @@ If you then still feel the need to ask a question and need clarification, we rec - Provide as much context as you can about what you're running into. - Provide project and platform versions (golang version, operating system, etc), depending on what seems relevant. -We will then take care of the issue as soon as possible. +We will then address the issue as soon as possible. ## I Want To Contribute @@ -58,11 +59,11 @@ We will then take care of the issue as soon as possible. #### Before Submitting a Bug Report -A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. +A good bug report shouldn't leave others needing to chase you down for more information. Therefore, we ask you to investigate carefully, collect information, and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. - Make sure that you are using the latest version. -- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](). If you are looking for support, you might want to check [this section](#i-have-a-question)). -- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/Azure/InnovationEngineissues?q=label%3Abug). +- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](./README.md). If you are looking for support, you might want to check [I Have A Question](#i-have-a-question)). +- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in [Issues](https://github.com/Azure/InnovationEngine/issues). - Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue. - Collect information about the bug: - Stack trace (Traceback) @@ -74,7 +75,7 @@ A good bug report shouldn't leave others needing to chase you up for more inform #### How Do I Submit a Good Bug Report? -> You must never report security related issues, vulnerabilities or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead sensitive bugs must be sent by email to mbifeld@microsoft.com. +> You must never report security related issues, vulnerabilities, or bugs including sensitive information to the issue tracker, or elsewhere in public. Instead, sensitive bugs must be sent by email to mbifeld@microsoft.com. We use GitHub issues to track bugs and errors. If you run into an issue with the project: @@ -90,9 +91,6 @@ Once it's filed: - A team member will try to reproduce the issue with your provided steps. If there are no reproduction steps or no obvious way to reproduce the issue, the team will ask you for those steps and mark the issue as `needs-repro`. Bugs with the `needs-repro` tag will not be addressed until they are reproduced. - If the team is able to reproduce the issue, it will be marked `needs-fix`, as well as possibly other tags (such as `critical`), and the issue will be left to be [implemented by someone](#your-first-code-contribution). - - - ### Suggesting Enhancements This section guides you through submitting an enhancement suggestion for InnovationEngine, **including completely new features and minor improvements to existing functionality**. Following these guidelines will help maintainers and the community to understand your suggestion and find related suggestions. @@ -101,24 +99,24 @@ This section guides you through submitting an enhancement suggestion for Innovat #### Before Submitting an Enhancement - Make sure that you are using the latest version. -- Read the [documentation]() carefully and find out if the functionality is already covered, maybe by an individual configuration. +- Read the [documentation](./README.md) carefully and find out if the functionality is already covered, maybe by an individual configuration. - Perform a [search](https://github.com/Azure/InnovationEngine/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. -- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library. +- Find out whether your idea fits within the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset. If you're just targeting a minority of users, consider writing an add-on/plugin library. #### How Do I Submit a Good Enhancement Suggestion? -Enhancement suggestions are tracked as [GitHub issues](https://github.com/Azure/InnovationEngine/issues). +Enhancement suggestions are tracked as [GitHub Issues](https://github.com/Azure/InnovationEngine/issues). - Use a **clear and descriptive title** for the issue to identify the suggestion. - Provide a **step-by-step description of the suggested enhancement** in as many details as possible. - **Describe the current behavior** and **explain which behavior you expected to see instead** and why. At this point you can also tell which alternatives do not work for you. -- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. +- You may want to **include screenshots and animated GIFs** which help you demonstrate the steps or point out the part which the suggestion is related to. - **Explain why this enhancement would be useful** to most InnovationEngine users. You may also want to point out the other projects that solved it better and which could serve as inspiration. ### Your First Code Contribution #### Innovation Engine -To get started with developing features for the innovation engine itself, you +To get started with developing features for the Innovation Engine itself, you will need `make` & `go`. Once you have those installed and the project cloned to a local repository, you can attempt to build the project using: @@ -137,17 +135,17 @@ make test-all If implementing a new feature, it is expected to add & update any necessary tests for the changes introduced by the feature. -If you're still looking for more information about how to build & run innovation engine, +If you're still looking for more information about how to build & run Innovation Engine, [README](./README.md) has a more comprehensive guide for how to get started with project development. #### Innovation Engine markdown scenarios -If you are contributing to one of the markdown scenarios (Executable documents) -for innovation engine, you are expected to follow the installation steps before +If you are contributing to one of the markdown scenarios (executable documents) +for Innovation Engine, you are expected to follow the installation steps before updating/adding your document. This is needed because once you've made changes or have added a new scenario, you should test your executable document by -using the innovation engine: +using the Innovation Engine: ```bash ie execute @@ -156,14 +154,21 @@ ie execute This will attempt to parse your document into an executable scenario, make sure that the commands extracted from codeblocks execute successfully, and that their corresponding result blocks (if any) also line up with what the command -returned. Once you get your scenario executes successfully, you should go ahead +returned. Once you get your scenario to execute successfully, you should go ahead and make a PR for it! -### Improving The Documentation -TODO +#### Creating a PR + + +When creating a PR, please include as much context as possible. At minimum, this should include what the PR does and the testing strategies for it. + +If your PR is a work in progress, please label it as a draft and include 'WIP' at the beginning of the PR title. + + ## Styleguides -For working on the innovation engine, `go fmt` is what is used to format the +For working on the Innovation Engine, `go fmt` is what is used to format the code for the project. The commit style for individual commits doesn't necessarily matter as From 3971b23f70d6e81c7cc53f5f385b8e65acdb1239 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Wed, 6 Sep 2023 19:35:12 +0000 Subject: [PATCH 147/226] [update] expected output jsons to be compared despite the casing of the result block. --- internal/engine/common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/engine/common.go b/internal/engine/common.go index 98403791..f28f24fe 100644 --- a/internal/engine/common.go +++ b/internal/engine/common.go @@ -59,7 +59,7 @@ func indentMultiLineCommand(content string, indentation int) string { // Compares the actual output of a command to the expected output of a command. func compareCommandOutputs(actualOutput string, expectedOutput string, expectedSimilarity float64, expectedOutputLanguage string) error { - if expectedOutputLanguage == "json" { + if strings.ToLower(expectedOutputLanguage) == "json" { logging.GlobalLogger.Debugf("Comparing JSON strings:\nExpected: %s\nActual%s", expectedOutput, actualOutput) meetsThreshold, err := utils.CompareJsonStrings(actualOutput, expectedOutput, expectedSimilarity) From e6d2145035d84a5be121d6c3d0043770b6fa0899 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 7 Sep 2023 20:36:02 +0000 Subject: [PATCH 148/226] [add] identity to the created vm & update logging so that errors are reported in the log file. --- cmd/ie/commands/execute.go | 3 +++ internal/engine/execution.go | 2 ++ scenarios/ocd/CreateLinuxVMAndSSH/README.md | 1 + 3 files changed, 6 insertions(+) diff --git a/cmd/ie/commands/execute.go b/cmd/ie/commands/execute.go index 111b75fe..3663b27d 100644 --- a/cmd/ie/commands/execute.go +++ b/cmd/ie/commands/execute.go @@ -5,6 +5,7 @@ import ( "os" "github.com/Azure/InnovationEngine/internal/engine" + "github.com/Azure/InnovationEngine/internal/logging" "github.com/spf13/cobra" ) @@ -52,6 +53,7 @@ var executeCommand = &cobra.Command{ // Parse the markdown file and create a scenario scenario, err := engine.CreateScenarioFromMarkdown(markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}) if err != nil { + logging.GlobalLogger.Errorf("Error creating scenario: %s", err) fmt.Printf("Error creating scenario: %s", err) os.Exit(1) } @@ -59,6 +61,7 @@ var executeCommand = &cobra.Command{ // Execute the scenario err = innovationEngine.ExecuteScenario(scenario) if err != nil { + logging.GlobalLogger.Errorf("Error executing scenario: %s", err) fmt.Printf("Error executing scenario: %s", err) os.Exit(1) } diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 0b3f072f..d0402658 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -245,6 +245,8 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { moveCursorPositionDown(lines) fmt.Printf(" %s\n", errorMessageStyle.Render(commandErr.Error())) + logging.GlobalLogger.Errorf("Error executing command: %s", commandErr.Error()) + ocdStatus.SetError(commandErr) reportOCDStatus(ocdStatus, e.Configuration.Environment) diff --git a/scenarios/ocd/CreateLinuxVMAndSSH/README.md b/scenarios/ocd/CreateLinuxVMAndSSH/README.md index cb664bcf..6cffa7f8 100644 --- a/scenarios/ocd/CreateLinuxVMAndSSH/README.md +++ b/scenarios/ocd/CreateLinuxVMAndSSH/README.md @@ -54,6 +54,7 @@ az vm create \ --name $MY_VM_NAME \ --image $MY_VM_IMAGE \ --admin-username $MY_USERNAME \ + --assign-identity \ --generate-ssh-keys \ --public-ip-sku Standard ``` From f6acf7a1b3f83d3aa590db2e3688752b65b41500 Mon Sep 17 00:00:00 2001 From: Adrian Joian <6505576+naioja@users.noreply.github.com> Date: Fri, 8 Sep 2023 20:01:13 +0200 Subject: [PATCH 149/226] Adding LAMP scenario (#22) * WIP : adding LAMP scenario * Code ready * Code ready * [update] Documentation content ready * Better handling of user rights assignment Depending on the user AAD configuration the user name migt be different from the UPN therefore using the AAD user ID is safer. * Improving VM status detection Before installing the aadsshlogin package we need to wait for cloud-init * Proper JSON formatting * Prefer default JSON output for each command --- scenarios/ocd/CreateLinuxVMLAMP/README.md | 769 ++++++++++++++++++++++ 1 file changed, 769 insertions(+) create mode 100644 scenarios/ocd/CreateLinuxVMLAMP/README.md diff --git a/scenarios/ocd/CreateLinuxVMLAMP/README.md b/scenarios/ocd/CreateLinuxVMLAMP/README.md new file mode 100644 index 00000000..3d660e31 --- /dev/null +++ b/scenarios/ocd/CreateLinuxVMLAMP/README.md @@ -0,0 +1,769 @@ +# Install a LAMP stack on Azure + +This article walks you through how to deploy an NGINX web server, Azure MySQL Flexible Server, and PHP (the LEMP stack) on an Ubuntu Linux VM in Azure. To see the LEMP server in action, you can optionally install and configure a WordPress site. In this tutorial you learn how to: + +> [!div class="checklist"] +> * Create a Linux Ubuntu VM +> * Open ports 80 and 443 for web traffic +> * Install and Secure NGINX, Azure Flexible MySQL Server, and PHP +> * Verify installation and configuration +> * Install WordPress + +# Create RG + +Create a resource group with the [az group create](https://learn.microsoft.com/cli/azure/group#az-group-create) command. An Azure resource group is a logical container into which Azure resources are deployed and managed. +The following example creates a resource group named `$MY_RESOURCE_GROUP_NAME` in the `eastus` location. + +## Variable declaration + +First we will define a few variables that will help with the configuration of the LAMP workload. + +```bash +export NETWORK_PREFIX="$(($RANDOM % 254 + 1))" +export RANDOM_ID="$(openssl rand -hex 3)" +export MY_RESOURCE_GROUP_NAME="myResourceGroup$RANDOM_ID" +export MY_LOCATION="eastus" +export MY_VM_NAME="myVMName$RANDOM_ID" +export MY_VM_USERNAME="azureadmin" +export MY_VM_SIZE='Standard_DS2_v2' +export MY_VM_IMAGE='Canonical:0001-com-ubuntu-minimal-jammy:minimal-22_04-lts-gen2:latest' +export MY_PUBLIC_IP_NAME="myPublicIP$RANDOM_ID" +export MY_DNS_LABEL="mydnslabel$RANDOM_ID" +export MY_NSG_NAME="myNSGName$RANDOM_ID" +export MY_NSG_SSH_RULE="Allow-Access$RANDOM_ID" +export MY_VM_NIC_NAME="myVMNicName$RANDOM_ID" +export MY_VNET_NAME="myVNet$RANDOM_ID" +export MY_VNET_PREFIX="10.$NETWORK_PREFIX.0.0/22" +export MY_SN_NAME="mySN$RANDOM_ID" +export MY_SN_PREFIX="10.$NETWORK_PREFIX.0.0/24" +export MY_MYSQL_DB_NAME="mydb$RANDOM_ID" +export MY_MYSQL_ADMIN_USERNAME="dbadmin$RANDOM_ID" +export MY_MYSQL_ADMIN_PW="$(openssl rand -base64 32)" +export MY_MYSQL_SN_NAME="myMySQLSN$RANDOM_ID" +export MY_WP_ADMIN_PW="$(openssl rand -base64 32)" +export MY_WP_ADMIN_USER="wpcliadmin" +export MY_AZURE_USER=$(az account show --query user.name --output tsv) +export MY_AZURE_USER_ID=$(az ad user list --filter "mail eq '$MY_AZURE_USER'" --query "[0].id" -o tsv) +export FQDN="${MY_DNS_LABEL}.${MY_LOCATION}.cloudapp.azure.com" +``` + +```bash +az group create \ + --name $MY_RESOURCE_GROUP_NAME \ + --location $MY_LOCATION -o JSON +``` +Results: + + +```JSON +{ + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc", + "location": "eastus", + "managedBy": null, + "name": "myResourceGroup6ad2bc", + "properties": { + "provisioningState": "Succeeded" + }, + "tags": null, + "type": "Microsoft.Resources/resourceGroups" +} +``` + +# Setup LAMP networking + +## Create an Azure Virtual Network + +A virtual network is the fundamental building block for private networks in Azure. Azure Virtual Network enables Azure resources like VMs to securely communicate with each other and the internet. +Use [az network vnet create](https://learn.microsoft.com/cli/azure/network/vnet#az-network-vnet-create) to create a virtual network named `$MY_VNET_NAME` with a subnet named `$MY_SN_NAME` in the `$MY_RESOURCE_GROUP_NAME` resource group. + +```bash +az network vnet create \ + --name $MY_VNET_NAME \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --location $MY_LOCATION \ + --address-prefix $MY_VNET_PREFIX \ + --subnet-name $MY_SN_NAME \ + --subnet-prefixes $MY_SN_PREFIX -o JSON +``` +Results: + + +```JSON +{ + "newVNet": { + "addressSpace": { + "addressPrefixes": [ + "10.19.0.0/22" + ] + }, + "enableDdosProtection": false, + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.Network/virtualNetworks/myVNet6ad2bc", + "location": "eastus", + "name": "myVNet6ad2bc", + "provisioningState": "Succeeded", + "resourceGroup": "myResourceGroup6ad2bc", + "subnets": [ + { + "addressPrefix": "10.19.0.0/24", + "delegations": [], + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.Network/virtualNetworks/myVNet6ad2bc/subnets/mySN6ad2bc", + "name": "mySN6ad2bc", + "privateEndpointNetworkPolicies": "Disabled", + "privateLinkServiceNetworkPolicies": "Enabled", + "provisioningState": "Succeeded", + "resourceGroup": "myResourceGroup6ad2bc", + "type": "Microsoft.Network/virtualNetworks/subnets" + } + ], + "type": "Microsoft.Network/virtualNetworks", + "virtualNetworkPeerings": [] + } +} +``` + +## Create an Azure Public IP + +Use [az network public-ip create](https://learn.microsoft.com/cli/azure/network/public-ip#az-network-public-ip-create) to create a standard zone-redundant public IPv4 address named `MY_PUBLIC_IP_NAME` in `$MY_RESOURCE_GROUP_NAME`. + +>[!NOTE] +>The below options for zones are only valid selections in regions with [Availability Zones](https://learn.microsoft.com/azure/reliability/availability-zones-service-support). + +```bash +az network public-ip create \ + --name $MY_PUBLIC_IP_NAME \ + --location $MY_LOCATION \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --dns-name $MY_DNS_LABEL \ + --sku Standard \ + --allocation-method static \ + --version IPv4 \ + --zone 1 2 3 -o JSON +``` +Results: + + +```JSON +{ + "publicIp": { + "ddosSettings": { + "protectionMode": "VirtualNetworkInherited" + }, + "dnsSettings": { + "domainNameLabel": "mydnslabel6ad2bc", + "fqdn": "mydnslabel6ad2bc.eastus.cloudapp.azure.com" + }, + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.Network/publicIPAddresses/myPublicIP6ad2bc", + "idleTimeoutInMinutes": 4, + "ipTags": [], + "location": "eastus", + "name": "myPublicIP6ad2bc", + "provisioningState": "Succeeded", + "publicIPAddressVersion": "IPv4", + "publicIPAllocationMethod": "Static", + "resourceGroup": "myResourceGroup6ad2bc", + "sku": { + "name": "Standard", + "tier": "Regional" + }, + "type": "Microsoft.Network/publicIPAddresses", + "zones": [ + "1", + "2", + "3" + ] + } +} +``` + +## Create an Azure Network Security Group + +Security rules in network security groups enable you to filter the type of network traffic that can flow in and out of virtual network subnets and network interfaces. To learn more about network security groups, see [Network security group overview](https://learn.microsoft.com/azure/virtual-network/network-security-groups-overview). + +```bash +az network nsg create \ + --name $MY_NSG_NAME \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --location $MY_LOCATION -o JSON +``` +Results: + + +```JSON +{ + "NewNSG": { + "defaultSecurityRules": + { + "access": "Allow", + "description": "Allow inbound traffic from all VMs in VNET", + "destinationAddressPrefix": "VirtualNetwork", + "destinationAddressPrefixes": [], + "destinationPortRange": "*", + "destinationPortRanges": [], + "direction": "Inbound", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup104/providers/Microsoft.Network/networkSecurityGroups/protect-vms/defaultSecurityRules/AllowVnetInBound", + "name": "AllowVnetInBound", + "priority": 65000, + "protocol": "*", + "provisioningState": "Succeeded", + "resourceGroup": "myResourceGroup104", + "sourceAddressPrefix": "VirtualNetwork", + "sourceAddressPrefixes": [], + "sourcePortRange": "*", + "sourcePortRanges": [], + "type": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules" + }, + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup104/providers/Microsoft.Network/networkSecurityGroups/protect-vms", + "location": "eastus", + "name": "protect-vms", + "provisioningState": "Succeeded", + "resourceGroup": "myResourceGroup104", + "securityRules": [], + "type": "Microsoft.Network/networkSecurityGroups" + } +} +``` + +## Create Azure Network Security Group rules + +You'll create a rule to allow connections to the virtual machine on port 22 for SSH and ports 80, 443 for HTTP and HTTPS. An extra rule is created to allow all ports for outbound connections. Use [az network nsg rule create](https://learn.microsoft.com/cli/azure/network/nsg/rule#az-network-nsg-rule-create) to create a network security group rule. + +```bash +az network nsg rule create \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --nsg-name $MY_NSG_NAME \ + --name $MY_NSG_SSH_RULE \ + --access Allow \ + --protocol Tcp \ + --direction Inbound \ + --priority 100 \ + --source-address-prefix '*' \ + --source-port-range '*' \ + --destination-address-prefix '*' \ + --destination-port-range 22 80 443 -o JSON +``` +Results: + + +```JSON +{ + "access": "Allow", + "destinationAddressPrefix": "*", + "destinationAddressPrefixes": [], + "destinationPortRanges": [ + "22", + "80", + "443" + ], + "direction": "Inbound", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.Network/networkSecurityGroups/myNSGName6ad2bc/securityRules/Allow-Access6ad2bc", + "name": "Allow-Access6ad2bc", + "priority": 100, + "protocol": "Tcp", + "provisioningState": "Succeeded", + "resourceGroup": "myResourceGroup6ad2bc", + "sourceAddressPrefix": "*", + "sourceAddressPrefixes": [], + "sourcePortRange": "*", + "sourcePortRanges": [], + "type": "Microsoft.Network/networkSecurityGroups/securityRules" +} +``` + +## Create an Azure Network Interface + +You'll use [az network nic create](https://learn.microsoft.com/cli/azure/network/nic#az-network-nic-create) to create the network interface for the virtual machine. The public IP addresses and the NSG created previously are associated with the NIC. The network interface is attached to the virtual network you created previously. + +```bash +az network nic create \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --name $MY_VM_NIC_NAME \ + --location $MY_LOCATION \ + --ip-forwarding false \ + --subnet $MY_SN_NAME \ + --vnet-name $MY_VNET_NAME \ + --network-security-group $MY_NSG_NAME \ + --public-ip-address $MY_PUBLIC_IP_NAME -o JSON +``` +Results: + + +```JSON +{ + "NewNIC": { + "auxiliaryMode": "None", + "auxiliarySku": "None", + "disableTcpStateTracking": false, + "dnsSettings": { + "appliedDnsServers": [], + "dnsServers": [] + }, + "enableAcceleratedNetworking": false, + "enableIPForwarding": false, + "hostedWorkloads": [], + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.Network/networkInterfaces/myVMNicName6ad2bc", + "ipConfigurations": [ + { + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.Network/networkInterfaces/myVMNicName6ad2bc/ipConfigurations/ipconfig1", + "name": "ipconfig1", + "primary": true, + "privateIPAddress": "10.19.0.4", + "privateIPAddressVersion": "IPv4", + "privateIPAllocationMethod": "Dynamic", + "provisioningState": "Succeeded", + "resourceGroup": "myResourceGroup6ad2bc", + "subnet": { + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.Network/virtualNetworks/myVNet6ad2bc/subnets/mySN6ad2bc", + "resourceGroup": "myResourceGroup6ad2bc" + }, + "type": "Microsoft.Network/networkInterfaces/ipConfigurations" + } + ], + "location": "eastus", + "name": "myVMNicName6ad2bc", + "networkSecurityGroup": { + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.Network/networkSecurityGroups/myNSGName6ad2bc", + "resourceGroup": "myResourceGroup6ad2bc" + }, + "nicType": "Standard", + "provisioningState": "Succeeded", + "resourceGroup": "myResourceGroup6ad2bc", + "tapConfigurations": [], + "type": "Microsoft.Network/networkInterfaces", + "vnetEncryptionSupported": false + } +} +``` + +# Cloud-init overview + +Cloud-init is a widely used approach to customize a Linux VM as it boots for the first time. You can use cloud-init to install packages and write files, or to configure users and security. As cloud-init runs during the initial boot process, there are no additional steps or required agents to apply your configuration. + +Cloud-init also works across distributions. For example, you don't use apt-get install or yum install to install a package. Instead you can define a list of packages to install. Cloud-init automatically uses the native package management tool for the distro you select. + +We are working with our partners to get cloud-init included and working in the images that they provide to Azure. For detailed information cloud-init support for each distribution, see [Cloud-init support for VMs in Azure](https://learn.microsoft.com/azure/virtual-machines/linux/using-cloud-init). + + +## Create cloud-init config file + +To see cloud-init in action, create a VM that installs a LAMP stack and runs a simple Wordpress app secured with an SSL certificate. The following cloud-init configuration installs the required packages, creates the Wordpress website, then initialize and starts the website. + +```bash +cat << EOF > cloud-init.txt +#cloud-config + +# Install, update, and upgrade packages +package_upgrade: true +package_update: true +package_reboot_if_require: true + +# Install packages +packages: + - vim + - certbot + - python3-certbot-nginx + - bash-completion + - nginx + - mysql-client + - php + - php-cli + - php-bcmath + - php-curl + - php-imagick + - php-intl + - php-json + - php-mbstring + - php-mysql + - php-gd + - php-xml + - php-xmlrpc + - php-zip + - php-fpm + +write_files: + - owner: www-data:www-data + path: /etc/nginx/sites-available/default.conf + content: | + server { + listen 80 default_server; + listen [::]:80 default_server; + root /var/www/html; + server_name $FQDN; + } + +write_files: + - owner: www-data:www-data + path: /etc/nginx/sites-available/$FQDN.conf + content: | + upstream php { + server unix:/run/php/php8.1-fpm.sock; + } + server { + listen 443 ssl http2; + listen [::]:443 ssl http2; + + server_name $FQDN; + + ssl_certificate /etc/letsencrypt/live/$FQDN/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/$FQDN/privkey.pem; + + root /var/www/$FQDN; + index index.php; + + location / { + try_files \$uri \$uri/ /index.php?\$args; + } + location ~ \.php$ { + include fastcgi_params; + fastcgi_intercept_errors on; + fastcgi_pass php; + fastcgi_param SCRIPT_FILENAME \$document_root\$fastcgi_script_name; + } + location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ { + expires max; + log_not_found off; + } + location = /favicon.ico { + log_not_found off; + access_log off; + } + + location = /robots.txt { + allow all; + log_not_found off; + access_log off; + } + } + server { + listen 80; + listen [::]:80; + server_name $FQDN; + return 301 https://$FQDN\$request_uri; + } + +runcmd: + - sed -i "s/;cgi.fix_pathinfo.*/cgi.fix_pathinfo = 1/" /etc/php/8.1/fpm/php.ini + - sed -i 's/^max_execution_time \= .*/max_execution_time \= 300/g' /etc/php/8.1/fpm/php.ini + - sed -i 's/^upload_max_filesize \= .*/upload_max_filesize \= 64M/g' /etc/php/8.1/fpm/php.ini + - sed -i 's/^post_max_size \= .*/post_max_size \= 64M/g' /etc/php/8.1/fpm/php.ini + - systemctl restart php8.1-fpm + - systemctl restart nginx + - certbot --nginx certonly --non-interactive --agree-tos -d $FQDN -m dummy@dummy.com --redirect + - ln -s /etc/nginx/sites-available/$FQDN.conf /etc/nginx/sites-enabled/ + - rm /etc/nginx/sites-enabled/default + - systemctl restart nginx + - curl --url https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar --output /tmp/wp-cli.phar + - mv /tmp/wp-cli.phar /usr/local/bin/wp + - chmod +x /usr/local/bin/wp + - wp cli update + - mkdir -m 0755 -p /var/www/$FQDN + - chown -R azureadmin:www-data /var/www/$FQDN + - sudo -u azureadmin -i -- wp core download --path=/var/www/$FQDN + - sudo -u azureadmin -i -- wp config create --dbhost=$MY_MYSQL_DB_NAME.mysql.database.azure.com --dbname=wp001 --dbuser=$MY_MYSQL_ADMIN_USERNAME --dbpass="$MY_MYSQL_ADMIN_PW" --path=/var/www/$FQDN + - sudo -u azureadmin -i -- wp core install --url=$FQDN --title="Azure hosted blog" --admin_user=$MY_WP_ADMIN_USER --admin_password="$MY_WP_ADMIN_PW" --admin_email=$MY_AZURE_USER --path=/var/www/$FQDN + - sudo -u azureadmin -i -- wp plugin update --all --path=/var/www/$FQDN + - chmod 600 /var/www/$FQDN/wp-config.php + - mkdir -p -m 0775 /var/www/$FQDN/wp-content/uploads + - chgrp www-data /var/www/$FQDN/wp-content/uploads +EOF +``` + +## Create an Azure Private DNS Zone for Azure MySQL Flexible Server + +Azure Private DNS Zone integration allows you to resolve the private DNS within the current VNET or any in-region peered VNET where the private DNS Zone is linked. You'll use [az network private-dns zone create](https://learn.microsoft.com/cli/azure/network/private-dns/zone#az-network-private-dns-zone-create) to create the private dns zone. + +```bash +az network private-dns zone create \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --name $MY_DNS_LABEL.private.mysql.database.azure.com -o JSON +``` +Results: + + +```JSON +{ + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myresourcegroup6ad2bc/providers/Microsoft.Network/privateDnsZones/mydnslabel6ad2bc.private.mysql.database.azure.com", + "location": "global", + "maxNumberOfRecordSets": 25000, + "maxNumberOfVirtualNetworkLinks": 1000, + "maxNumberOfVirtualNetworkLinksWithRegistration": 100, + "name": "mydnslabel6ad2bc.private.mysql.database.azure.com", + "numberOfRecordSets": 1, + "numberOfVirtualNetworkLinks": 0, + "numberOfVirtualNetworkLinksWithRegistration": 0, + "provisioningState": "Succeeded", + "resourceGroup": "myresourcegroup6ad2bc", + "tags": null, + "type": "Microsoft.Network/privateDnsZones" +} +``` + +# Create an Azure Database for MySQL - Flexible Server + +Azure Database for MySQL - Flexible Server is a managed service that you can use to run, manage, and scale highly available MySQL servers in the cloud. Create a flexible server with the [az mysql flexible-server create](https://learn.microsoft.com/cli/azure/mysql/flexible-server#az-mysql-flexible-server-create) command. A server can contain multiple databases. The following command creates a server using service defaults and variable values from your Azure CLI's local environment: + +```bash +echo "Your MySQL user $MY_MYSQL_ADMIN_USERNAME password is: $MY_WP_ADMIN_PW" +``` + +```bash +az mysql flexible-server create \ + --admin-password $MY_MYSQL_ADMIN_PW \ + --admin-user $MY_MYSQL_ADMIN_USERNAME \ + --auto-scale-iops Disabled \ + --high-availability Disabled \ + --iops 500 \ + --location $MY_LOCATION \ + --name $MY_MYSQL_DB_NAME \ + --database-name wp001 \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --sku-name Standard_B2s \ + --storage-auto-grow Disabled \ + --storage-size 20 \ + --subnet $MY_MYSQL_SN_NAME \ + --private-dns-zone $MY_DNS_LABEL.private.mysql.database.azure.com \ + --tier Burstable \ + --version 8.0.21 \ + --vnet $MY_VNET_NAME \ + --yes -o JSON +``` +Results: + + +```JSON +{ + "databaseName": "wp001", + "host": "mydb6ad2bc.mysql.database.azure.com", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.DBforMySQL/flexibleServers/mydb6ad2bc", + "location": "East US", + "resourceGroup": "myResourceGroup6ad2bc", + "skuname": "Standard_B2s", + "subnetId": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.Network/virtualNetworks/myVNet6ad2bc/subnets/myMySQLSN6ad2bc", + "username": "dbadmin6ad2bc", + "version": "8.0.21" +} +``` + +The server created has the below attributes: +- The server name, admin username, admin password, resource group name, location are already specified in local context environment of the cloud shell, and will be created in the same location as your the resource group and the other Azure components. +- Service defaults for remaining server configurations: compute tier (Burstable), compute size/SKU (Standard_B2s), backup retention period (7 days), and MySQL version (8.0.21) +- The default connectivity method is Private access (VNet Integration) with a linked virtual network and a auto-generated subnet. + +> [!NOTE] +> The connectivity method cannot be changed after creating the server. For example, if you selected `Private access (VNet Integration)` during create then you cannot change to `Public access (allowed IP addresses)` after create. We highly recommend creating a server with Private access to securely access your server using VNet Integration. Learn more about Private access in the [concepts article](https://learn.microsoft.com/azure/mysql/flexible-server/concepts-networking-vnet). + +If you'd like to change any defaults, please refer to the Azure CLI [reference documentation](https://learn.microsoft.com/cli/azure//mysql/flexible-server) for the complete list of configurable CLI parameters. + +## Check the Azure Database for MySQL - Flexible Server status + +It takes a few minutes to create the Azure Database for MySQL - Flexible Server and supporting resources. +```bash +runtime="10 minute"; endtime=$(date -ud "$runtime" +%s); while [[ $(date -u +%s) -le $endtime ]]; do STATUS=$(az mysql flexible-server show -g $MY_RESOURCE_GROUP_NAME -n $MY_MYSQL_DB_NAME --query state -o tsv); echo $STATUS; if [ "$STATUS" = 'Ready' ]; then break; else sleep 10; fi; done +``` + +## Configure server parameters in Azure Database for MySQL - Flexible Server + +You can manage Azure Database for MySQL - Flexible Server configuration using server parameters. The server parameters are configured with the default and recommended value when you create the server. + +Show server parameter details +To show details about a particular parameter for a server, run the [az mysql flexible-server parameter show](https://learn.microsoft.com/cli/azure/mysql/flexible-server/parameter) command. + +### Disable Azure Database for MySQL - Flexible Server SSL connection parameter for Wordpress integration + +Modify a server parameter value +You can also modify the value of a certain server parameter, which updates the underlying configuration value for the MySQL server engine. To update the server parameter, use the [az mysql flexible-server parameter set](https://learn.microsoft.com/cli/azure/mysql/flexible-server/parameter#az-mysql-flexible-server-parameter-set) command. + +```bash +az mysql flexible-server parameter set \ + -g $MY_RESOURCE_GROUP_NAME \ + -s $MY_MYSQL_DB_NAME \ + -n require_secure_transport -v "OFF" -o JSON +``` +Results: + + +```JSON +{ + "allowedValues": "ON,OFF", + "currentValue": "OFF", + "dataType": "Enumeration", + "defaultValue": "ON", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.DBforMySQL/flexibleServers/mydb6ad2bc/configurations/require_secure_transport", + "isConfigPendingRestart": "False", + "isDynamicConfig": "True", + "isReadOnly": "False", + "name": "require_secure_transport", + "resourceGroup": "myResourceGroup6ad2bc", + "source": "user-override", + "systemData": null, + "type": "Microsoft.DBforMySQL/flexibleServers/configurations", + "value": "OFF" +} +``` + +# Create an Azure Linux Virtual Machine + +The following example creates a VM named `$MY_VM_NAME` and creates SSH keys if they do not already exist in a default key location. The command also sets `$MY_VM_USERNAME` as an administrator user name. +To improve the security of Linux virtual machines in Azure, you can integrate with Azure Active Directory authentication. You can now use Azure AD as a core authentication platform and a certificate authority to SSH into a Linux VM by using Azure AD and OpenSSH certificate-based authentication. This functionality allows organizations to manage access to VMs with Azure role-based access control and Conditional Access policies. + +Create a VM with the [az vm create](https://learn.microsoft.com/cli/azure/vm#az-vm-create) command. + +```bash +az vm create \ + --name $MY_VM_NAME \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --admin-username $MY_VM_USERNAME \ + --authentication-type ssh \ + --image $MY_VM_IMAGE \ + --location $MY_LOCATION \ + --nic-delete-option Delete \ + --os-disk-caching ReadOnly \ + --os-disk-delete-option Delete \ + --os-disk-size-gb 30 \ + --size $MY_VM_SIZE \ + --generate-ssh-keys \ + --storage-sku Premium_LRS \ + --nics $MY_VM_NIC_NAME \ + --custom-data cloud-init.txt -o JSON +``` +Results: + + +```JSON +{ + "fqdns": "mydnslabel6ad2bc.eastus.cloudapp.azure.com", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.Compute/virtualMachines/myVMName6ad2bc", + "identity": { + "principalId": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy", + "tenantId": "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz", + "type": "SystemAssigned", + "userAssignedIdentities": null + }, + "location": "eastus", + "macAddress": "60-45-BD-D8-1D-84", + "powerState": "VM running", + "privateIpAddress": "10.19.0.4", + "resourceGroup": "myResourceGroup6ad2bc", + "zones": "" +} +``` + +## Check the Azure Linux Virtual Machine status + +It takes a few minutes to create the VM and supporting resources. The provisioningState value of Succeeded appears when the extension is successfully installed on the VM. The VM must have a running [VM agent](https://learn.microsoft.com/azure/virtual-machines/extensions/agent-linux) to install the extension. + +```bash +runtime="10 minute"; endtime=$(date -ud "$runtime" +%s); while [[ $(date -u +%s) -le $endtime ]]; do STATUS=$(ssh -o StrictHostKeyChecking=no $FQDN "cloud-init status"); echo $STATUS; if [ "$STATUS" = 'status: done' ]; then break; else sleep 10; fi; done +``` + +## Enable Azure AD login for a Linux Virtual Machine in Azure + +The following example deploys a VM and then installs the extension to enable Azure AD login for a Linux VM. VM extensions are small applications that provide post-deployment configuration and automation tasks on Azure virtual machines. + +```bash +az vm extension set \ + --publisher Microsoft.Azure.ActiveDirectory \ + --name AADSSHLoginForLinux \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --vm-name $MY_VM_NAME -o JSON +``` +Results: + + +```JSON +{ + "autoUpgradeMinorVersion": true, + "enableAutomaticUpgrade": null, + "forceUpdateTag": null, + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroupfa636b/providers/Microsoft.Compute/virtualMachines/myVMNamefa636b/extensions/AADSSHLoginForLinux", + "instanceView": null, + "location": "eastus", + "name": "AADSSHLoginForLinux", + "protectedSettings": null, + "protectedSettingsFromKeyVault": null, + "provisioningState": "Succeeded", + "publisher": "Microsoft.Azure.ActiveDirectory", + "resourceGroup": "myResourceGroupfa636b", + "settings": null, + "suppressFailures": null, + "tags": null, + "type": "Microsoft.Compute/virtualMachines/extensions", + "typeHandlerVersion": "1.0", + "typePropertiesType": "AADSSHLoginForLinux" +} +``` + +## Assign Azure AD RBAC for Azure AD login for Linux Virtual Machine + +The below command uses [az role assignment create](https://learn.microsoft.com/cli/azure/role/assignment#az-role-assignment-create) to assign the `Virtual Machine Administrator Login` role to the VM for your current Azure user. + +```bash +export MY_RESOURCE_GROUP_ID=$(az group show --resource-group $MY_RESOURCE_GROUP_NAME --query id -o tsv) + +az role assignment create \ + --role "Virtual Machine Administrator Login" \ + --assignee $MY_AZURE_USER_ID \ + --scope $MY_RESOURCE_GROUP_ID -o JSON +``` +Results: + + +```JSON +{ + "condition": null, + "conditionVersion": null, + "createdBy": null, + "createdOn": "2023-09-04T09:29:16.895907+00:00", + "delegatedManagedIdentityResourceId": null, + "description": null, + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroupfa636b/providers/Microsoft.Authorization/roleAssignments/yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy", + "name": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy", + "principalId": "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz", + "principalType": "User", + "resourceGroup": "myResourceGroupfa636b", + "roleDefinitionId": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/providers/Microsoft.Authorization/roleDefinitions/zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz", + "scope": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroupfa636b", + "type": "Microsoft.Authorization/roleAssignments", + "updatedBy": "wwwwwwww-wwww-wwww-wwww-wwwwwwwwwwww", + "updatedOn": "2023-09-04T09:29:17.237445+00:00" +} +``` + +## Export the SSH configuration for use with SSH clients that support OpenSSH +Login to Azure Linux VMs with Azure AD supports exporting the OpenSSH certificate and configuration. That means you can use any SSH clients that support OpenSSH-based certificates to sign in through Azure AD. The following example exports the configuration for all IP addresses assigned to the VM: + +```bash +az ssh config --file ~/.ssh/config --name $MY_VM_NAME --resource-group $MY_RESOURCE_GROUP_NAME +``` +Results: + + +```ASCII +Generated SSH certificate /home/admn/.ssh/az_ssh_config/myResourceGroupfa636b-myVMNamefa636b/id_rsa.pub-aadcert.pub is valid until 2023-09-04 12:37:25 PM in local time. +/home/admn/.ssh/az_ssh_config/myResourceGroupfa636b-myVMNamefa636b contains sensitive information (id_rsa, id_rsa.pub, id_rsa.pub-aadcert.pub). Please delete it once you no longer need this config file. +``` + +# Browse your WordPress website + +[WordPress](https://www.wordpress.org) is an open source content management system (CMS) used by over 40% of the web to create websites, blogs, and other applications. WordPress can be run on a few different Azure services: [AKS](https://learn.microsoft.com/azure/mysql/flexible-server/tutorial-deploy-wordpress-on-aks), Virtual Machines, and App Service. For a full list of WordPress options on Azure, see [WordPress on Azure Marketplace](https://azuremarketplace.microsoft.com/marketplace/apps?page=1&search=wordpress). + +This WordPress setup is only for proof of concept. To install the latest WordPress in production with recommended security settings, see the [WordPress documentation](https://codex.wordpress.org/Main_Page). + +Validate that the application is running by visiting the application url: + +```bash +curl --max-time 120 "https://$FQDN" +``` +Results: + + +```HTML + + + + + + +Azure hosted blog + + +``` From 95ec51a379b3346fd496900dab875c7f747488cc Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 11 Sep 2023 18:13:56 +0000 Subject: [PATCH 150/226] [add] --variables flag to allow for environment variables to be overidden via the CLI. --- README.md | 4 ++-- cmd/ie/commands/execute.go | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b2ed150c..aa764f47 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,11 @@ cd InnovationEngine; make build-ie; ``` -Now you can run the interactive Innovation Engine tutorial with the following +Now you can run the Innovation Engine tutorial with the following command: ```bash -./bin/ie interactive tutorial.md +./bin/ie execute scenarios/demos/tutorial.md ``` The general format to run an executable document is: diff --git a/cmd/ie/commands/execute.go b/cmd/ie/commands/execute.go index 3663b27d..6321a1ab 100644 --- a/cmd/ie/commands/execute.go +++ b/cmd/ie/commands/execute.go @@ -3,6 +3,7 @@ package commands import ( "fmt" "os" + "strings" "github.com/Azure/InnovationEngine/internal/engine" "github.com/Azure/InnovationEngine/internal/logging" @@ -21,6 +22,7 @@ func init() { executeCommand.PersistentFlags().String("correlation-id", "", "Adds a correlation ID to the user agent used by a scenarios azure-cli commands.") executeCommand.PersistentFlags().String("subscription", "", "Sets the subscription ID used by a scenarios azure-cli commands. Will rely on the default subscription if not set.") executeCommand.PersistentFlags().String("working-directory", ".", "Sets the working directory for innovation engine to operate out of. Restores the current working directory when finished.") + executeCommand.PersistentFlags().StringArray("variables", []string{}, "Sets the environment variable for the scenario. Format: --variables =") } var executeCommand = &cobra.Command{ @@ -30,6 +32,7 @@ var executeCommand = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { markdownFile := args[0] if markdownFile == "" { + logging.GlobalLogger.Errorf("Error: No markdown file specified.") cmd.Help() os.Exit(1) } @@ -39,8 +42,23 @@ var executeCommand = &cobra.Command{ subscription, _ := cmd.Flags().GetString("subscription") correlationId, _ := cmd.Flags().GetString("correlation-id") environment, _ := cmd.Flags().GetString("environment") + environmentVariables, _ := cmd.Flags().GetStringArray("variables") workingDirectory, _ := cmd.Flags().GetString("working-directory") + userDefinedEnvironmentVariables := make(map[string]string) + + for _, environmentVariable := range environmentVariables { + keyValuePair := strings.SplitN(environmentVariable, "=", 2) + if len(keyValuePair) != 2 { + logging.GlobalLogger.Errorf("Error: Invalid environment variable format: %s", environmentVariable) + fmt.Printf("Error: Invalid environment variable format: %s", environmentVariable) + cmd.Help() + os.Exit(1) + } + + userDefinedEnvironmentVariables[keyValuePair[0]] = keyValuePair[1] + } + innovationEngine := engine.NewEngine(engine.EngineConfiguration{ Verbose: verbose, DoNotDelete: doNotDelete, From e23e4489a4ae334f2e4493f0b95db385b45ab4fb Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 11 Sep 2023 19:27:59 +0000 Subject: [PATCH 151/226] [update] parameter name to be --var & update the parser to inject the environment variable override values into the markdown export statements. --- cmd/ie/commands/execute.go | 7 +-- cmd/ie/commands/test.go | 2 +- internal/engine/scenario.go | 60 +++++++++++++++++++-- scenarios/ocd/CreateLinuxVMAndSSH/README.md | 6 +-- 4 files changed, 63 insertions(+), 12 deletions(-) diff --git a/cmd/ie/commands/execute.go b/cmd/ie/commands/execute.go index 6321a1ab..1dac8567 100644 --- a/cmd/ie/commands/execute.go +++ b/cmd/ie/commands/execute.go @@ -22,7 +22,7 @@ func init() { executeCommand.PersistentFlags().String("correlation-id", "", "Adds a correlation ID to the user agent used by a scenarios azure-cli commands.") executeCommand.PersistentFlags().String("subscription", "", "Sets the subscription ID used by a scenarios azure-cli commands. Will rely on the default subscription if not set.") executeCommand.PersistentFlags().String("working-directory", ".", "Sets the working directory for innovation engine to operate out of. Restores the current working directory when finished.") - executeCommand.PersistentFlags().StringArray("variables", []string{}, "Sets the environment variable for the scenario. Format: --variables =") + executeCommand.PersistentFlags().StringArray("var", []string{}, "Sets an environment variable for the scenario. Format: --var =") } var executeCommand = &cobra.Command{ @@ -42,9 +42,10 @@ var executeCommand = &cobra.Command{ subscription, _ := cmd.Flags().GetString("subscription") correlationId, _ := cmd.Flags().GetString("correlation-id") environment, _ := cmd.Flags().GetString("environment") - environmentVariables, _ := cmd.Flags().GetStringArray("variables") + environmentVariables, _ := cmd.Flags().GetStringArray("var") workingDirectory, _ := cmd.Flags().GetString("working-directory") + fmt.Println("environmentVariables: ", environmentVariables) userDefinedEnvironmentVariables := make(map[string]string) for _, environmentVariable := range environmentVariables { @@ -69,7 +70,7 @@ var executeCommand = &cobra.Command{ }) // Parse the markdown file and create a scenario - scenario, err := engine.CreateScenarioFromMarkdown(markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}) + scenario, err := engine.CreateScenarioFromMarkdown(markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}, userDefinedEnvironmentVariables) if err != nil { logging.GlobalLogger.Errorf("Error creating scenario: %s", err) fmt.Printf("Error creating scenario: %s", err) diff --git a/cmd/ie/commands/test.go b/cmd/ie/commands/test.go index 532bb700..e3260c38 100644 --- a/cmd/ie/commands/test.go +++ b/cmd/ie/commands/test.go @@ -34,7 +34,7 @@ var testCommand = &cobra.Command{ CorrelationId: "", }) - scenario, err := engine.CreateScenarioFromMarkdown(markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}) + scenario, err := engine.CreateScenarioFromMarkdown(markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}, nil) if err != nil { panic(err) } diff --git a/internal/engine/scenario.go b/internal/engine/scenario.go index 24dce6f2..2de02b55 100644 --- a/internal/engine/scenario.go +++ b/internal/engine/scenario.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "regexp" "strings" "github.com/Azure/InnovationEngine/internal/logging" @@ -51,11 +52,7 @@ func groupCodeBlocksIntoSteps(blocks []parsers.CodeBlock) []Step { // Creates a scenario object from a given markdown file. languagesToExecute is // used to filter out code blocks that should not be parsed out of the markdown // file. -func CreateScenarioFromMarkdown(path string, languagesToExecute []string) (*Scenario, error) { - if path == "" { - return nil, nil - } - +func CreateScenarioFromMarkdown(path string, languagesToExecute []string, environmentVariableOverrides map[string]string) (*Scenario, error) { if !utils.FileExists(path) { return nil, fmt.Errorf("markdown file '%s' does not exist", path) } @@ -85,15 +82,62 @@ func CreateScenarioFromMarkdown(path string, languagesToExecute []string) (*Scen } } + // Convert the markdonw into an AST and extract the scenario variables. markdown := parsers.ParseMarkdownIntoAst(source) scenarioVariables := parsers.ExtractScenarioVariablesFromAst(markdown, source) for key, value := range scenarioVariables { environmentVariables[key] = value } + // Extract the code blocks from the markdown file. codeBlocks := parsers.ExtractCodeBlocksFromAst(markdown, source, languagesToExecute) logging.GlobalLogger.WithField("CodeBlocks", codeBlocks).Debugf("Found %d code blocks", len(codeBlocks)) + varsToExport := utils.CopyMap(environmentVariableOverrides) + for key, value := range environmentVariableOverrides { + logging.GlobalLogger.Debugf("Attempting to override %s with %s", key, value) + exportRegex := regexp.MustCompile(fmt.Sprintf(`export %s=["']?([a-z-A-Z0-9_]+)["']?`, key)) + + for index, codeBlock := range codeBlocks { + matches := exportRegex.FindAllStringSubmatch(codeBlock.Content, -1) + + if len(matches) != 0 { + logging.GlobalLogger.Debugf("Found %d matches for %s, deleting from varsToExport", len(matches), key) + delete(varsToExport, key) + } else { + logging.GlobalLogger.Debugf("Found no matches for %s inside of %s", key, codeBlock.Content) + } + + for _, match := range matches { + wholeLine := match[0] + oldValue := match[1] + + // Replace the old export with the new export statement + newLine := strings.Replace(wholeLine, oldValue, value, 1) + logging.GlobalLogger.Debugf("Replacing '%s' with '%s'", wholeLine, newLine) + + codeBlocks[index].Content = strings.Replace(codeBlock.Content, wholeLine, newLine, 1) + } + + } + } + + if len(varsToExport) != 0 { + logging.GlobalLogger.Debugf("Found %d variables to export", len(varsToExport)) + exportCodeBlock := parsers.CodeBlock{ + Language: "bash", + Content: "", + Header: "Exporting variables defined via the CLI and not in the markdown file.", + ExpectedOutput: parsers.ExpectedOutputBlock{}, + } + for key, value := range varsToExport { + exportCodeBlock.Content += fmt.Sprintf("export %s=\"%s\"\n", key, value) + } + + codeBlocks = append([]parsers.CodeBlock{exportCodeBlock}, codeBlocks...) + } + + // Group the code blocks into steps. steps := groupCodeBlocksIntoSteps(codeBlocks) title, err := parsers.ExtractScenarioTitleFromAst(markdown, source) if err != nil { @@ -110,6 +154,12 @@ func CreateScenarioFromMarkdown(path string, languagesToExecute []string) (*Scen }, nil } +func (s *Scenario) OverwriteEnvironmentVariables(environmentVariables map[string]string) { + for key, value := range environmentVariables { + s.Environment[key] = value + } +} + // Convert a scenario into a shell script func (s *Scenario) ToShellScript() string { var script strings.Builder diff --git a/scenarios/ocd/CreateLinuxVMAndSSH/README.md b/scenarios/ocd/CreateLinuxVMAndSSH/README.md index 6cffa7f8..0cfe205a 100644 --- a/scenarios/ocd/CreateLinuxVMAndSSH/README.md +++ b/scenarios/ocd/CreateLinuxVMAndSSH/README.md @@ -7,7 +7,7 @@ The First step in this tutorial is to define environment variables ```bash export UNIQUE_POSTFIX="$(($RANDOM % 100 + 1))" export MY_RESOURCE_GROUP_NAME="myResourceGroup$UNIQUE_POSTFIX" -export MY_LOCATION=EastUS +export REGION=EastUS export MY_VM_NAME="myVM$UNIQUE_POSTFIX" export MY_USERNAME=azureuser export MY_VM_IMAGE="Canonical:0001-com-ubuntu-minimal-jammy:minimal-22_04-lts-gen2:latest" @@ -19,10 +19,10 @@ In order to run commands against Azure using the CLI you need to login. This is # Create a resource group -A resource group is a container for related resources. All resources must be placed in a resource group. We will create one for this tutorial. The following command creates a resource group with the previously defined $MY_RESOURCE_GROUP_NAME and $MY_LOCATION parameters. +A resource group is a container for related resources. All resources must be placed in a resource group. We will create one for this tutorial. The following command creates a resource group with the previously defined $MY_RESOURCE_GROUP_NAME and $REGION parameters. ```bash -az group create --name $MY_RESOURCE_GROUP_NAME --location $MY_LOCATION +az group create --name $MY_RESOURCE_GROUP_NAME --location $REGION ``` Results: From 0aae1d0ce6f3c79aa5cca2a0aeb586802a2cc7c2 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 11 Sep 2023 19:37:11 +0000 Subject: [PATCH 152/226] [update] all scenarios to use REGION as their location variable. --- cmd/ie/commands/execute.go | 9 +++-- scenarios/ocd/CreateAKSDeployment/README.md | 14 ++++---- .../README.md | 36 +++++++++---------- scenarios/ocd/CreateLinuxVMLAMP/README.md | 18 +++++----- 4 files changed, 38 insertions(+), 39 deletions(-) diff --git a/cmd/ie/commands/execute.go b/cmd/ie/commands/execute.go index 1dac8567..9127967d 100644 --- a/cmd/ie/commands/execute.go +++ b/cmd/ie/commands/execute.go @@ -45,9 +45,8 @@ var executeCommand = &cobra.Command{ environmentVariables, _ := cmd.Flags().GetStringArray("var") workingDirectory, _ := cmd.Flags().GetString("working-directory") - fmt.Println("environmentVariables: ", environmentVariables) - userDefinedEnvironmentVariables := make(map[string]string) - + // Parse the environment variables + cliEnvironmentVariables := make(map[string]string) for _, environmentVariable := range environmentVariables { keyValuePair := strings.SplitN(environmentVariable, "=", 2) if len(keyValuePair) != 2 { @@ -57,7 +56,7 @@ var executeCommand = &cobra.Command{ os.Exit(1) } - userDefinedEnvironmentVariables[keyValuePair[0]] = keyValuePair[1] + cliEnvironmentVariables[keyValuePair[0]] = keyValuePair[1] } innovationEngine := engine.NewEngine(engine.EngineConfiguration{ @@ -70,7 +69,7 @@ var executeCommand = &cobra.Command{ }) // Parse the markdown file and create a scenario - scenario, err := engine.CreateScenarioFromMarkdown(markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}, userDefinedEnvironmentVariables) + scenario, err := engine.CreateScenarioFromMarkdown(markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}, cliEnvironmentVariables) if err != nil { logging.GlobalLogger.Errorf("Error creating scenario: %s", err) fmt.Printf("Error creating scenario: %s", err) diff --git a/scenarios/ocd/CreateAKSDeployment/README.md b/scenarios/ocd/CreateAKSDeployment/README.md index 01d8c336..341ad202 100644 --- a/scenarios/ocd/CreateAKSDeployment/README.md +++ b/scenarios/ocd/CreateAKSDeployment/README.md @@ -10,7 +10,7 @@ export SSL_EMAIL_ADDRESS="$(az account show --query user.name --output tsv)" export NETWORK_PREFIX="$(($RANDOM % 254 + 1))" export RANDOM_ID="$(openssl rand -hex 3)" export MY_RESOURCE_GROUP_NAME="myResourceGroup$RANDOM_ID" -export MY_LOCATION="eastus" +export REGION="eastus" export MY_AKS_CLUSTER_NAME="myAKSCluster$RANDOM_ID" export MY_PUBLIC_IP_NAME="myPublicIP$RANDOM_ID" export MY_DNS_LABEL="mydnslabel$RANDOM_ID" @@ -18,15 +18,15 @@ export MY_VNET_NAME="myVNet$RANDOM_ID" export MY_VNET_PREFIX="10.$NETWORK_PREFIX.0.0/16" export MY_SN_NAME="mySN$RANDOM_ID" export MY_SN_PREFIX="10.$NETWORK_PREFIX.0.0/22" -export FQDN="${MY_DNS_LABEL}.${MY_LOCATION}.cloudapp.azure.com" +export FQDN="${MY_DNS_LABEL}.${REGION}.cloudapp.azure.com" ``` # Create a resource group -A resource group is a container for related resources. All resources must be placed in a resource group. We will create one for this tutorial. The following command creates a resource group with the previously defined $MY_RESOURCE_GROUP_NAME and $MY_LOCATION parameters. +A resource group is a container for related resources. All resources must be placed in a resource group. We will create one for this tutorial. The following command creates a resource group with the previously defined $MY_RESOURCE_GROUP_NAME and $REGION parameters. ```bash -az group create --name $MY_RESOURCE_GROUP_NAME --location $MY_LOCATION +az group create --name $MY_RESOURCE_GROUP_NAME --location $REGION ``` Results: @@ -52,7 +52,7 @@ A virtual network is the fundamental building block for private networks in Azur ```bash az network vnet create \ --resource-group $MY_RESOURCE_GROUP_NAME \ - --location $MY_LOCATION \ + --location $REGION \ --name $MY_VNET_NAME \ --address-prefix $MY_VNET_PREFIX \ --subnet-name $MY_SN_NAME \ @@ -115,7 +115,7 @@ az aks create \ --auto-upgrade-channel stable \ --enable-cluster-autoscaler \ --enable-addons monitoring \ - --location $MY_LOCATION \ + --location $REGION \ --node-count 1 \ --min-count 1 \ --max-count 3 \ @@ -156,7 +156,7 @@ kubectl get nodes ## Install NGINX Ingress Controller ```bash -export MY_STATIC_IP=$(az network public-ip create --resource-group MC_${MY_RESOURCE_GROUP_NAME}_${MY_AKS_CLUSTER_NAME}_${MY_LOCATION} --location ${MY_LOCATION} --name ${MY_PUBLIC_IP_NAME} --dns-name ${MY_DNS_LABEL} --sku Standard --allocation-method static --version IPv4 --zone 1 2 3 --query publicIp.ipAddress -o tsv) +export MY_STATIC_IP=$(az network public-ip create --resource-group MC_${MY_RESOURCE_GROUP_NAME}_${MY_AKS_CLUSTER_NAME}_${REGION} --location ${MY_LOCATION} --name ${MY_PUBLIC_IP_NAME} --dns-name ${MY_DNS_LABEL} --sku Standard --allocation-method static --version IPv4 --zone 1 2 3 --query publicIp.ipAddress -o tsv) helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx helm repo update diff --git a/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md b/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md index 632f4147..7c524515 100644 --- a/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md +++ b/scenarios/ocd/CreateContainerAppDeploymentFromSource/README.md @@ -16,7 +16,7 @@ The first step in this tutorial is to define environment variables. **Replace th ```bash export SUFFIX=$(cat /dev/urandom | LC_ALL=C tr -dc 'a-z0-9' | fold -w 8 | head -n 1) export MY_RESOURCE_GROUP_NAME=rg$SUFFIX -export MY_LOCATION=westus +export REGION=westus export MY_STORAGE_ACCOUNT_NAME=storage$SUFFIX export MY_DATABASE_SERVER_NAME=dbserver$SUFFIX export MY_DATABASE_NAME=db$SUFFIX @@ -43,10 +43,10 @@ In order to run commands against Azure using [the CLI ](https://learn.microsoft. ## Create a resource group -A resource group is a container for related resources. All resources must be placed in a resource group. We will create one for this tutorial. The following command creates a resource group with the previously defined $MY_RESOURCE_GROUP_NAME and $MY_LOCATION parameters. +A resource group is a container for related resources. All resources must be placed in a resource group. We will create one for this tutorial. The following command creates a resource group with the previously defined $MY_RESOURCE_GROUP_NAME and $REGION parameters. ```bash -az group create --name $MY_RESOURCE_GROUP_NAME --location $MY_LOCATION +az group create --name $MY_RESOURCE_GROUP_NAME --location $REGION ``` Results: @@ -55,7 +55,7 @@ Results: ```json { "id": "/subscriptions/ab9d8365-2f65-47a4-8df4-7e40db70c8d2/resourceGroups/$MY_RESOURCE_GROUP_NAME", - "location": "$MY_LOCATION", + "location": "$REGION", "managedBy": null, "name": "$MY_RESOURCE_GROUP_NAME", "properties": { @@ -71,7 +71,7 @@ Results: To create a storage account in this resource group we need to run a simple command. To this command, we are passing the name of the storage account, the resource group to deploy it in, the physical region to deploy it in, and the SKU of the storage account. All values are configured using environment variables. ```bash -az storage account create --name $MY_STORAGE_ACCOUNT_NAME --resource-group $MY_RESOURCE_GROUP_NAME --location $MY_LOCATION --sku Standard_LRS +az storage account create --name $MY_STORAGE_ACCOUNT_NAME --resource-group $MY_RESOURCE_GROUP_NAME --location $REGION --sku Standard_LRS ``` Results: @@ -129,7 +129,7 @@ Results: "kind": "StorageV2", "largeFileSharesState": null, "lastGeoFailoverTime": null, - "location": "$MY_LOCATION", + "location": "$REGION", "minimumTlsVersion": "TLS1_0", "name": "$MY_STORAGE_ACCOUNT_NAME", "networkRuleSet": { @@ -149,7 +149,7 @@ Results: "table": "https://$MY_STORAGE_ACCOUNT_NAME.table.core.windows.net/", "web": "https://$MY_STORAGE_ACCOUNT_NAME.z22.web.core.windows.net/" }, - "primaryLocation": "$MY_LOCATION", + "primaryLocation": "$REGION", "privateEndpointConnections": [], "provisioningState": "Succeeded", "publicNetworkAccess": null, @@ -213,7 +213,7 @@ az postgres flexible-server create \ --name $MY_DATABASE_SERVER_NAME \ --database-name $MY_DATABASE_NAME \ --resource-group $MY_RESOURCE_GROUP_NAME \ - --location $MY_LOCATION \ + --location $REGION \ --tier Burstable \ --sku-name Standard_B1ms \ --storage-size 32 \ @@ -233,7 +233,7 @@ Results: "firewallName": "FirewallIPAddress_2023-8-10_10-53-21", "host": "$MY_DATABASE_NAME.postgres.database.azure.com", "id": "/subscriptions/ab9d8365-2f65-47a4-8df4-7e40db70c8d2/resourceGroups/$MY_RESOURCE_GROUP_NAME/providers/Microsoft.DBforPostgreSQL/flexibleServers/$MY_DATABASE_NAME", - "location": "$MY_LOCATION", + "location": "$REGION", "password": "$MY_DATABASE_PASSWORD", "resourceGroup": "$MY_RESOURCE_GROUP_NAME", "skuname": "Standard_B1ms", @@ -259,7 +259,7 @@ We will be creating a Computer Vision resource to be able to identify cats or do az cognitiveservices account create \ --name $MY_COMPUTER_VISION_NAME \ --resource-group $MY_RESOURCE_GROUP_NAME \ - --location $MY_LOCATION \ + --location $REGION \ --kind ComputerVision \ --sku S1 \ --yes @@ -274,7 +274,7 @@ Results: "id": "/subscriptions/ab9d8365-2f65-47a4-8df4-7e40db70c8d2/resourceGroups/$MY_RESOURCE_GROUP_NAME/providers/Microsoft.CognitiveServices/accounts/$MY_COMPUTER_VISION_NAME", "identity": null, "kind": "ComputerVision", - "location": "$MY_LOCATION", + "location": "$REGION", "name": "$MY_COMPUTER_VISION_NAME", "properties": { "allowedFqdnList": null, @@ -365,10 +365,10 @@ Results: "disableLocalAuth": null, "dynamicThrottlingEnabled": null, "encryption": null, - "endpoint": "https://$MY_LOCATION.api.cognitive.microsoft.com/", + "endpoint": "https://$REGION.api.cognitive.microsoft.com/", "endpoints": { - "Computer Vision": "https://$MY_LOCATION.api.cognitive.microsoft.com/", - "Container": "https://$MY_LOCATION.api.cognitive.microsoft.com/" + "Computer Vision": "https://$REGION.api.cognitive.microsoft.com/", + "Container": "https://$REGION.api.cognitive.microsoft.com/" }, "internalId": "93645816f9594fe49a8f4023c0bf34b4", "isMigrated": false, @@ -430,7 +430,7 @@ This command will create an Azure Container Registry resource to host our Docker az containerapp up \ --name $MY_CONTAINER_APP_NAME \ --resource-group $MY_RESOURCE_GROUP_NAME \ - --location $MY_LOCATION \ + --location $REGION \ --environment $MY_CONTAINER_APP_ENV_NAME \ --context-path computer-vision-nextjs-webapp \ --source computer-vision-nextjs-webapp \ @@ -472,7 +472,7 @@ Results: "customDomains": null, "exposedPort": 0, "external": true, - "fqdn": "$MY_CONTAINER_APP_NAME.kindocean-a506af76.$MY_LOCATION.azurecontainerapps.io", + "fqdn": "$MY_CONTAINER_APP_NAME.kindocean-a506af76.$REGION.azurecontainerapps.io", "ipSecurityRestrictions": null, "stickySessions": null, "targetPort": 3000, @@ -491,9 +491,9 @@ Results: }, "customDomainVerificationId": "06C64CD176439F8B6CCBBE1B531758828A5CACEABFB30B4DC9750641532924F6", "environmentId": "/subscriptions/fake3265-2f64-47a4-8df4-7e41ab70c8dh/resourceGroups/$MY_RESOURCE_GROUP_NAME/providers/Microsoft.App/managedEnvironments/$MY_CONTAINER_APP_ENV_NAME", - "eventStreamEndpoint": "https://$MY_LOCATION.azurecontainerapps.dev/subscriptions/eb9d8265-2f64-47a4-8df4-7e41db70c8d8/resourceGroups/$MY_RESOURCE_GROUP_NAME/containerApps/$MY_CONTAINER_APP_NAME/eventstream", + "eventStreamEndpoint": "https://$REGION.azurecontainerapps.dev/subscriptions/eb9d8265-2f64-47a4-8df4-7e41db70c8d8/resourceGroups/$MY_RESOURCE_GROUP_NAME/containerApps/$MY_CONTAINER_APP_NAME/eventstream", "latestReadyRevisionName": "$MY_CONTAINER_APP_NAME--jl6fh75", - "latestRevisionFqdn": "$MY_CONTAINER_APP_NAME--jl6fh75.kindocean-a506af76.$MY_LOCATION.azurecontainerapps.io", + "latestRevisionFqdn": "$MY_CONTAINER_APP_NAME--jl6fh75.kindocean-a506af76.$REGION.azurecontainerapps.io", "latestRevisionName": "$MY_CONTAINER_APP_NAME--jl6fh75", "managedEnvironmentId": "/subscriptions/eb9d8265-2f64-47a4-8df4-7e41db70c8d8/resourceGroups/$MY_RESOURCE_GROUP_NAME/providers/Microsoft.App/managedEnvironments/$MY_CONTAINER_APP_ENV_NAME", "outboundIpAddresses": ["20.237.221.47"], diff --git a/scenarios/ocd/CreateLinuxVMLAMP/README.md b/scenarios/ocd/CreateLinuxVMLAMP/README.md index 3d660e31..991fb289 100644 --- a/scenarios/ocd/CreateLinuxVMLAMP/README.md +++ b/scenarios/ocd/CreateLinuxVMLAMP/README.md @@ -22,7 +22,7 @@ First we will define a few variables that will help with the configuration of th export NETWORK_PREFIX="$(($RANDOM % 254 + 1))" export RANDOM_ID="$(openssl rand -hex 3)" export MY_RESOURCE_GROUP_NAME="myResourceGroup$RANDOM_ID" -export MY_LOCATION="eastus" +export REGION="eastus" export MY_VM_NAME="myVMName$RANDOM_ID" export MY_VM_USERNAME="azureadmin" export MY_VM_SIZE='Standard_DS2_v2' @@ -44,13 +44,13 @@ export MY_WP_ADMIN_PW="$(openssl rand -base64 32)" export MY_WP_ADMIN_USER="wpcliadmin" export MY_AZURE_USER=$(az account show --query user.name --output tsv) export MY_AZURE_USER_ID=$(az ad user list --filter "mail eq '$MY_AZURE_USER'" --query "[0].id" -o tsv) -export FQDN="${MY_DNS_LABEL}.${MY_LOCATION}.cloudapp.azure.com" +export FQDN="${MY_DNS_LABEL}.${REGION}.cloudapp.azure.com" ``` ```bash az group create \ --name $MY_RESOURCE_GROUP_NAME \ - --location $MY_LOCATION -o JSON + --location $REGION -o JSON ``` Results: @@ -80,7 +80,7 @@ Use [az network vnet create](https://learn.microsoft.com/cli/azure/network/vnet# az network vnet create \ --name $MY_VNET_NAME \ --resource-group $MY_RESOURCE_GROUP_NAME \ - --location $MY_LOCATION \ + --location $REGION \ --address-prefix $MY_VNET_PREFIX \ --subnet-name $MY_SN_NAME \ --subnet-prefixes $MY_SN_PREFIX -o JSON @@ -131,7 +131,7 @@ Use [az network public-ip create](https://learn.microsoft.com/cli/azure/network/ ```bash az network public-ip create \ --name $MY_PUBLIC_IP_NAME \ - --location $MY_LOCATION \ + --location $REGION \ --resource-group $MY_RESOURCE_GROUP_NAME \ --dns-name $MY_DNS_LABEL \ --sku Standard \ @@ -183,7 +183,7 @@ Security rules in network security groups enable you to filter the type of netwo az network nsg create \ --name $MY_NSG_NAME \ --resource-group $MY_RESOURCE_GROUP_NAME \ - --location $MY_LOCATION -o JSON + --location $REGION -o JSON ``` Results: @@ -277,7 +277,7 @@ You'll use [az network nic create](https://learn.microsoft.com/cli/azure/network az network nic create \ --resource-group $MY_RESOURCE_GROUP_NAME \ --name $MY_VM_NIC_NAME \ - --location $MY_LOCATION \ + --location $REGION \ --ip-forwarding false \ --subnet $MY_SN_NAME \ --vnet-name $MY_VNET_NAME \ @@ -512,7 +512,7 @@ az mysql flexible-server create \ --auto-scale-iops Disabled \ --high-availability Disabled \ --iops 500 \ - --location $MY_LOCATION \ + --location $REGION \ --name $MY_MYSQL_DB_NAME \ --database-name wp001 \ --resource-group $MY_RESOURCE_GROUP_NAME \ @@ -614,7 +614,7 @@ az vm create \ --admin-username $MY_VM_USERNAME \ --authentication-type ssh \ --image $MY_VM_IMAGE \ - --location $MY_LOCATION \ + --location $REGION \ --nic-delete-option Delete \ --os-disk-caching ReadOnly \ --os-disk-delete-option Delete \ From 14e2aca5d7aedc12bfdab44ff0ce47bd87779fee Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 12 Sep 2023 16:38:53 +0000 Subject: [PATCH 153/226] [update] documentation and organization. --- cmd/ie/commands/execute.go | 24 ++++++++++++++---------- internal/engine/scenario.go | 17 ++++++++++------- internal/shells/bash.go | 4 +++- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/cmd/ie/commands/execute.go b/cmd/ie/commands/execute.go index 9127967d..2f2310e8 100644 --- a/cmd/ie/commands/execute.go +++ b/cmd/ie/commands/execute.go @@ -22,6 +22,8 @@ func init() { executeCommand.PersistentFlags().String("correlation-id", "", "Adds a correlation ID to the user agent used by a scenarios azure-cli commands.") executeCommand.PersistentFlags().String("subscription", "", "Sets the subscription ID used by a scenarios azure-cli commands. Will rely on the default subscription if not set.") executeCommand.PersistentFlags().String("working-directory", ".", "Sets the working directory for innovation engine to operate out of. Restores the current working directory when finished.") + + // StringArray flags executeCommand.PersistentFlags().StringArray("var", []string{}, "Sets an environment variable for the scenario. Format: --var =") } @@ -39,13 +41,15 @@ var executeCommand = &cobra.Command{ verbose, _ := cmd.Flags().GetBool("verbose") doNotDelete, _ := cmd.Flags().GetBool("do-not-delete") + subscription, _ := cmd.Flags().GetString("subscription") correlationId, _ := cmd.Flags().GetString("correlation-id") environment, _ := cmd.Flags().GetString("environment") - environmentVariables, _ := cmd.Flags().GetStringArray("var") workingDirectory, _ := cmd.Flags().GetString("working-directory") - // Parse the environment variables + environmentVariables, _ := cmd.Flags().GetStringArray("var") + + // Parse the environment variables from the command line into a map cliEnvironmentVariables := make(map[string]string) for _, environmentVariable := range environmentVariables { keyValuePair := strings.SplitN(environmentVariable, "=", 2) @@ -59,6 +63,14 @@ var executeCommand = &cobra.Command{ cliEnvironmentVariables[keyValuePair[0]] = keyValuePair[1] } + // Parse the markdown file and create a scenario + scenario, err := engine.CreateScenarioFromMarkdown(markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}, cliEnvironmentVariables) + if err != nil { + logging.GlobalLogger.Errorf("Error creating scenario: %s", err) + fmt.Printf("Error creating scenario: %s", err) + os.Exit(1) + } + innovationEngine := engine.NewEngine(engine.EngineConfiguration{ Verbose: verbose, DoNotDelete: doNotDelete, @@ -68,14 +80,6 @@ var executeCommand = &cobra.Command{ WorkingDirectory: workingDirectory, }) - // Parse the markdown file and create a scenario - scenario, err := engine.CreateScenarioFromMarkdown(markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}, cliEnvironmentVariables) - if err != nil { - logging.GlobalLogger.Errorf("Error creating scenario: %s", err) - fmt.Printf("Error creating scenario: %s", err) - os.Exit(1) - } - // Execute the scenario err = innovationEngine.ExecuteScenario(scenario) if err != nil { diff --git a/internal/engine/scenario.go b/internal/engine/scenario.go index 2de02b55..04dd8a86 100644 --- a/internal/engine/scenario.go +++ b/internal/engine/scenario.go @@ -28,8 +28,7 @@ type Scenario struct { } // Groups the codeblocks into steps based on the header of the codeblock. -// This organizes the codeblocks into steps that can be executed in a linear -// order. +// This organizes the codeblocks into steps that can be executed linearly. func groupCodeBlocksIntoSteps(blocks []parsers.CodeBlock) []Step { var groupedSteps []Step var headerIndex = make(map[string]int) @@ -109,21 +108,25 @@ func CreateScenarioFromMarkdown(path string, languagesToExecute []string, enviro } for _, match := range matches { - wholeLine := match[0] + oldLine := match[0] oldValue := match[1] // Replace the old export with the new export statement - newLine := strings.Replace(wholeLine, oldValue, value, 1) - logging.GlobalLogger.Debugf("Replacing '%s' with '%s'", wholeLine, newLine) + newLine := strings.Replace(oldLine, oldValue, value, 1) + logging.GlobalLogger.Debugf("Replacing '%s' with '%s'", oldLine, newLine) - codeBlocks[index].Content = strings.Replace(codeBlock.Content, wholeLine, newLine, 1) + // Update the code block with the new export statement + codeBlocks[index].Content = strings.Replace(codeBlock.Content, oldLine, newLine, 1) } } } + // If there are some variables left after going through each of the codeblocks, + // do not update the scenario + // steps. if len(varsToExport) != 0 { - logging.GlobalLogger.Debugf("Found %d variables to export", len(varsToExport)) + logging.GlobalLogger.Debugf("Found %d variables to add to the scenario as a step.", len(varsToExport)) exportCodeBlock := parsers.CodeBlock{ Language: "bash", Content: "", diff --git a/internal/shells/bash.go b/internal/shells/bash.go index eca54b91..3419ca17 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -96,12 +96,14 @@ func ExecuteBashCommand(command string, config BashCommandConfiguration) (Comman var stdoutBuffer, stderrBuffer bytes.Buffer + // If the command requires interaction, we provide the user with the ability + // to interact with the command. However, we cannot capture the buffer this + // way. if config.InteractiveCommand { commandToExecute.Stdout = os.Stdout commandToExecute.Stderr = os.Stderr commandToExecute.Stdin = os.Stdin } else { - // Capture std out and std err as separate buffers. commandToExecute.Stdout = &stdoutBuffer commandToExecute.Stderr = &stderrBuffer } From feff2bc00df4b8fd3cd64cd146c593d3528e08ea Mon Sep 17 00:00:00 2001 From: Adrian Joian <6505576+naioja@users.noreply.github.com> Date: Tue, 19 Sep 2023 21:09:08 +0200 Subject: [PATCH 154/226] Fixing all markdownlint messages. (#35) --- scenarios/ocd/CreateAKSDeployment/README.md | 138 ++++++++++++-------- 1 file changed, 83 insertions(+), 55 deletions(-) diff --git a/scenarios/ocd/CreateAKSDeployment/README.md b/scenarios/ocd/CreateAKSDeployment/README.md index 341ad202..6251c9c6 100644 --- a/scenarios/ocd/CreateAKSDeployment/README.md +++ b/scenarios/ocd/CreateAKSDeployment/README.md @@ -1,9 +1,10 @@ # Quickstart: Deploy a Scalable & Secure Azure Kubernetes Service cluster using the Azure CLI -Welcome to this tutorial where we will take you step by step in creating an Azure Kubernetes Web Application that is secured via https. This tutorial assumes you are logged into Azure CLI already and have selected a subscription to use with the CLI. It also assumes that you have Helm installed (Instructions can be found here https://helm.sh/docs/intro/install/). + +Welcome to this tutorial where we will take you step by step in creating an Azure Kubernetes Web Application that is secured via https. This tutorial assumes you are logged into Azure CLI already and have selected a subscription to use with the CLI. It also assumes that you have Helm installed ([Instructions can be found here](https://helm.sh/docs/intro/install/)). ## Define Environment Variables -The First step in this tutorial is to define environment variables +The First step in this tutorial is to define environment variables. ```bash export SSL_EMAIL_ADDRESS="$(az account show --query user.name --output tsv)" @@ -21,13 +22,14 @@ export MY_SN_PREFIX="10.$NETWORK_PREFIX.0.0/22" export FQDN="${MY_DNS_LABEL}.${REGION}.cloudapp.azure.com" ``` -# Create a resource group +## Create a resource group A resource group is a container for related resources. All resources must be placed in a resource group. We will create one for this tutorial. The following command creates a resource group with the previously defined $MY_RESOURCE_GROUP_NAME and $REGION parameters. ```bash az group create --name $MY_RESOURCE_GROUP_NAME --location $REGION ``` + Results: @@ -58,6 +60,7 @@ az network vnet create \ --subnet-name $MY_SN_NAME \ --subnet-prefixes $MY_SN_PREFIX ``` + Results: @@ -95,6 +98,7 @@ Results: ``` ## Register to AKS Azure Resource Providers + Verify Microsoft.OperationsManagement and Microsoft.OperationalInsights providers are registered on your subscription. These are Azure resource providers required to support [Container insights](https://docs.microsoft.com/en-us/azure/azure-monitor/containers/container-insights-overview). To check the registration status, run the following commands ```bash @@ -102,10 +106,12 @@ az provider register --namespace Microsoft.OperationsManagement az provider register --namespace Microsoft.OperationalInsights ``` -## Create AKS Cluster +## Create AKS Cluster + Create an AKS cluster using the az aks create command with the --enable-addons monitoring parameter to enable Container insights. The following example creates an autoscaling, availability zone enabled cluster named myAKSCluster: This will take a few minutes + ```bash export MY_SN_ID=$(az network vnet subnet list --resource-group $MY_RESOURCE_GROUP_NAME --vnet-name $MY_VNET_NAME --query "[0].id" --output tsv) @@ -128,30 +134,31 @@ az aks create \ ``` ## Connect to the cluster + To manage a Kubernetes cluster, use the Kubernetes command-line client, kubectl. kubectl is already installed if you use Azure Cloud Shell. 1. Install az aks CLI locally using the az aks install-cli command -```bash -if ! [ -x "$(command -v kubectl)" ]; then az aks install-cli; fi -``` + ```bash + if ! [ -x "$(command -v kubectl)" ]; then az aks install-cli; fi + ``` 2. Configure kubectl to connect to your Kubernetes cluster using the az aks get-credentials command. The following command: - Downloads credentials and configures the Kubernetes CLI to use them. - - Uses ~/.kube/config, the default location for the Kubernetes configuration file. Specify a different location for your Kubernetes configuration file using --file argument. + - Uses ~/.kube/config, the default location for the Kubernetes configuration file. Specify a different location for your Kubernetes configuration file using --file argument. -> [!WARNING] -> This will overwrite any existing credentials with the same entry + > [!WARNING] + > This will overwrite any existing credentials with the same entry -```bash -az aks get-credentials --resource-group $MY_RESOURCE_GROUP_NAME --name $MY_AKS_CLUSTER_NAME --overwrite-existing -``` + ```bash + az aks get-credentials --resource-group $MY_RESOURCE_GROUP_NAME --name $MY_AKS_CLUSTER_NAME --overwrite-existing + ``` 3. Verify the connection to your cluster using the kubectl get command. This command returns a list of the cluster nodes. -```bash -kubectl get nodes -``` + ```bash + kubectl get nodes + ``` ## Install NGINX Ingress Controller @@ -168,7 +175,8 @@ helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \ --set controller.service.annotations."service\.beta\.kubernetes\.io/azure-load-balancer-health-probe-request-path"=/healthz ``` -## Deploy the Application +## Deploy the Application + A Kubernetes manifest file defines a cluster's desired state, such as which container images to run. In this quickstart, you will use a manifest to create all objects needed to run the Azure Vote application. This manifest includes two Kubernetes deployments: @@ -183,7 +191,8 @@ Two Kubernetes Services are also created: Finally, an Ingress resource is created to route traffic to the Azure Vote application. -A test voting app YML file is already prepared. To deploy this app run the following command +A test voting app YML file is already prepared. To deploy this app run the following command + ```bash kubectl apply -f azure-vote-start.yml ``` @@ -192,13 +201,15 @@ kubectl apply -f azure-vote-start.yml Validate that the application is running by either visiting the public ip or the application url. The application url can be found by running the following command: ->[!Note] +>[!Note] >It often takes 2-3 minutes for the PODs to be created and the site to be reachable via http + ```bash runtime="5 minute"; endtime=$(date -ud "$runtime" +%s); while [[ $(date -u +%s) -le $endtime ]]; do STATUS=$(kubectl get pods -l app=azure-vote-front -o 'jsonpath={..status.conditions[?(@.type=="Ready")].status}'); echo $STATUS; if [ "$STATUS" = 'True' ]; then break; else sleep 10; fi; done curl "http://$FQDN" ``` + Results: @@ -234,75 +245,90 @@ Results: ``` -# Add HTTPS termination to custom domain +## Add HTTPS termination to custom domain + At this point in the tutorial you have an AKS web app with NGINX as the Ingress controller and a custom domain you can use to access your application. The next step is to add an SSL certificate to the domain so that users can reach your application securely via https. ## Set Up Cert Manager + In order to add HTTPS we are going to use Cert Manager. Cert Manager is an open source tool used to obtain and manage SSL certificate for Kubernetes deployments. Cert Manager will obtain certificates from a variety of Issuers, both popular public Issuers as well as private Issuers, and ensure the certificates are valid and up-to-date, and will attempt to renew certificates at a configured time before expiry. 1. In order to install cert-manager, we must first create a namespace to run it in. This tutorial will install cert-manager into the cert-manager namespace. It is possible to run cert-manager in a different namespace, although you will need to make modifications to the deployment manifests. -```bash -kubectl create namespace cert-manager -``` + + ```bash + kubectl create namespace cert-manager + ``` 2. We can now install cert-manager. All resources are included in a single YAML manifest file. This can be installed by running the following: -```bash -kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.7.0/cert-manager.crds.yaml -``` + + ```bash + kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.7.0/cert-manager.crds.yaml + ``` 3. Add the certmanager.k8s.io/disable-validation: "true" label to the cert-manager namespace by running the following. This will allow the system resources that cert-manager requires to bootstrap TLS to be created in its own namespace. -```bash -kubectl label namespace cert-manager certmanager.k8s.io/disable-validation=true -``` + + ```bash + kubectl label namespace cert-manager certmanager.k8s.io/disable-validation=true + ``` ## Obtain certificate via Helm Charts + Helm is a Kubernetes deployment tool for automating creation, packaging, configuration, and deployment of applications and services to Kubernetes clusters. Cert-manager provides Helm charts as a first-class method of installation on Kubernetes. 1. Add the Jetstack Helm repository -This repository is the only supported source of cert-manager charts. There are some other mirrors and copies across the internet, but those are entirely unofficial and could present a security risk. -```bash -helm repo add jetstack https://charts.jetstack.io -``` -2. Update local Helm Chart repository cache -```bash -helm repo update -``` + This repository is the only supported source of cert-manager charts. There are some other mirrors and copies across the internet, but those are entirely unofficial and could present a security risk. + + ```bash + helm repo add jetstack https://charts.jetstack.io + ``` + +2. Update local Helm Chart repository cache + + ```bash + helm repo update + ``` 3. Install Cert-Manager addon via helm by running the following: -```bash -helm install cert-manager jetstack/cert-manager --namespace cert-manager --version v1.7.0 -``` + + ```bash + helm install cert-manager jetstack/cert-manager --namespace cert-manager --version v1.7.0 + ``` 4. Apply Certificate Issuer YAML File ClusterIssuers are Kubernetes resources that represent certificate authorities (CAs) that are able to generate signed certificates by honoring certificate signing requests. All cert-manager certificates require a referenced issuer that is in a ready condition to attempt to honor the request. - The issuer we are using can be found in the `cluster-issuer-prod.yml file` -```bash -envsubst < cluster-issuer-prod.yml | kubectl apply -f - -``` -5. Upate Voting App Application to use Cert-Manager to obtain an SSL Certificate. + ```bash + envsubst < cluster-issuer-prod.yml | kubectl apply -f - + ``` + +5. Upate Voting App Application to use Cert-Manager to obtain an SSL Certificate. The full YAML file can be found in `azure-vote-nginx-ssl.yml` -```bash -envsubst < azure-vote-nginx-ssl.yml | kubectl apply -f - -``` + + ```bash + envsubst < azure-vote-nginx-ssl.yml | kubectl apply -f - + ``` + ## Validate application is working Wait for SSL certificate to issue. The following command will query the status of the SSL certificate for 3 minutes. In rare occasions it may take up to 15 minutes for Lets Encrypt to issue a successful challenge and the ready state to be 'True' + ```bash runtime="10 minute"; endtime=$(date -ud "$runtime" +%s); while [[ $(date -u +%s) -le $endtime ]]; do STATUS=$(kubectl get certificate --output jsonpath={..status.conditions[0].status}); echo $STATUS; if [ "$STATUS" = 'True' ]; then break; else sleep 10; fi; done ``` Validate SSL certificate is True by running the follow command: + ```bash kubectl get certificate --output jsonpath={..status.conditions[0].status} ``` + Results: @@ -310,16 +336,19 @@ Results: True ``` -## Browse your AKS Deployment Secured via HTTPS! +## Browse your AKS Deployment Secured via HTTPS + Run the following command to get the HTTPS endpoint for your application: >[!Note] -> It often takes 2-3 minutes for the SSL certificate to propogate and the site to be reachable via https +> It often takes 2-3 minutes for the SSL certificate to propogate and the site to be reachable via https. + ```bash runtime="5 minute"; endtime=$(date -ud "$runtime" +%s); while [[ $(date -u +%s) -le $endtime ]]; do STATUS=$(kubectl get svc --namespace=ingress-nginx ingress-nginx-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}'); echo $STATUS; if [ "$STATUS" = "$MY_STATIC_IP" ]; then break; else sleep 10; fi; done curl https://$FQDN ``` + Results: @@ -355,10 +384,9 @@ Results: ``` - ## Next Steps -* [Azure Kubernetes Service Documentation](https://learn.microsoft.com/en-us/azure/aks/) -* [Create an Azure Container Registry](https://learn.microsoft.com/en-us/azure/aks/tutorial-kubernetes-prepare-acr?tabs=azure-cli) -* [Scale your Applciation in AKS](https://learn.microsoft.com/en-us/azure/aks/tutorial-kubernetes-scale?tabs=azure-cli) -* [Update your application in AKS](https://learn.microsoft.com/en-us/azure/aks/tutorial-kubernetes-app-update?tabs=azure-cli) +- [Azure Kubernetes Service Documentation](https://learn.microsoft.com/en-us/azure/aks/) +- [Create an Azure Container Registry](https://learn.microsoft.com/en-us/azure/aks/tutorial-kubernetes-prepare-acr?tabs=azure-cli) +- [Scale your Applciation in AKS](https://learn.microsoft.com/en-us/azure/aks/tutorial-kubernetes-scale?tabs=azure-cli) +- [Update your application in AKS](https://learn.microsoft.com/en-us/azure/aks/tutorial-kubernetes-app-update?tabs=azure-cli) From 49f268aeb6fcc3451bfafc8562951f0f22c900b6 Mon Sep 17 00:00:00 2001 From: Adrian Joian <6505576+naioja@users.noreply.github.com> Date: Tue, 19 Sep 2023 21:09:38 +0200 Subject: [PATCH 155/226] Fixing all markdownlint messages the LAMP doc. (#36) --- scenarios/ocd/CreateLinuxVMLAMP/README.md | 49 +++++++++++++++-------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/scenarios/ocd/CreateLinuxVMLAMP/README.md b/scenarios/ocd/CreateLinuxVMLAMP/README.md index 991fb289..7ba7a175 100644 --- a/scenarios/ocd/CreateLinuxVMLAMP/README.md +++ b/scenarios/ocd/CreateLinuxVMLAMP/README.md @@ -3,15 +3,16 @@ This article walks you through how to deploy an NGINX web server, Azure MySQL Flexible Server, and PHP (the LEMP stack) on an Ubuntu Linux VM in Azure. To see the LEMP server in action, you can optionally install and configure a WordPress site. In this tutorial you learn how to: > [!div class="checklist"] -> * Create a Linux Ubuntu VM + +> * Create a Linux Ubuntu VM > * Open ports 80 and 443 for web traffic > * Install and Secure NGINX, Azure Flexible MySQL Server, and PHP > * Verify installation and configuration -> * Install WordPress +> * Install WordPress -# Create RG +## Create RG -Create a resource group with the [az group create](https://learn.microsoft.com/cli/azure/group#az-group-create) command. An Azure resource group is a logical container into which Azure resources are deployed and managed. +Create a resource group with the [az group create](https://learn.microsoft.com/cli/azure/group#az-group-create) command. An Azure resource group is a logical container into which Azure resources are deployed and managed. The following example creates a resource group named `$MY_RESOURCE_GROUP_NAME` in the `eastus` location. ## Variable declaration @@ -52,6 +53,7 @@ az group create \ --name $MY_RESOURCE_GROUP_NAME \ --location $REGION -o JSON ``` + Results: @@ -69,7 +71,7 @@ Results: } ``` -# Setup LAMP networking +## Setup LAMP networking ## Create an Azure Virtual Network @@ -85,6 +87,7 @@ az network vnet create \ --subnet-name $MY_SN_NAME \ --subnet-prefixes $MY_SN_PREFIX -o JSON ``` + Results: @@ -139,6 +142,7 @@ az network public-ip create \ --version IPv4 \ --zone 1 2 3 -o JSON ``` + Results: @@ -185,6 +189,7 @@ az network nsg create \ --resource-group $MY_RESOURCE_GROUP_NAME \ --location $REGION -o JSON ``` + Results: @@ -241,6 +246,7 @@ az network nsg rule create \ --destination-address-prefix '*' \ --destination-port-range 22 80 443 -o JSON ``` + Results: @@ -284,6 +290,7 @@ az network nic create \ --network-security-group $MY_NSG_NAME \ --public-ip-address $MY_PUBLIC_IP_NAME -o JSON ``` + Results: @@ -334,7 +341,7 @@ Results: } ``` -# Cloud-init overview +## Cloud-init overview Cloud-init is a widely used approach to customize a Linux VM as it boots for the first time. You can use cloud-init to install packages and write files, or to configure users and security. As cloud-init runs during the initial boot process, there are no additional steps or required agents to apply your configuration. @@ -342,8 +349,7 @@ Cloud-init also works across distributions. For example, you don't use apt-get i We are working with our partners to get cloud-init included and working in the images that they provide to Azure. For detailed information cloud-init support for each distribution, see [Cloud-init support for VMs in Azure](https://learn.microsoft.com/azure/virtual-machines/linux/using-cloud-init). - -## Create cloud-init config file +### Create cloud-init config file To see cloud-init in action, create a VM that installs a LAMP stack and runs a simple Wordpress app secured with an SSL certificate. The following cloud-init configuration installs the required packages, creates the Wordpress website, then initialize and starts the website. @@ -476,6 +482,7 @@ az network private-dns zone create \ --resource-group $MY_RESOURCE_GROUP_NAME \ --name $MY_DNS_LABEL.private.mysql.database.azure.com -o JSON ``` + Results: @@ -497,7 +504,7 @@ Results: } ``` -# Create an Azure Database for MySQL - Flexible Server +## Create an Azure Database for MySQL - Flexible Server Azure Database for MySQL - Flexible Server is a managed service that you can use to run, manage, and scale highly available MySQL servers in the cloud. Create a flexible server with the [az mysql flexible-server create](https://learn.microsoft.com/cli/azure/mysql/flexible-server#az-mysql-flexible-server-create) command. A server can contain multiple databases. The following command creates a server using service defaults and variable values from your Azure CLI's local environment: @@ -526,6 +533,7 @@ az mysql flexible-server create \ --vnet $MY_VNET_NAME \ --yes -o JSON ``` + Results: @@ -544,9 +552,10 @@ Results: ``` The server created has the below attributes: -- The server name, admin username, admin password, resource group name, location are already specified in local context environment of the cloud shell, and will be created in the same location as your the resource group and the other Azure components. -- Service defaults for remaining server configurations: compute tier (Burstable), compute size/SKU (Standard_B2s), backup retention period (7 days), and MySQL version (8.0.21) -- The default connectivity method is Private access (VNet Integration) with a linked virtual network and a auto-generated subnet. + +* The server name, admin username, admin password, resource group name, location are already specified in local context environment of the cloud shell, and will be created in the same location as your the resource group and the other Azure components. +* Service defaults for remaining server configurations: compute tier (Burstable), compute size/SKU (Standard_B2s), backup retention period (7 days), and MySQL version (8.0.21) +* The default connectivity method is Private access (VNet Integration) with a linked virtual network and a auto-generated subnet. > [!NOTE] > The connectivity method cannot be changed after creating the server. For example, if you selected `Private access (VNet Integration)` during create then you cannot change to `Public access (allowed IP addresses)` after create. We highly recommend creating a server with Private access to securely access your server using VNet Integration. Learn more about Private access in the [concepts article](https://learn.microsoft.com/azure/mysql/flexible-server/concepts-networking-vnet). @@ -556,11 +565,12 @@ If you'd like to change any defaults, please refer to the Azure CLI [reference d ## Check the Azure Database for MySQL - Flexible Server status It takes a few minutes to create the Azure Database for MySQL - Flexible Server and supporting resources. + ```bash runtime="10 minute"; endtime=$(date -ud "$runtime" +%s); while [[ $(date -u +%s) -le $endtime ]]; do STATUS=$(az mysql flexible-server show -g $MY_RESOURCE_GROUP_NAME -n $MY_MYSQL_DB_NAME --query state -o tsv); echo $STATUS; if [ "$STATUS" = 'Ready' ]; then break; else sleep 10; fi; done ``` -## Configure server parameters in Azure Database for MySQL - Flexible Server +## Configure server parameters in Azure Database for MySQL - Flexible Server You can manage Azure Database for MySQL - Flexible Server configuration using server parameters. The server parameters are configured with the default and recommended value when you create the server. @@ -578,6 +588,7 @@ az mysql flexible-server parameter set \ -s $MY_MYSQL_DB_NAME \ -n require_secure_transport -v "OFF" -o JSON ``` + Results: @@ -600,12 +611,12 @@ Results: } ``` -# Create an Azure Linux Virtual Machine +## Create an Azure Linux Virtual Machine The following example creates a VM named `$MY_VM_NAME` and creates SSH keys if they do not already exist in a default key location. The command also sets `$MY_VM_USERNAME` as an administrator user name. To improve the security of Linux virtual machines in Azure, you can integrate with Azure Active Directory authentication. You can now use Azure AD as a core authentication platform and a certificate authority to SSH into a Linux VM by using Azure AD and OpenSSH certificate-based authentication. This functionality allows organizations to manage access to VMs with Azure role-based access control and Conditional Access policies. -Create a VM with the [az vm create](https://learn.microsoft.com/cli/azure/vm#az-vm-create) command. +Create a VM with the [az vm create](https://learn.microsoft.com/cli/azure/vm#az-vm-create) command. ```bash az vm create \ @@ -625,6 +636,7 @@ az vm create \ --nics $MY_VM_NIC_NAME \ --custom-data cloud-init.txt -o JSON ``` + Results: @@ -666,6 +678,7 @@ az vm extension set \ --resource-group $MY_RESOURCE_GROUP_NAME \ --vm-name $MY_VM_NAME -o JSON ``` + Results: @@ -704,6 +717,7 @@ az role assignment create \ --assignee $MY_AZURE_USER_ID \ --scope $MY_RESOURCE_GROUP_ID -o JSON ``` + Results: @@ -729,11 +743,13 @@ Results: ``` ## Export the SSH configuration for use with SSH clients that support OpenSSH + Login to Azure Linux VMs with Azure AD supports exporting the OpenSSH certificate and configuration. That means you can use any SSH clients that support OpenSSH-based certificates to sign in through Azure AD. The following example exports the configuration for all IP addresses assigned to the VM: ```bash az ssh config --file ~/.ssh/config --name $MY_VM_NAME --resource-group $MY_RESOURCE_GROUP_NAME ``` + Results: @@ -742,7 +758,7 @@ Generated SSH certificate /home/admn/.ssh/az_ssh_config/myResourceGroupfa636b-my /home/admn/.ssh/az_ssh_config/myResourceGroupfa636b-myVMNamefa636b contains sensitive information (id_rsa, id_rsa.pub, id_rsa.pub-aadcert.pub). Please delete it once you no longer need this config file. ``` -# Browse your WordPress website +## Browse your WordPress website [WordPress](https://www.wordpress.org) is an open source content management system (CMS) used by over 40% of the web to create websites, blogs, and other applications. WordPress can be run on a few different Azure services: [AKS](https://learn.microsoft.com/azure/mysql/flexible-server/tutorial-deploy-wordpress-on-aks), Virtual Machines, and App Service. For a full list of WordPress options on Azure, see [WordPress on Azure Marketplace](https://azuremarketplace.microsoft.com/marketplace/apps?page=1&search=wordpress). @@ -753,6 +769,7 @@ Validate that the application is running by visiting the application url: ```bash curl --max-time 120 "https://$FQDN" ``` + Results: From 6feac36e93fb83ccbc8d128f8c91592e3f36b98e Mon Sep 17 00:00:00 2001 From: rguptar <69279773+rguptar@users.noreply.github.com> Date: Tue, 19 Sep 2023 13:23:59 -0700 Subject: [PATCH 156/226] Fix scenarios (#40) * Register Microsoft.Insights RP * Remove role assignment creation --- scenarios/ocd/CreateAKSDeployment/README.md | 1 + scenarios/ocd/CreateLinuxVMAndSSH/README.md | 12 ------------ 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/scenarios/ocd/CreateAKSDeployment/README.md b/scenarios/ocd/CreateAKSDeployment/README.md index 6251c9c6..abd3c043 100644 --- a/scenarios/ocd/CreateAKSDeployment/README.md +++ b/scenarios/ocd/CreateAKSDeployment/README.md @@ -102,6 +102,7 @@ Results: Verify Microsoft.OperationsManagement and Microsoft.OperationalInsights providers are registered on your subscription. These are Azure resource providers required to support [Container insights](https://docs.microsoft.com/en-us/azure/azure-monitor/containers/container-insights-overview). To check the registration status, run the following commands ```bash +az provider register --namespace Microsoft.Insights az provider register --namespace Microsoft.OperationsManagement az provider register --namespace Microsoft.OperationalInsights ``` diff --git a/scenarios/ocd/CreateLinuxVMAndSSH/README.md b/scenarios/ocd/CreateLinuxVMAndSSH/README.md index 0cfe205a..d86116c7 100644 --- a/scenarios/ocd/CreateLinuxVMAndSSH/README.md +++ b/scenarios/ocd/CreateLinuxVMAndSSH/README.md @@ -96,18 +96,6 @@ export IP_ADDRESS=$(az vm show --show-details --resource-group $MY_RESOURCE_GROU ``` # SSH Into VM -The following example uses [az role assignment create](https://learn.microsoft.com/cli/azure/role/assignment#az-role-assignment-create) to assign the Virtual Machine Administrator Login role to the VM for your current Azure user. - -```bash -export MY_AZURE_USER=$(az account show --query user.name --output tsv) -export MY_RESOURCE_GROUP_ID=$(az group show --resource-group $MY_RESOURCE_GROUP_NAME --query id -o tsv) - -az role assignment create \ - --role "Virtual Machine Administrator Login" \ - --assignee $MY_AZURE_USER \ - --scope $MY_RESOURCE_GROUP_ID -``` - ## Export the SSH configuration for use with SSH clients that support OpenSSH Login to Azure Linux VMs with Azure AD supports exporting the OpenSSH certificate and configuration. That means you can use any SSH clients that support OpenSSH-based certificates to sign in through Azure AD. The following example exports the configuration for all IP addresses assigned to the VM: From bf5e75b6e01285eb61bebcc2c8ac44e5adc96349 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 19 Sep 2023 14:04:31 -0700 Subject: [PATCH 157/226] [add] command stdout and stderr to our logs. --- cmd/ie/commands/root.go | 2 ++ internal/engine/execution.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/cmd/ie/commands/root.go b/cmd/ie/commands/root.go index 4e36fe46..0ead4121 100644 --- a/cmd/ie/commands/root.go +++ b/cmd/ie/commands/root.go @@ -26,11 +26,13 @@ var rootCommand = &cobra.Command{ environment, err := cmd.Flags().GetString("environment") if err != nil { fmt.Printf("Error getting environment: %s", err) + logging.GlobalLogger.Errorf("Error getting environment: %s", err) os.Exit(1) } if !engine.IsValidEnvironment(environment) { fmt.Printf("Invalid environment: %s", environment) + logging.GlobalLogger.Errorf("Invalid environment: %s", err) os.Exit(1) } }, diff --git a/internal/engine/execution.go b/internal/engine/execution.go index d0402658..796dd87d 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -188,6 +188,8 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { go func(block parsers.CodeBlock) { output, err := shells.ExecuteBashCommand(block.Content, shells.BashCommandConfiguration{EnvironmentVariables: utils.CopyMap(env), InheritEnvironment: true, InteractiveCommand: false, WriteToHistory: true}) + logging.GlobalLogger.Infof("Command output to stdout:\n %s", output.StdOut) + logging.GlobalLogger.Infof("Command output to stderr:\n %s", output.StdErr) commandOutput = output done <- err }(block) From 6febd8b6a8612a7c0b67b792f4699c4ced046f7e Mon Sep 17 00:00:00 2001 From: rguptar <69279773+rguptar@users.noreply.github.com> Date: Tue, 19 Sep 2023 15:06:28 -0700 Subject: [PATCH 158/226] Wait for resource to reach ready state (#43) --- scenarios/ocd/CreateAKSDeployment/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scenarios/ocd/CreateAKSDeployment/README.md b/scenarios/ocd/CreateAKSDeployment/README.md index abd3c043..eb34cc33 100644 --- a/scenarios/ocd/CreateAKSDeployment/README.md +++ b/scenarios/ocd/CreateAKSDeployment/README.md @@ -173,7 +173,8 @@ helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \ --create-namespace \ --set controller.service.annotations."service\.beta\.kubernetes\.io/azure-dns-label-name"=$MY_DNS_LABEL \ --set controller.service.loadBalancerIP=$MY_STATIC_IP \ - --set controller.service.annotations."service\.beta\.kubernetes\.io/azure-load-balancer-health-probe-request-path"=/healthz + --set controller.service.annotations."service\.beta\.kubernetes\.io/azure-load-balancer-health-probe-request-path"=/healthz \ + --wait ``` ## Deploy the Application From 0164c76f84febae6ced21b78f519ee3c8c327f24 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 19 Sep 2023 19:24:51 -0700 Subject: [PATCH 159/226] [add] a functional but basic test mode, update logging, and add new targets. --- Makefile | 12 ++++++ cmd/ie/commands/execute.go | 8 +++- cmd/ie/commands/root.go | 1 + cmd/ie/commands/test.go | 16 +++++++- internal/engine/engine.go | 57 ++++++++++++++++++++++++---- internal/engine/execution.go | 8 ---- internal/engine/testing.go | 73 +++++++++++++++++++++++------------- 7 files changed, 130 insertions(+), 45 deletions(-) diff --git a/Makefile b/Makefile index f54a5949..b353c5ae 100644 --- a/Makefile +++ b/Makefile @@ -27,6 +27,18 @@ test-all: @go clean -testcache @go test -v ./... +SUBSCRIPTION_ID ?= 00000000-0000-0000-0000-000000000000 +SCENARIO ?= ./README.md +test-scenario: + @echo "Running scenario $(SCENARIO)" + $(IE_BINARY) execute $(SCENARIO) --subscription $(SCENARIO) + +test-scenarios: + @echo "Testing out the scenarios" + for dir in ./scenarios/ocd/*/; do \ + $(MAKE) test-scenario SCENARIO="$${dir}README.md"; \ + done + # ------------------------------- Run targets ---------------------------------- run-ie: build-ie diff --git a/cmd/ie/commands/execute.go b/cmd/ie/commands/execute.go index 9127967d..39708a52 100644 --- a/cmd/ie/commands/execute.go +++ b/cmd/ie/commands/execute.go @@ -59,7 +59,7 @@ var executeCommand = &cobra.Command{ cliEnvironmentVariables[keyValuePair[0]] = keyValuePair[1] } - innovationEngine := engine.NewEngine(engine.EngineConfiguration{ + innovationEngine, err := engine.NewEngine(engine.EngineConfiguration{ Verbose: verbose, DoNotDelete: doNotDelete, Subscription: subscription, @@ -68,6 +68,12 @@ var executeCommand = &cobra.Command{ WorkingDirectory: workingDirectory, }) + if err != nil { + logging.GlobalLogger.Errorf("Error creating engine: %s", err) + fmt.Printf("Error creating engine: %s", err) + os.Exit(1) + } + // Parse the markdown file and create a scenario scenario, err := engine.CreateScenarioFromMarkdown(markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}, cliEnvironmentVariables) if err != nil { diff --git a/cmd/ie/commands/root.go b/cmd/ie/commands/root.go index 0ead4121..de20e378 100644 --- a/cmd/ie/commands/root.go +++ b/cmd/ie/commands/root.go @@ -45,6 +45,7 @@ func ExecuteCLI() { if err := rootCommand.Execute(); err != nil { fmt.Println(err) + logging.GlobalLogger.Errorf("Error executing command: %s", err) os.Exit(1) } } diff --git a/cmd/ie/commands/test.go b/cmd/ie/commands/test.go index e3260c38..16c287fb 100644 --- a/cmd/ie/commands/test.go +++ b/cmd/ie/commands/test.go @@ -1,7 +1,11 @@ package commands import ( + "fmt" + "os" + "github.com/Azure/InnovationEngine/internal/engine" + "github.com/Azure/InnovationEngine/internal/logging" "github.com/spf13/cobra" ) @@ -27,16 +31,24 @@ var testCommand = &cobra.Command{ verbose, _ := cmd.Flags().GetBool("verbose") subscription, _ := cmd.Flags().GetString("subscription") - innovationEngine := engine.NewEngine(engine.EngineConfiguration{ + innovationEngine, err := engine.NewEngine(engine.EngineConfiguration{ Verbose: verbose, DoNotDelete: false, Subscription: subscription, CorrelationId: "", }) + if err != nil { + logging.GlobalLogger.Errorf("Error creating engine %s", err) + fmt.Printf("Error creating engine %s", err) + os.Exit(1) + } + scenario, err := engine.CreateScenarioFromMarkdown(markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}, nil) if err != nil { - panic(err) + logging.GlobalLogger.Errorf("Error creating scenario %s", err) + fmt.Printf("Error creating engine %s", err) + os.Exit(1) } innovationEngine.TestScenario(scenario) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 8f3051f9..d3bea07e 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -39,10 +39,40 @@ type Engine struct { } // / Create a new engine instance. -func NewEngine(configuration EngineConfiguration) *Engine { +func NewEngine(configuration EngineConfiguration) (*Engine, error) { + err := refreshAccessToken() + if err != nil { + logging.GlobalLogger.Errorf("Invalid Config: Failed to login: %s", err) + return nil, err + } + + err = setSubscription(configuration.Subscription) + if err != nil { + logging.GlobalLogger.Errorf("Invalid Config: Failed to set subscription: %s", err) + return nil, err + } + return &Engine{ Configuration: configuration, + }, nil +} + +func refreshAccessToken() error { + // Login + command := "az account get-access-token > ~/.azure/accessTokens.json" + logging.GlobalLogger.Info("Logging into the azure cli.") + output, err := shells.ExecuteBashCommand(command, shells.BashCommandConfiguration{EnvironmentVariables: map[string]string{}, InteractiveCommand: true, WriteToHistory: false, InheritEnvironment: false}) + + logging.GlobalLogger.Debugf("Login stdout: %s", output.StdOut) + logging.GlobalLogger.Debugf("Login stderr: %s", output.StdErr) + + if err != nil { + logging.GlobalLogger.Errorf("Failed to login %s", err) + return err } + + logging.GlobalLogger.Infof("Login successful.") + return nil } func setSubscription(subscription string) error { @@ -75,13 +105,18 @@ func setWorkingDirectory(directory string) error { return nil } -// Executes a deployment scenario. -func (e *Engine) ExecuteScenario(scenario *Scenario) error { - err := setSubscription(e.Configuration.Subscription) - if err != nil { - return err +// If the correlation ID is set, we need to set the AZURE_HTTP_USER_AGENT +// environment variable so that the Azure CLI will send the correlation ID +// with Azure Resource Manager requests. +func setCorrelationId(correlationId string, env map[string]string) { + if correlationId != "" { + env["AZURE_HTTP_USER_AGENT"] = fmt.Sprintf("innovation-engine-%s") + logging.GlobalLogger.Info("Resource tracking enabled. Tracking ID: " + env["AZURE_HTTP_USER_AGENT"]) } +} +// Executes a deployment scenario. +func (e *Engine) ExecuteScenario(scenario *Scenario) error { // Store the current directory so we can restore it later originalDirectory, err := os.Getwd() if err != nil { @@ -90,6 +125,7 @@ func (e *Engine) ExecuteScenario(scenario *Scenario) error { } setWorkingDirectory(e.Configuration.WorkingDirectory) + setCorrelationId(e.Configuration.CorrelationId, scenario.Environment) // Execute the steps fmt.Println(scenarioTitleStyle.Render(scenario.Name)) @@ -103,12 +139,19 @@ func (e *Engine) ExecuteScenario(scenario *Scenario) error { // Validates a deployment scenario. func (e *Engine) TestScenario(scenario *Scenario) error { - err := setSubscription(e.Configuration.Subscription) + // Store the current directory so we can restore it later + originalDirectory, err := os.Getwd() if err != nil { + logging.GlobalLogger.Error("Failed to get current directory", err) return err } + setWorkingDirectory(e.Configuration.WorkingDirectory) + setCorrelationId(e.Configuration.CorrelationId, scenario.Environment) + fmt.Println(scenarioTitleStyle.Render(scenario.Name)) e.TestSteps(scenario.Steps, utils.CopyMap(scenario.Environment)) + + setWorkingDirectory(originalDirectory) return nil } diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 796dd87d..72df39c7 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -129,14 +129,6 @@ func findAllDeployedResourceURIs(resourceGroup string) []string { // Executes the steps from a scenario and renders the output to the terminal. func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { - // If the correlation ID is set, we need to set the AZURE_HTTP_USER_AGENT - // environment variable so that the Azure CLI will send the correlation ID - // with Azure Resource Manager requests. - if e.Configuration.CorrelationId != "" { - env["AZURE_HTTP_USER_AGENT"] = fmt.Sprintf("innovation-engine-%s", e.Configuration.CorrelationId) - logging.GlobalLogger.Info("Resource tracking enabled. Tracking ID: " + env["AZURE_HTTP_USER_AGENT"]) - } - var resourceGroupName string var ocdStatus = ocd.NewOneClickDeploymentStatus() diff --git a/internal/engine/testing.go b/internal/engine/testing.go index 13a0b1f0..3da4aa80 100644 --- a/internal/engine/testing.go +++ b/internal/engine/testing.go @@ -2,7 +2,6 @@ package engine import ( "fmt" - "strings" "time" "github.com/Azure/InnovationEngine/internal/logging" @@ -12,28 +11,26 @@ import ( ) func (e *Engine) TestSteps(steps []Step, env map[string]string) { - for stepNumber, step := range steps { - fmt.Printf("%d. %s\n", stepNumber+1, step.Name) - for _, block := range step.CodeBlocks { - // Render the codeblock. - indentedBlock := indentMultiLineCommand(block.Content, 4) - fmt.Print(" " + indentedBlock) - - // Grab the number of lines it contains & set the cursor to the - // beginning of the block. - lines := strings.Count(block.Content, "\n") - fmt.Printf("\033[%dA", lines) + var resourceGroupName string + stepsToExecute := filterDeletionCommands(steps, true) - // Render the spinner and hide the cursor. - fmt.Print(spinnerStyle.Render(" "+string(spinnerFrames[0])) + " ") - fmt.Print("\033[?25l") +testRunner: + for stepNumber, step := range stepsToExecute { + stepTitle := fmt.Sprintf(" %d. %s\n", stepNumber+1, step.Name) + fmt.Println(stepTitleStyle.Render(stepTitle)) + moveCursorPositionUp(1) + hideCursor() + for _, block := range step.CodeBlocks { // execute the command as a goroutine to allow for the spinner to be // rendered while the command is executing. done := make(chan error) var commandOutput shells.CommandOutput go func(block parsers.CodeBlock) { + logging.GlobalLogger.Infof("Executing command: %s", block.Content) output, err := shells.ExecuteBashCommand(block.Content, shells.BashCommandConfiguration{EnvironmentVariables: utils.CopyMap(env), InheritEnvironment: true, InteractiveCommand: false, WriteToHistory: true}) + logging.GlobalLogger.Infof("Command stdout: %s", output.StdOut) + logging.GlobalLogger.Infof("Command stderr: %s", output.StdErr) commandOutput = output done <- err }(block) @@ -46,9 +43,7 @@ func (e *Engine) TestSteps(steps []Step, env map[string]string) { for { select { case err = <-done: - // Show the cursor, check the result of the command, and display the - // final status. - fmt.Print("\033[?25h") + showCursor() if err == nil { actualOutput := commandOutput.StdOut @@ -60,21 +55,30 @@ func (e *Engine) TestSteps(steps []Step, env map[string]string) { if err != nil { logging.GlobalLogger.Errorf("Error comparing command outputs: %s", err.Error()) - fmt.Printf("\r %s \n", errorStyle.Render("✗")) - moveCursorPositionDown(lines) - fmt.Printf(" %s\n", errorMessageStyle.Render(err.Error())) + fmt.Print(errorStyle.Render("Error when comparing the command outputs: %s\n", err.Error())) + } + // Extract the resource group name from the command output if + // it's not already set. + if resourceGroupName == "" && azCommand.MatchString(block.Content) { + tmpResourceGroup := findResourceGroupName(commandOutput.StdOut) + if tmpResourceGroup != "" { + logging.GlobalLogger.Infof("Found resource group: %s", tmpResourceGroup) + resourceGroupName = tmpResourceGroup + } } fmt.Printf("\r %s \n", checkStyle.Render("✔")) - fmt.Printf("\033[%dB\n", lines) - if e.Configuration.Verbose { - fmt.Printf(" %s\n", verboseStyle.Render(commandOutput.StdOut)) - } + moveCursorPositionDown(1) } else { + fmt.Printf("\r %s \n", errorStyle.Render("✗")) - fmt.Printf("\033[%dB", lines) - fmt.Printf(" %s\n", errorMessageStyle.Render(err.Error())) + moveCursorPositionDown(1) + fmt.Printf(" %s\n", errorStyle.Render("Error executing command: %s\n", err.Error())) + + logging.GlobalLogger.Errorf("Error executing command: %s", err.Error()) + + break testRunner } break loop @@ -86,5 +90,20 @@ func (e *Engine) TestSteps(steps []Step, env map[string]string) { } } } + + // If the resource group name was set, delete it. + if resourceGroupName != "" { + fmt.Printf("\n") + fmt.Printf("Deleting resource group: %s\n", resourceGroupName) + command := fmt.Sprintf("az group delete --name %s --yes", resourceGroupName) + output, err := shells.ExecuteBashCommand(command, shells.BashCommandConfiguration{EnvironmentVariables: utils.CopyMap(env), InheritEnvironment: true, InteractiveCommand: false, WriteToHistory: true}) + if err != nil { + fmt.Print(errorStyle.Render("Error deleting resource group: %s\n", err.Error())) + logging.GlobalLogger.Errorf("Error deleting resource group: %s", err.Error()) + } else { + fmt.Print(output.StdOut) + } + } + shells.ResetStoredEnvironmentVariables() } From 1ce60b8af3e8915436d89ac624e6ddec8e9d764a Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 19 Sep 2023 19:26:01 -0700 Subject: [PATCH 160/226] [fix] correlationId not being set. --- internal/engine/engine.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index d3bea07e..da2a9be1 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -110,7 +110,7 @@ func setWorkingDirectory(directory string) error { // with Azure Resource Manager requests. func setCorrelationId(correlationId string, env map[string]string) { if correlationId != "" { - env["AZURE_HTTP_USER_AGENT"] = fmt.Sprintf("innovation-engine-%s") + env["AZURE_HTTP_USER_AGENT"] = fmt.Sprintf("innovation-engine-%s", correlationId) logging.GlobalLogger.Info("Resource tracking enabled. Tracking ID: " + env["AZURE_HTTP_USER_AGENT"]) } } From 9e0f21d859c68d47c7630765f05cfe81028dd87d Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 19 Sep 2023 19:56:16 -0700 Subject: [PATCH 161/226] [update] the ssh command and add a unit test to test our regex out. --- internal/engine/execution.go | 3 ++- internal/engine/execution_test.go | 42 +++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 internal/engine/execution_test.go diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 72df39c7..d8776b9b 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -22,7 +22,8 @@ const ( ) var ( - sshCommand = regexp.MustCompile(`(^|\s)\bssh\b\s`) + // An SSH command regex where there must be a username@host somewhere present in the command. + sshCommand = regexp.MustCompile(`(^|\s)\bssh\b\s+([^\s]+(\s+|$))+((?P[a-zA-Z0-9_-]+|\$[A-Z_0-9]+)@(?P[a-zA-Z0-9.-]+|\$[A-Z_0-9]+))`) // Az cli command regex azCommand = regexp.MustCompile(`az\s+([a-z]+)\s+([a-z]+)`) diff --git a/internal/engine/execution_test.go b/internal/engine/execution_test.go new file mode 100644 index 00000000..b91627ed --- /dev/null +++ b/internal/engine/execution_test.go @@ -0,0 +1,42 @@ +package engine + +import ( + "testing" +) + +func TestRegex(t *testing.T) { + + t.Run("Test ssh command regex", func(t *testing.T) { + testCases := []string{ + "Run ssh -i key.pem username@host to connect", + "ssh -p 22 -L 8080:localhost:8080 username@host", + "ssh -Y username@host", + "Use ssh to connect", + "sshusername@host is not correct", + " ssh username@domain.com", + "Invalid ssh username@@domain.com", + "ssh -o StrictHostKeyChecking=no $MY_USERNAME@$IP_ADDRESS", + } + + testResults := []bool{ + true, + true, + true, + false, + false, + false, + false, + true, + } + + for index, testCase := range testCases { + match := sshCommand.FindString(testCase) + if match == "" && testResults[index] { + t.Errorf("Expected match not found: %s\n", testCase) + } else if match != "" && !testResults[index] { + t.Errorf("Unexpected match found: %s\n", testCase) + } + } + }) + +} From 13cf701f1c1f2771f79bc19ae5db4982d97d9a9a Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 19 Sep 2023 19:59:06 -0700 Subject: [PATCH 162/226] [remove] access token/login auth from engine initialization for now. --- internal/engine/engine.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index da2a9be1..7687915a 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -40,13 +40,14 @@ type Engine struct { // / Create a new engine instance. func NewEngine(configuration EngineConfiguration) (*Engine, error) { - err := refreshAccessToken() - if err != nil { - logging.GlobalLogger.Errorf("Invalid Config: Failed to login: %s", err) - return nil, err - } - - err = setSubscription(configuration.Subscription) + // Temporarily disable until login code worked out. + // err := refreshAccessToken() + // if err != nil { + // logging.GlobalLogger.Errorf("Invalid Config: Failed to login: %s", err) + // return nil, err + // } + + err := setSubscription(configuration.Subscription) if err != nil { logging.GlobalLogger.Errorf("Invalid Config: Failed to set subscription: %s", err) return nil, err From fdbd2193b8e324a47d89e612b6e982fdab8cfe3f Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 19 Sep 2023 20:33:38 -0700 Subject: [PATCH 163/226] [update] scenario to send a yes into the az ssh config command on cloud shell. --- scenarios/ocd/CreateLinuxVMAndSSH/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenarios/ocd/CreateLinuxVMAndSSH/README.md b/scenarios/ocd/CreateLinuxVMAndSSH/README.md index d86116c7..c49ccac4 100644 --- a/scenarios/ocd/CreateLinuxVMAndSSH/README.md +++ b/scenarios/ocd/CreateLinuxVMAndSSH/README.md @@ -100,7 +100,7 @@ export IP_ADDRESS=$(az vm show --show-details --resource-group $MY_RESOURCE_GROU Login to Azure Linux VMs with Azure AD supports exporting the OpenSSH certificate and configuration. That means you can use any SSH clients that support OpenSSH-based certificates to sign in through Azure AD. The following example exports the configuration for all IP addresses assigned to the VM: ```bash -az ssh config --file ~/.ssh/config --name $MY_VM_NAME --resource-group $MY_RESOURCE_GROUP_NAME +yes | az ssh config --file ~/.ssh/config --name $MY_VM_NAME --resource-group $MY_RESOURCE_GROUP_NAME ``` You can now SSH into the VM by running the output of the following command in your ssh client of choice From 8ffe4a8ee49611470632b79c2ab79e0cddf0e9ed Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 19 Sep 2023 20:33:59 -0700 Subject: [PATCH 164/226] [update] title. --- scenarios/ocd/CreateLinuxVMAndSSH/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenarios/ocd/CreateLinuxVMAndSSH/README.md b/scenarios/ocd/CreateLinuxVMAndSSH/README.md index c49ccac4..e72949c2 100644 --- a/scenarios/ocd/CreateLinuxVMAndSSH/README.md +++ b/scenarios/ocd/CreateLinuxVMAndSSH/README.md @@ -96,7 +96,7 @@ export IP_ADDRESS=$(az vm show --show-details --resource-group $MY_RESOURCE_GROU ``` # SSH Into VM -## Export the SSH configuration for use with SSH clients that support OpenSSH +## Export the SSH configuration for use with SSH clients that support OpenSSH & SSH into the VM. Login to Azure Linux VMs with Azure AD supports exporting the OpenSSH certificate and configuration. That means you can use any SSH clients that support OpenSSH-based certificates to sign in through Azure AD. The following example exports the configuration for all IP addresses assigned to the VM: ```bash From 15e12fecc127dc5e6c24caa1a161e150ef16e6db Mon Sep 17 00:00:00 2001 From: vmarcella Date: Wed, 20 Sep 2023 10:17:07 -0700 Subject: [PATCH 165/226] [update] ssh scenario to temporarily not configure the ssh config. --- scenarios/ocd/CreateLinuxVMAndSSH/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scenarios/ocd/CreateLinuxVMAndSSH/README.md b/scenarios/ocd/CreateLinuxVMAndSSH/README.md index e72949c2..a35aa1b6 100644 --- a/scenarios/ocd/CreateLinuxVMAndSSH/README.md +++ b/scenarios/ocd/CreateLinuxVMAndSSH/README.md @@ -99,9 +99,10 @@ export IP_ADDRESS=$(az vm show --show-details --resource-group $MY_RESOURCE_GROU ## Export the SSH configuration for use with SSH clients that support OpenSSH & SSH into the VM. Login to Azure Linux VMs with Azure AD supports exporting the OpenSSH certificate and configuration. That means you can use any SSH clients that support OpenSSH-based certificates to sign in through Azure AD. The following example exports the configuration for all IP addresses assigned to the VM: -```bash + You can now SSH into the VM by running the output of the following command in your ssh client of choice From 9ae4977072f1ab2f399672932e8afd66af1d9ab1 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Wed, 20 Sep 2023 12:27:36 -0700 Subject: [PATCH 166/226] [add] az ssh config command back. --- scenarios/ocd/CreateLinuxVMAndSSH/README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scenarios/ocd/CreateLinuxVMAndSSH/README.md b/scenarios/ocd/CreateLinuxVMAndSSH/README.md index a35aa1b6..e72949c2 100644 --- a/scenarios/ocd/CreateLinuxVMAndSSH/README.md +++ b/scenarios/ocd/CreateLinuxVMAndSSH/README.md @@ -99,10 +99,9 @@ export IP_ADDRESS=$(az vm show --show-details --resource-group $MY_RESOURCE_GROU ## Export the SSH configuration for use with SSH clients that support OpenSSH & SSH into the VM. Login to Azure Linux VMs with Azure AD supports exporting the OpenSSH certificate and configuration. That means you can use any SSH clients that support OpenSSH-based certificates to sign in through Azure AD. The following example exports the configuration for all IP addresses assigned to the VM: - You can now SSH into the VM by running the output of the following command in your ssh client of choice From 23dff752fabf739ec12c6082703869975085e52d Mon Sep 17 00:00:00 2001 From: Rahul Gupta Date: Wed, 20 Sep 2023 15:03:48 -0700 Subject: [PATCH 167/226] Add envsubst --- scenarios/ocd/CreateAKSDeployment/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scenarios/ocd/CreateAKSDeployment/README.md b/scenarios/ocd/CreateAKSDeployment/README.md index eb34cc33..7604acfd 100644 --- a/scenarios/ocd/CreateAKSDeployment/README.md +++ b/scenarios/ocd/CreateAKSDeployment/README.md @@ -305,6 +305,8 @@ Cert-manager provides Helm charts as a first-class method of installation on Kub The issuer we are using can be found in the `cluster-issuer-prod.yml file` ```bash + export PATH=$PATH:$(go env GOPATH)/bin + go install github.com/a8m/envsubst/cmd/envsubst@v1.4.2 envsubst < cluster-issuer-prod.yml | kubectl apply -f - ``` From d6d6dfd7898d42e5a888abc29538aaeba6723b1a Mon Sep 17 00:00:00 2001 From: Adrian Joian Date: Thu, 21 Sep 2023 12:38:49 +0200 Subject: [PATCH 168/226] Alterntives to envsubst Since not all customers have go installed an alternative to envsubst binary shipped in Ubuntu distros by the package getext-base https://packages.ubuntu.com/jammy/amd64/gettext-base/filelist is to use bash parameter substitution. We can also simply pass the variables directly to the kubectl command using --env and reworking a bit the deployment files. --- scenarios/ocd/CreateAKSDeployment/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scenarios/ocd/CreateAKSDeployment/README.md b/scenarios/ocd/CreateAKSDeployment/README.md index 7604acfd..a587c396 100644 --- a/scenarios/ocd/CreateAKSDeployment/README.md +++ b/scenarios/ocd/CreateAKSDeployment/README.md @@ -305,9 +305,8 @@ Cert-manager provides Helm charts as a first-class method of installation on Kub The issuer we are using can be found in the `cluster-issuer-prod.yml file` ```bash - export PATH=$PATH:$(go env GOPATH)/bin - go install github.com/a8m/envsubst/cmd/envsubst@v1.4.2 - envsubst < cluster-issuer-prod.yml | kubectl apply -f - + cluster_issuer_variables=$( Date: Thu, 21 Sep 2023 08:59:19 -0700 Subject: [PATCH 169/226] [remove] az ssh command again. --- scenarios/ocd/CreateLinuxVMAndSSH/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scenarios/ocd/CreateLinuxVMAndSSH/README.md b/scenarios/ocd/CreateLinuxVMAndSSH/README.md index e72949c2..8f9402f1 100644 --- a/scenarios/ocd/CreateLinuxVMAndSSH/README.md +++ b/scenarios/ocd/CreateLinuxVMAndSSH/README.md @@ -99,9 +99,11 @@ export IP_ADDRESS=$(az vm show --show-details --resource-group $MY_RESOURCE_GROU ## Export the SSH configuration for use with SSH clients that support OpenSSH & SSH into the VM. Login to Azure Linux VMs with Azure AD supports exporting the OpenSSH certificate and configuration. That means you can use any SSH clients that support OpenSSH-based certificates to sign in through Azure AD. The following example exports the configuration for all IP addresses assigned to the VM: + You can now SSH into the VM by running the output of the following command in your ssh client of choice From b4e8345dcb861d9c0a16c5e30f5045134ccfec80 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 21 Sep 2023 15:57:14 -0700 Subject: [PATCH 170/226] [add] pipeline to run scenarios automatically. --- .github/workflows/scenario-testing.yaml | 28 +++++++++++++++++++++++++ Makefile | 4 ++-- internal/engine/engine.go | 2 +- 3 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/scenario-testing.yaml diff --git a/.github/workflows/scenario-testing.yaml b/.github/workflows/scenario-testing.yaml new file mode 100644 index 00000000..14997cda --- /dev/null +++ b/.github/workflows/scenario-testing.yaml @@ -0,0 +1,28 @@ +name: scenario-testing + +on: + schedule: + - cron: "0 */2 * * *" + push: + branches: + - main + - vmarcella/port-to-go + +jobs: + build-test-release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Build all targets. + run: | + make build-all + + - name: Sign into Azure. + uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + + - name: Run all one click deployment scenarios. + run: | + make test-scenarios SUBSCRIPTION=${{ secrets.AZURE_SUBSCRIPTION }} + diff --git a/Makefile b/Makefile index b353c5ae..ce89b7d0 100644 --- a/Makefile +++ b/Makefile @@ -30,8 +30,8 @@ test-all: SUBSCRIPTION_ID ?= 00000000-0000-0000-0000-000000000000 SCENARIO ?= ./README.md test-scenario: - @echo "Running scenario $(SCENARIO)" - $(IE_BINARY) execute $(SCENARIO) --subscription $(SCENARIO) + @echo "Running scenario $(SCENARIO)" + $(IE_BINARY) test $(SCENARIO) --subscription $(SCENARIO) test-scenarios: @echo "Testing out the scenarios" diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 7687915a..77f2defa 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -82,7 +82,7 @@ func setSubscription(subscription string) error { _, err := shells.ExecuteBashCommand(command, shells.BashCommandConfiguration{EnvironmentVariables: map[string]string{}, InteractiveCommand: false, WriteToHistory: false, InheritEnvironment: false}) if err != nil { - logging.GlobalLogger.Error("Failed to set subscription", err) + logging.GlobalLogger.Errorf("Failed to set subscription: %s", err) return err } From bb52ca1db9d321ff8a4d5fd9659a388efe772a4c Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 21 Sep 2023 15:59:22 -0700 Subject: [PATCH 171/226] [update] name of the stage. --- .github/workflows/scenario-testing.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/scenario-testing.yaml b/.github/workflows/scenario-testing.yaml index 14997cda..d0b92920 100644 --- a/.github/workflows/scenario-testing.yaml +++ b/.github/workflows/scenario-testing.yaml @@ -9,7 +9,7 @@ on: - vmarcella/port-to-go jobs: - build-test-release: + test-ocd-scenarios: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 From 331dea803e2f3dba38461a8f459b5cdd64d2c804 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 21 Sep 2023 16:04:42 -0700 Subject: [PATCH 172/226] [update] pipeline. --- .github/workflows/scenario-testing.yaml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/scenario-testing.yaml b/.github/workflows/scenario-testing.yaml index d0b92920..207eb92c 100644 --- a/.github/workflows/scenario-testing.yaml +++ b/.github/workflows/scenario-testing.yaml @@ -17,12 +17,14 @@ jobs: run: | make build-all - - name: Sign into Azure. + - name: Sign into Azure uses: azure/login@v1 with: creds: ${{ secrets.AZURE_CREDENTIALS }} - name: Run all one click deployment scenarios. - run: | - make test-scenarios SUBSCRIPTION=${{ secrets.AZURE_SUBSCRIPTION }} + uses: azure/CLI@v1 + with: + inlineScript: | + make test-scenarios SUBSCRIPTION=${{ secrets.AZURE_SUBSCRIPTION }} From 46690a107a8b8305e945b72e6503fefc35f84513 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 21 Sep 2023 16:07:43 -0700 Subject: [PATCH 173/226] [update] subscription ID. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index ce89b7d0..559d2446 100644 --- a/Makefile +++ b/Makefile @@ -31,7 +31,7 @@ SUBSCRIPTION_ID ?= 00000000-0000-0000-0000-000000000000 SCENARIO ?= ./README.md test-scenario: @echo "Running scenario $(SCENARIO)" - $(IE_BINARY) test $(SCENARIO) --subscription $(SCENARIO) + $(IE_BINARY) test $(SCENARIO) --subscription $(SUBSCRIPTION_ID) test-scenarios: @echo "Testing out the scenarios" From 8ca673572a9995446548e16728499baf22397ee5 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 21 Sep 2023 16:12:39 -0700 Subject: [PATCH 174/226] [update] subscription to be passed in from top level scope. --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 559d2446..9b6a32ee 100644 --- a/Makefile +++ b/Makefile @@ -36,7 +36,7 @@ test-scenario: test-scenarios: @echo "Testing out the scenarios" for dir in ./scenarios/ocd/*/; do \ - $(MAKE) test-scenario SCENARIO="$${dir}README.md"; \ + $(MAKE) test-scenario SCENARIO="$${dir}README.md" SUBCRIPTION="$(SUBSCRIPTION_ID)"; \ done # ------------------------------- Run targets ---------------------------------- From 0853012bdfd941d1de94e3d919d86104f0dba890 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 21 Sep 2023 16:15:22 -0700 Subject: [PATCH 175/226] [fix] subscription. --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 9b6a32ee..a192c177 100644 --- a/Makefile +++ b/Makefile @@ -27,16 +27,16 @@ test-all: @go clean -testcache @go test -v ./... -SUBSCRIPTION_ID ?= 00000000-0000-0000-0000-000000000000 +SUBSCRIPTION ?= 00000000-0000-0000-0000-000000000000 SCENARIO ?= ./README.md test-scenario: @echo "Running scenario $(SCENARIO)" - $(IE_BINARY) test $(SCENARIO) --subscription $(SUBSCRIPTION_ID) + $(IE_BINARY) test $(SCENARIO) --subscription $(SUBSCRIPTION) test-scenarios: @echo "Testing out the scenarios" for dir in ./scenarios/ocd/*/; do \ - $(MAKE) test-scenario SCENARIO="$${dir}README.md" SUBCRIPTION="$(SUBSCRIPTION_ID)"; \ + $(MAKE) test-scenario SCENARIO="$${dir}README.md" SUBCRIPTION="$(SUBSCRIPTION)"; \ done # ------------------------------- Run targets ---------------------------------- From 02a35c265a23352a92f76348a2a5172e15d64305 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 21 Sep 2023 16:25:51 -0700 Subject: [PATCH 176/226] [update] pipeline to print out the ie log file. --- .github/workflows/scenario-testing.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/scenario-testing.yaml b/.github/workflows/scenario-testing.yaml index 207eb92c..e25c4585 100644 --- a/.github/workflows/scenario-testing.yaml +++ b/.github/workflows/scenario-testing.yaml @@ -6,6 +6,7 @@ on: push: branches: - main + - dev - vmarcella/port-to-go jobs: @@ -28,3 +29,8 @@ jobs: inlineScript: | make test-scenarios SUBSCRIPTION=${{ secrets.AZURE_SUBSCRIPTION }} + - name: Display ie.log file + run: | + cat ie.log + + From 16ec206126ae48261d2184b9d65e4f42605ac0c6 Mon Sep 17 00:00:00 2001 From: Bruna Moreira Bruno <79637926+brmoreir@users.noreply.github.com> Date: Sat, 23 Sep 2023 01:36:23 +0800 Subject: [PATCH 177/226] ReadMe for LinuxSecureWebServer (#24) * Add README.md * Fixing Markdown Syntax Errors. * Adding VM Status Check * Making changes to README.md. Deployed AKV extension to manage self-signed certificates. Created system-managed identity and allowed it to access AKV. * Update README.md * Adjusting intro for InnovationEngine * Updating variable declaration * Updating script to match ie format --------- Co-authored-by: Adrian Joian <6505576+naioja@users.noreply.github.com> Co-authored-by: Adrian Joian --- .../CreateLinuxVMSecureWebServer/README.md | 811 ++++++++++++++++-- 1 file changed, 733 insertions(+), 78 deletions(-) diff --git a/scenarios/ocd/CreateLinuxVMSecureWebServer/README.md b/scenarios/ocd/CreateLinuxVMSecureWebServer/README.md index 8f09b2ad..8677db83 100644 --- a/scenarios/ocd/CreateLinuxVMSecureWebServer/README.md +++ b/scenarios/ocd/CreateLinuxVMSecureWebServer/README.md @@ -1,170 +1,825 @@ # Intro to Create a NGINX Webserver Secured via HTTPS -Welcome to this tutorial where we will create a VM. This tutorial assumes you are logged into Azure CLI already and have selected a subscription to use with the CLI. If you have not done this already. Press b and hit ctl c to exit the program. Following that you can enter -'az login' followed by 'az account list --output table' and 'az account set --subscription "name of subscription to use"' +To secure web servers, a Transport Layer Security (TLS), previously known as Secure Sockets Layer (SSL), certificate can be used to encrypt web traffic. These TLS/SSL certificates can be stored in Azure Key Vault, and allow secure deployments of certificates to Linux virtual machines (VMs) in Azure. In this tutorial you learn how to: +> [!div class="checklist"] -If you need to install Azure CLI run the following command - curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash +> * Setup and secure Azure Networking +> * Create an Azure Key Vault +> * Generate or upload a certificate to the Key Vault +> * Create a VM and install the NGINX web server +> * Inject the certificate into the VM and configure NGINX with a TLS binding +If you choose to install and use the CLI locally, this tutorial requires that you're running the Azure CLI version 2.0.30 or later. Run `az --version` to find the version. If you need to install or upgrade, see [Install Azure CLI]( https://learn.microsoft.com//cli/azure/install-azure-cli ). -Assuming the pre requisites are met press space bar to proceed +## Variable Declaration -## Create a resource Group -The first thing we need to do is create a resource group. You can do this by running the following command +List of all the environment variables you'll need to execute this tutorial: -'az group create --name $RESOURCE_GROUP_NAME --location $RESOURCE_LOCATION' +```bash +export NETWORK_PREFIX="$(($RANDOM % 254 + 1))" +export RANDOM_ID="$(openssl rand -hex 3)" +export MY_RESOURCE_GROUP_NAME="myResourceGroup$RANDOM_ID" +export MY_KEY_VAULT="mykeyvault$RANDOM_ID" +export MY_CERT_NAME="nginxcert$RANDOM_ID" +export REGION="eastus" +export MY_VM_NAME="myVMName$RANDOM_ID" +export MY_VM_ID_NAME="myVMIDName$RANDOM_ID" +export MY_VM_IMAGE='Ubuntu2204' +export MY_VM_USERNAME="azureuser" +export MY_VM_SIZE='Standard_DS2_v2' +export MY_VNET_NAME="myVNet$RANDOM_ID" +export MY_VM_NIC_NAME="myVMNicName$RANDOM_ID" +export MY_NSG_SSH_RULE="Allow-Access$RANDOM_ID" +export MY_VM_NIC_NAME="myVMNicName$RANDOM_ID" +export MY_VNET_PREFIX="10.$NETWORK_PREFIX.0.0/16" +export MY_SN_NAME="mySN$RANDOM_ID" +export MY_SN_PREFIX="10.$NETWORK_PREFIX.0.0/24" +export MY_PUBLIC_IP_NAME="myPublicIP$RANDOM_ID" +export MY_DNS_LABEL="mydnslabel$RANDOM_ID" +export MY_NSG_NAME="myNSGName$RANDOM_ID" +export FQDN="${MY_DNS_LABEL}.${REGION}.cloudapp.azure.com" +``` + +## Create a Resource Group + +Before you can create a secure Linux VM, create a resource group with az group create. The following example creates a resource group equal to the contents of the variable *MY_RESOURCE_GROUP_NAME* in the location specified by the variable contents *REGION*: ```bash -az group create --name $RESOURCE_GROUP_NAME --location $RESOURCE_LOCATION +az group create \ + --name $MY_RESOURCE_GROUP_NAME \ + --location $REGION -o JSON ``` Results: -``` + + +```JSON { - "id": "/subscriptions/8c487e6a-8bbb-42bb-81e6-3c122d1bb1c7/resourceGroups/$RESOURCE_GROUP_NAME", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroupb1404f", "location": "eastus", "managedBy": null, - "name": "$RESOURCE_GROUP_NAME", + "name": "myResourceGroupb1404f", "properties": { "provisioningState": "Succeeded" }, "tags": null, "type": "Microsoft.Resources/resourceGroups" } - ``` -## Create a Virtual Machine (VM) -You can do this by running the following command: +## Set up VM Network -'az vm create --resource-group $RESOURCE_GROUP_NAME --name $VM_NAME --image $VM_IMAGE --admin-username $VM_ADMIN_USERNAME --generate-ssh-keys' +Use az network vnet create to create a virtual network named *$MY_VNET_NAME* with a subnet named *$MY_SN_NAME*in the *$MY_RESOURCE_GROUP_NAME*resource group. ```bash -az vm create --resource-group $RESOURCE_GROUP_NAME --name $VM_NAME --image $VM_IMAGE --admin-username $VM_ADMIN_USERNAME --generate-ssh-keys +az network vnet create \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --name $MY_VNET_NAME \ + --location $REGION \ + --address-prefix $MY_VNET_PREFIX \ + --subnet-name $MY_SN_NAME \ + --subnet-prefix $MY_SN_PREFIX -o JSON ``` Results: -``` + +```JSON { - "fqdns": "", - "id": "/subscriptions//resourceGroups/$RESOURCE_GROUP_NAME2/providers/Microsoft.Compute/virtualMachines/$VM_NAME", - "location": "eastus", - "macAddress": "00-0D-3A-23-9A-49", - "powerState": "VM running", - "privateIpAddress": "10.0.0.4", - "publicIpAddress": "52.174.34.95", - "resourceGroup": "$RESOURCE_GROUP_NAME" + "newVNet": { + "addressSpace": { + "addressPrefixes": [ + "10.168.0.0/16" + ] + }, + "bgpCommunities": null, + "ddosProtectionPlan": null, + "dhcpOptions": { + "dnsServers": [] + }, + "enableDdosProtection": false, + "enableVmProtection": null, + "encryption": null, + "extendedLocation": null, + "flowTimeoutInMinutes": null, + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroupb1404f/providers/Microsoft.Network/virtualNetworks/myVNetb1404f", + "ipAllocations": null, + "location": "eastus", + "name": "myVNetb1404f", + "provisioningState": "Succeeded", + "resourceGroup": "myResourceGroupb1404f", + "subnets": [ + { + "addressPrefix": "10.168.0.0/24", + "addressPrefixes": null, + "applicationGatewayIpConfigurations": null, + "delegations": [], + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroupb1404f/providers/Microsoft.Network/virtualNetworks/myVNetb1404f/subnets/mySNb1404f", + "ipAllocations": null, + "ipConfigurationProfiles": null, + "ipConfigurations": null, + "name": "mySNb1404f", + "natGateway": null, + "networkSecurityGroup": null, + "privateEndpointNetworkPolicies": "Disabled", + "privateEndpoints": null, + "privateLinkServiceNetworkPolicies": "Enabled", + "provisioningState": "Succeeded", + "purpose": null, + "resourceGroup": "myResourceGroupb1404f", + "resourceNavigationLinks": null, + "routeTable": null, + "serviceAssociationLinks": null, + "serviceEndpointPolicies": null, + "serviceEndpoints": null, + "type": "Microsoft.Network/virtualNetworks/subnets" + } + ], + "tags": {}, + "type": "Microsoft.Network/virtualNetworks", + "virtualNetworkPeerings": [] + } } ``` -Congrats you created a VM! Next we will open port 80 and install NGINX. +Use az network public-ip create to create a standard zone-redundant public IPv4 address named *$MY_PUBLIC_IP_NAME* in *$MY_RESOURCE_GROUP_NAME*. -## Store IP Address as environment Variable -The following command will store the IP Address as a environment variable that we can access later to do SSH +```bash +az network public-ip create \ + --name $MY_PUBLIC_IP_NAME \ + --location $REGION \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --dns-name $MY_DNS_LABEL \ + --sku Standard \ + --allocation-method static \ + --version IPv4 \ + --zone 1 2 3 -o JSON +``` -'export IP_ADDRESS=$(az vm show --show-details --resource-group $RESOURCE_GROUP_NAME --name $VM_NAME --query publicIps --output tsv)' +Results: -```bash -export IP_ADDRESS=$(az vm show --show-details --resource-group $RESOURCE_GROUP_NAME --name $VM_NAME --query publicIps --output tsv) + +```JSON +{ + "publicIp": { + "ddosSettings": null, + "deleteOption": null, + "dnsSettings": { + "domainNameLabel": "mydnslabelb1404f", + "fqdn": "mydnslabelb1404f.eastus.cloudapp.azure.com", + "reverseFqdn": null + }, + "extendedLocation": null, + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroupb1404f/providers/Microsoft.Network/publicIPAddresses/myPublicIPb1404f", + "idleTimeoutInMinutes": 4, + "ipAddress": "20.88.178.210", + "ipConfiguration": null, + "ipTags": [], + "linkedPublicIpAddress": null, + "location": "eastus", + "migrationPhase": null, + "name": "myPublicIPb1404f", + "natGateway": null, + "provisioningState": "Succeeded", + "publicIpAddressVersion": "IPv4", + "publicIpAllocationMethod": "Static", + "publicIpPrefix": null, + "resourceGroup": "myResourceGroupb1404f", + "servicePublicIpAddress": null, + "sku": { + "name": "Standard", + "tier": "Regional" + }, + "tags": null, + "type": "Microsoft.Network/publicIPAddresses", + "zones": [ + "1", + "2", + "3" + ] + } +} ``` -## Validate IP_ADDRESS -Let's make sure the IP Address is correctly stored +Security rules in network security groups enable you to filter the type of network traffic that can flow in and out of virtual network subnets and network interfaces. To learn more about network security groups, see [Network security group overview](https://learn.microsoft.com/azure/virtual-network/network-security-groups-overview). ```bash -echo $IP_ADDRESS +az network nsg create \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --name $MY_NSG_NAME \ + --location $REGION -o JSON ``` -# Open Port 80 to allow web traffic -Open port 80 with the following command: +Results: + + +```JSON +{ + "NewNSG": { + "defaultSecurityRules": [ + { + "access": "Allow", + "description": "Allow inbound traffic from all VMs in VNET", + "destinationAddressPrefix": "VirtualNetwork", + "destinationAddressPrefixes": [], + "destinationPortRange": "*", + "destinationPortRanges": [], + "direction": "Inbound", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroupb1404f/providers/Microsoft.Network/networkSecurityGroups/myNSGNameb1404f/defaultSecurityRules/AllowVnetInBound", + "name": "AllowVnetInBound", + "priority": 65000, + "protocol": "*", + "provisioningState": "Succeeded", + "resourceGroup": "myResourceGroupb1404f", + "sourceAddressPrefix": "VirtualNetwork", + "sourceAddressPrefixes": [], + "sourcePortRange": "*", + "sourcePortRanges": [], + "type": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules" + }, + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroupb1404f/providers/Microsoft.Network/networkSecurityGroups/myNSGNameb1404f", + "location": "eastus", + "name": "myNSGNameb1404f", + "provisioningState": "Succeeded", + "resourceGroup": "myResourceGroupb1404f", + "securityRules": [], + "type": "Microsoft.Network/networkSecurityGroups" + } +} +``` -'az vm open-port --port 80,443 --resource-group $RESOURCE_GROUP_NAME --name $VM_NAME' +Open ports 22 (SSH), 80 (HTTP) and 443 (HTTPS) to allow SSH and Web traffic ```bash -az vm open-port --port 80,443 --resource-group $RESOURCE_GROUP_NAME --name $VM_NAME +az network nsg rule create \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --nsg-name $MY_NSG_NAME \ + --name $MY_NSG_SSH_RULE \ + --access Allow \ + --protocol Tcp \ + --direction Inbound \ + --priority 100 \ + --source-address-prefix '*' \ + --source-port-range '*' \ + --destination-address-prefix '*' \ + --destination-port-range 22 80 443 -o JSON ``` -## Validate SSH Connection -To validate you are connected to your VM you can run the following command: +Results: -'ssh -o StrictHostKeyChecking=no $VM_ADMIN_USERNAME@$IP_ADDRESS hostname' -Note - For the following commands we place ssh before hand as we must connect to the VM each time to run commands + +```JSON +{ + "access": "Allow", + "description": null, + "destinationAddressPrefix": "*", + "destinationAddressPrefixes": [], + "destinationApplicationSecurityGroups": null, + "destinationPortRange": null, + "destinationPortRanges": [ + "22", + "80", + "443" + ], + "direction": "Inbound", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroupb1404f/providers/Microsoft.Network/networkSecurityGroups/myNSGNameb1404f/securityRules/MY_NSG_SSH_RULE", + "name": "MY_NSG_SSH_RULE", + "priority": 100, + "protocol": "Tcp", + "provisioningState": "Succeeded", + "resourceGroup": "myResourceGroupb1404f", + "sourceAddressPrefix": "*", + "sourceAddressPrefixes": [], + "sourceApplicationSecurityGroups": null, + "sourcePortRange": "*", + "sourcePortRanges": [], + "type": "Microsoft.Network/networkSecurityGroups/securityRules" +} +``` + +And finally create the Network Interface Card (NIC): ```bash -ssh -o StrictHostKeyChecking=no $VM_ADMIN_USERNAME@$IP_ADDRESS hostname +az network nic create \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --name $MY_VM_NIC_NAME \ + --location $REGION \ + --ip-forwarding false \ + --subnet $MY_SN_NAME \ + --vnet-name $MY_VNET_NAME \ + --network-security-group $MY_NSG_NAME \ + --public-ip-address $MY_PUBLIC_IP_NAME -o JSON +``` + +Results: + + +```JSON +{ + "NewNIC": { + "auxiliaryMode": "None", + "auxiliarySku": "None", + "disableTcpStateTracking": false, + "dnsSettings": { + "appliedDnsServers": [], + "dnsServers": [] + }, + "enableAcceleratedNetworking": false, + "enableIPForwarding": false, + "hostedWorkloads": [], + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroupb1404f/providers/Microsoft.Network/networkInterfaces/myVMNicNameb1404f", + "ipConfigurations": [ + { + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroupb1404f/providers/Microsoft.Network/networkInterfaces/myVMNicNameb1404f/ipConfigurations/ipconfig1", + "name": "ipconfig1", + "primary": true, + "privateIPAddress": "10.168.0.4", + "privateIPAddressVersion": "IPv4", + "privateIPAllocationMethod": "Dynamic", + "provisioningState": "Succeeded", + "resourceGroup": "myResourceGroupb1404f", + "subnet": { + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroupb1404f/providers/Microsoft.Network/virtualNetworks/myVNetb1404f/subnets/mySNb1404f", + "resourceGroup": "myResourceGroupb1404f" + }, + "type": "Microsoft.Network/networkInterfaces/ipConfigurations" + } + ], + "location": "eastus", + "name": "myVMNicNameb1404f", + "networkSecurityGroup": { + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroupb1404f/providers/Microsoft.Network/networkSecurityGroups/myNSGNameb1404f", + "resourceGroup": "myResourceGroupb1404f" + }, + "nicType": "Standard", + "provisioningState": "Succeeded", + "resourceGroup": "myResourceGroupb1404f", + "tapConfigurations": [], + "type": "Microsoft.Network/networkInterfaces", + "vnetEncryptionSupported": false + } +} ``` -## Ensure VM is up to date -Ensure the VM is up to date by running the following command: +## Generate a certificate and store it in Azure Key Vault + +Azure Key Vault safeguards cryptographic keys and secrets, such as certificates or passwords. Key Vault helps streamline the certificate management process and enables you to maintain control of keys that access those certificates. You can create a self-signed certificate inside Key Vault, or upload an existing, trusted certificate that you already own. For this tutorial we'll create self-signed certificates inside the Key Vault and afterwards inject these certificates into a running VM. This process ensures that the most up-to-date certificates are installed on a web server during deployment. -'sudo apt update' -Note - This may take ~30 seconds to complete +The following example creates an Azure Key Vault named *$MY_KEY_VAULT* in the chosen region *$REGION* with a retention policy of 7 days. This means once a secret, key, certificate, or key vault is deleted, it will remain recoverable for a configurable period of 7 to 90 calendar days. ```bash -ssh $VM_ADMIN_USERNAME@$IP_ADDRESS sudo apt update +az keyvault create \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --name $MY_KEY_VAULT \ + --location $REGION \ + --retention-days 7\ + --enabled-for-deployment true -o JSON +``` + +Results: + + +```JSON +{ + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroupb1404f/providers/Microsoft.KeyVault/vaults/myKeyVaultb1404f", + "location": "eastus", + "name": "myKeyVaultb1404f", + "properties": { + "accessPolicies": [ + { + "applicationId": null, + "permissions": { + "certificates": [ + "all" + ], + "keys": [ + "all" + ], + "secrets": [ + "all" + ], + "storage": [ + "all" + ] + }, + "tenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + } + ], + "createMode": null, + "enablePurgeProtection": null, + "enableRbacAuthorization": null, + "enableSoftDelete": true, + "enabledForDeployment": true, + "enabledForDiskEncryption": null, + "enabledForTemplateDeployment": null, + "hsmPoolResourceId": null, + "networkAcls": null, + "privateEndpointConnections": null, + "provisioningState": "Succeeded", + "publicNetworkAccess": "Enabled", + "sku": { + "family": "A", + "name": "standard" + }, + "softDeleteRetentionInDays": 7, + "tenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "vaultUri": "https://mykeyvaultb1404f.vault.azure.net/" + }, + "resourceGroup": "myResourceGroupb1404f", + "systemData": { + "createdAt": "2023-09-18T12:25:55.208000+00:00", + "createdBy": "example@microsoft.com", + "createdByType": "User", + "lastModifiedAt": "2023-09-18T12:25:55.208000+00:00", + "lastModifiedBy": "example@microsoft.com", + "lastModifiedByType": "User" + }, + "tags": {}, + "type": "Microsoft.KeyVault/vaults" +} ``` -## Install NGINX -Run the following command to install the NGINX webserver +## Create a certificate and store in Azure key Vault -'sudo apt install nginx' -This may take a few minutes... +Now let's generate a self-signed certificate with az keyvault certificate create that uses the default certificate policy: ```bash -ssh $VM_ADMIN_USERNAME@$IP_ADDRESS sudo apt --yes --force-yes install nginx +az keyvault certificate create \ + --vault-name $MY_KEY_VAULT \ + --name $MY_CERT_NAME \ + --policy "$(az keyvault certificate get-default-policy)" -o JSON +``` + +Results: + + +```JSON +{ + "cancellationRequested": false, + "csr": "MIICr...", + "error": null, + "id": "https://mykeyvault67a7ba.vault.azure.net/certificates/nginxcert67a7ba/pending", + "issuerParameters": { + "certificateTransparency": null, + "certificateType": null, + "name": "Self" + }, + "name": "nginxcert67a7ba", + "status": "completed", + "statusDetails": null, + "target": "https://mykeyvault67a7ba.vault.azure.net/certificates/nginxcert67a7ba" +} ``` -## View Your webserver running +Finally, we need to prepare the certificate so it can be used during the VM create process. To do so we need to obtain the ID of the certificate with az keyvault secret list-versions, and convert the certificate with az vm secret format. The following example assigns the output of these commands to variables for ease of use in the next steps: ```bash -echo $IP_ADDRESS +az identity create \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --name $MY_VM_ID_NAME -o JSON ``` -Congratulations you have now created a Virtual Machine and installed a webserver! +Results: -Press 1 to end the tutorial and 2 to secure your webserver via https + +```JSON +{ + "clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourcegroups/myResourceGroupb1404f/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myVMIDNameb1404f", + "location": "eastus", + "name": "myVMIDNameb1404f", + "principalId": "e09ebfce-97f0-4aff-9abd-415ebd6f915c", + "resourceGroup": "myResourceGroupb1404f", + "tags": {}, + "tenantId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "type": "Microsoft.ManagedIdentity/userAssignedIdentities" +} +``` -1. Quit the tutorial -2. Secure your webserver via https and add a custom domain +```bash +MY_VM_PRINCIPALID=$(az identity show --resource-group $MY_RESOURCE_GROUP_NAME --name $MY_VM_ID_NAME --query principalId -o tsv) + +az keyvault set-policy \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --name $MY_KEY_VAULT \ + --object-id $MY_VM_PRINCIPALID \ + --secret-permissions get list \ + --certificate-permissions get list -o JSON +``` -## Select unique custom domain Name +Results: -When prompted to enter a value for CUSTOM_DOMAIN_NAME enter a custom domain for your webserver Note - This must be unique on Azure + +```JSON +{ + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroupb1404f/providers/Microsoft.KeyVault/vaults/myKeyVaultb1404f", + "location": "eastus", + "name": "myKeyVaultb1404f", + "properties": { + "accessPolicies": [ + { + "applicationId": null, + "objectId": "ceeb4e98-5831-4d9f-b8ba-2ee14b3cdf80", + "permissions": { + "certificates": [ + "all" + ], + "keys": [ + "all" + ], + "secrets": [ + "all" + ], + "storage": [ + "all" + ] + }, + "tenantId": "bd7153ee-d085-4a28-a928-2f0ef402f076" + }, + { + "applicationId": null, + "objectId": "e09ebfce-97f0-4aff-9abd-415ebd6f915c", + "permissions": { + "certificates": [ + "list", + "get" + ], + "keys": null, + "secrets": [ + "list", + "get" + ], + "storage": null + }, + "tenantId": "bd7153ee-d085-4a28-a928-2f0ef402f076" + } + ], + "createMode": null, + "enablePurgeProtection": null, + "enableRbacAuthorization": null, + "enableSoftDelete": true, + "enabledForDeployment": true, + "enabledForDiskEncryption": null, + "enabledForTemplateDeployment": null, + "hsmPoolResourceId": null, + "networkAcls": null, + "privateEndpointConnections": null, + "provisioningState": "Succeeded", + "publicNetworkAccess": "Enabled", + "sku": { + "family": "A", + "name": "standard" + }, + "softDeleteRetentionInDays": 7, + "tenantId": "bd7153ee-d085-4a28-a928-2f0ef402f076", + "vaultUri": "https://mykeyvaultb1404f.vault.azure.net/" + }, + "resourceGroup": "myResourceGroupb1404f", + "systemData": { + "createdAt": "2023-09-18T12:25:55.208000+00:00", + "createdBy": "ajoian@microsoft.com", + "createdByType": "User", + "lastModifiedAt": "2023-09-18T12:48:08.966000+00:00", + "lastModifiedBy": "ajoian@microsoft.com", + "lastModifiedByType": "User" + }, + "tags": {}, + "type": "Microsoft.KeyVault/vaults" +} +``` + +## Create the VM + +Now create a VM with az vm create. Use the --custom-data parameter to pass in the cloud-init config file, named *cloud-init-nginx.txt*. +Cloud-init is a widely used approach to customize a Linux VM as it boots for the first time. You can use cloud-init to install packages and write files, or to configure users and security. As cloud-init runs during the initial boot process, there are no extra steps or required agents to apply your configuration. +When you create a VM, certificates and keys are stored in the protected /var/lib/waagent/ directory. In this example, we are installing and configuring the NGINX web server. ```bash -echo $CUSTOM_DOMAIN_NAME +cat > cloud-init-nginx.txt </dev/null; echo "0 * * * * /root/convert_akv_cert.sh && service nginx reload") | crontab - + - service nginx restart +EOF ``` -install Az CLI extension for Front Door in order to add HTTPS +The following example creates a VM named *myVMName$UNIQUE_POSTFIX*: ```bash -az extension add --name front-door +MY_VM_ID=$(az identity show --resource-group $MY_RESOURCE_GROUP_NAME --name $MY_VM_ID_NAME --query id -o tsv) + +az vm create \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --name $MY_VM_NAME \ + --image $MY_VM_IMAGE \ + --admin-username $MY_VM_USERNAME \ + --generate-ssh-keys \ + --assign-identity $MY_VM_ID \ + --size $MY_VM_SIZE \ + --custom-data cloud-init-nginx.txt \ + --nics $MY_VM_NIC_NAME ``` -## Setting Up HTTPS Terminated EndPoint +Results: + + +```JSON +{ + "fqdns": "mydnslabel67a7ba.eastus.cloudapp.azure.com", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup67a7ba/providers/Microsoft.Compute/virtualMachines/myVMName67a7ba", + "identity": { + "systemAssignedIdentity": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "userAssignedIdentities": { + "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourcegroups/myResourceGroup67a7ba/providers/Microsoft.ManagedIdentity/userAssignedIdentities/myVMIDName67a7ba": { + "clientId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + "principalId": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + } + } + }, + "location": "eastus", + "macAddress": "60-45-BD-D3-B5-29", + "powerState": "VM running", + "privateIpAddress": "10.56.0.4", + "publicIpAddress": "20.231.118.239", + "resourceGroup": "myResourceGroup67a7ba", + "zones": "" +} +``` + +## Deploying AKV extension for VM $vm_name to retrieve cert $cert_name from AKV $akv_name..." -The following command will set up a custom domain secured via https. This may take a few minutes ```bash -az network front-door create --backend-address $IP_ADDRESS --name $CUSTOM_DOMAIN_NAME --resource-group $RESOURCE_GROUP_NAME --accepted-protocols Http Https --forwarding-protocol HttpOnly --protocol Http +MY_CERT_ID=$(az keyvault certificate show --vault-name $MY_KEY_VAULT --name $MY_CERT_NAME --query sid -o tsv) +MY_VM_CLIENTID=$(az identity show --resource-group $MY_RESOURCE_GROUP_NAME --name $MY_VM_ID_NAME --query clientId -o tsv) +MY_AKV_EXT_SETTINGS="{\"secretsManagementSettings\":{\"pollingIntervalInS\":\"3600\",\"requireInitialSync\":"true",\"certificateStoreLocation\":\"/etc/nginx/ssl/\",\"observedCertificates\":[\"$MY_CERT_ID\"]},\"authenticationSettings\":{\"msiClientId\":\"${MY_VM_CLIENTID}\"}}" + +az vm extension set \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --vm-name $MY_VM_NAME \ + -n "KeyVaultForLinux" \ + --publisher Microsoft.Azure.KeyVault \ + --version 2.0 \ + --enable-auto-upgrade true \ + --settings $MY_AKV_EXT_SETTINGS -o JSON +``` + +Results: + + +```JSON +{ + "autoUpgradeMinorVersion": true, + "enableAutomaticUpgrade": true, + "forceUpdateTag": null, + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup67a7ba/providers/Microsoft.Compute/virtualMachines/myVMName67a7ba/extensions/KeyVaultForLinux", + "instanceView": null, + "location": "eastus", + "name": "KeyVaultForLinux", + "protectedSettings": null, + "protectedSettingsFromKeyVault": null, + "provisioningState": "Succeeded", + "publisher": "Microsoft.Azure.KeyVault", + "resourceGroup": "myResourceGroup67a7ba", + "settings": { + "secretsManagementSettings": { + "certificateStoreLocation": "/etc/nginx/ssl", + "observedCertificates": [ + "https://mykeyvault67a7ba.vault.azure.net/secrets/nginxcert67a7ba/aac9b30a90c04fc58bc230ae15b1148f" + ], + "pollingIntervalInS": "3600" + } + }, + "suppressFailures": null, + "tags": null, + "type": "Microsoft.Compute/virtualMachines/extensions", + "typeHandlerVersion": "2.0", + "typePropertiesType": "KeyVaultForLinux" +} ``` -## See your webserver running HTTPS +## Enable Azure AD login for a Linux Virtual Machine in Azure -Run the following command to see the url of your webserver. -NOTE - It may take ~5 minutes for backends to update appropriately and for your site to be secured via https. +The following example deploys a VM and then installs the extension to enable Azure AD login for a Linux VM. VM extensions are small applications that provide post-deployment configuration and automation tasks on Azure virtual machines. ```bash -az network front-door show --name $CUSTOM_DOMAIN_NAME --resource-group $RESOURCE_GROUP_NAME --query frontendEndpoints[*].hostName --output tsv +az vm extension set \ + --publisher Microsoft.Azure.ActiveDirectory \ + --name AADSSHLoginForLinux \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --vm-name $MY_VM_NAME -o JSON +``` + +Results: + + +```JSON +{ + "autoUpgradeMinorVersion": true, + "enableAutomaticUpgrade": null, + "forceUpdateTag": null, + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroupfa636b/providers/Microsoft.Compute/virtualMachines/myVMNamefa636b/extensions/AADSSHLoginForLinux", + "instanceView": null, + "location": "eastus", + "name": "AADSSHLoginForLinux", + "protectedSettings": null, + "protectedSettingsFromKeyVault": null, + "provisioningState": "Succeeded", + "publisher": "Microsoft.Azure.ActiveDirectory", + "resourceGroup": "myResourceGroupfa636b", + "settings": null, + "suppressFailures": null, + "tags": null, + "type": "Microsoft.Compute/virtualMachines/extensions", + "typeHandlerVersion": "1.0", + "typePropertiesType": "AADSSHLoginForLinux" +} ``` -## Conclusion +## Browse your secure website + +Validate that the application is running by visiting the application url: -You have completed the tutorial! View your resources on portal.azure.com +```bash +curl --max-time 120 -k "https://$FQDN" +``` -# Next Steps +Results: -* [VM Documentation](https://learn.microsoft.com/en-us/azure/virtual-machines/) -* [Create Vm Scale Set](https://learn.microsoft.com/en-us/azure/virtual-machine-scale-sets/flexible-virtual-machine-scale-sets-cli) -* [Load Balance VMs](https://learn.microsoft.com/en-us/azure/load-balancer/quickstart-load-balancer-standard-public-cli) -* [Baclup VMs](https://learn.microsoft.com/en-us/azure/virtual-machines/backup-recovery) \ No newline at end of file + +```html + + + +Welcome to nginx! + + + +

Welcome to nginx!

+

If you see this page, the nginx web server is successfully installed and +working. Further configuration is required.

+ +

For online documentation and support please refer to +nginx.org.
+Commercial support is available at +nginx.com.

+ +

Thank you for using nginx.

+ + +``` From 181d5b2c9b28d6f8052c83ef047670689a47d34c Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 22 Sep 2023 12:40:18 -0700 Subject: [PATCH 178/226] [update] ssh config name and for the scenario to assign an identity to the vm. --- scenarios/ocd/CreateLinuxVMLAMP/README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/scenarios/ocd/CreateLinuxVMLAMP/README.md b/scenarios/ocd/CreateLinuxVMLAMP/README.md index 7ba7a175..7033bfcf 100644 --- a/scenarios/ocd/CreateLinuxVMLAMP/README.md +++ b/scenarios/ocd/CreateLinuxVMLAMP/README.md @@ -624,6 +624,7 @@ az vm create \ --resource-group $MY_RESOURCE_GROUP_NAME \ --admin-username $MY_VM_USERNAME \ --authentication-type ssh \ + --assign-identity \ --image $MY_VM_IMAGE \ --location $REGION \ --nic-delete-option Delete \ @@ -664,7 +665,10 @@ Results: It takes a few minutes to create the VM and supporting resources. The provisioningState value of Succeeded appears when the extension is successfully installed on the VM. The VM must have a running [VM agent](https://learn.microsoft.com/azure/virtual-machines/extensions/agent-linux) to install the extension. ```bash -runtime="10 minute"; endtime=$(date -ud "$runtime" +%s); while [[ $(date -u +%s) -le $endtime ]]; do STATUS=$(ssh -o StrictHostKeyChecking=no $FQDN "cloud-init status"); echo $STATUS; if [ "$STATUS" = 'status: done' ]; then break; else sleep 10; fi; done +runtime="10 minute"; +endtime=$(date -ud "$runtime" +%s); + +while [[ $(date -u +%s) -le $endtime ]]; do STATUS=$(ssh -o StrictHostKeyChecking=no $FQDN "cloud-init status"); echo $STATUS; if [ "$STATUS" = 'status: done' ]; then break; else sleep 10; fi; done ``` ## Enable Azure AD login for a Linux Virtual Machine in Azure @@ -747,7 +751,7 @@ Results: Login to Azure Linux VMs with Azure AD supports exporting the OpenSSH certificate and configuration. That means you can use any SSH clients that support OpenSSH-based certificates to sign in through Azure AD. The following example exports the configuration for all IP addresses assigned to the VM: ```bash -az ssh config --file ~/.ssh/config --name $MY_VM_NAME --resource-group $MY_RESOURCE_GROUP_NAME +az ssh config --file ~/.ssh/azure-config --name $MY_VM_NAME --resource-group $MY_RESOURCE_GROUP_NAME ``` Results: From ff798bd919322b4f208add9f91c88ba99941505d Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 22 Sep 2023 13:58:47 -0700 Subject: [PATCH 179/226] [update] LEMP scenario to assign an identity and write the azure config somewhere else. --- scenarios/ocd/CreateLinuxVMLAMP/README.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/scenarios/ocd/CreateLinuxVMLAMP/README.md b/scenarios/ocd/CreateLinuxVMLAMP/README.md index 7033bfcf..40ca2bb5 100644 --- a/scenarios/ocd/CreateLinuxVMLAMP/README.md +++ b/scenarios/ocd/CreateLinuxVMLAMP/README.md @@ -668,7 +668,15 @@ It takes a few minutes to create the VM and supporting resources. The provisioni runtime="10 minute"; endtime=$(date -ud "$runtime" +%s); -while [[ $(date -u +%s) -le $endtime ]]; do STATUS=$(ssh -o StrictHostKeyChecking=no $FQDN "cloud-init status"); echo $STATUS; if [ "$STATUS" = 'status: done' ]; then break; else sleep 10; fi; done +while [[ $(date -u +%s) -le $endtime ]]; do + STATUS=$(ssh -o StrictHostKeyChecking=no $FQDN "cloud-init status"); + echo $STATUS; + if [ "$STATUS" = 'status: done' ]; then + break; + else + sleep 10; + fi; +done ``` ## Enable Azure AD login for a Linux Virtual Machine in Azure From a0d49bb78d28ef9cbe5e724cb9df268b61f68aa8 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 22 Sep 2023 14:14:59 -0700 Subject: [PATCH 180/226] [remove] result block for the az ssh config scenario. --- scenarios/ocd/CreateLinuxVMLAMP/README.md | 8 -------- 1 file changed, 8 deletions(-) diff --git a/scenarios/ocd/CreateLinuxVMLAMP/README.md b/scenarios/ocd/CreateLinuxVMLAMP/README.md index 40ca2bb5..fb7796a1 100644 --- a/scenarios/ocd/CreateLinuxVMLAMP/README.md +++ b/scenarios/ocd/CreateLinuxVMLAMP/README.md @@ -762,14 +762,6 @@ Login to Azure Linux VMs with Azure AD supports exporting the OpenSSH certificat az ssh config --file ~/.ssh/azure-config --name $MY_VM_NAME --resource-group $MY_RESOURCE_GROUP_NAME ``` -Results: - - -```ASCII -Generated SSH certificate /home/admn/.ssh/az_ssh_config/myResourceGroupfa636b-myVMNamefa636b/id_rsa.pub-aadcert.pub is valid until 2023-09-04 12:37:25 PM in local time. -/home/admn/.ssh/az_ssh_config/myResourceGroupfa636b-myVMNamefa636b contains sensitive information (id_rsa, id_rsa.pub, id_rsa.pub-aadcert.pub). Please delete it once you no longer need this config file. -``` - ## Browse your WordPress website [WordPress](https://www.wordpress.org) is an open source content management system (CMS) used by over 40% of the web to create websites, blogs, and other applications. WordPress can be run on a few different Azure services: [AKS](https://learn.microsoft.com/azure/mysql/flexible-server/tutorial-deploy-wordpress-on-aks), Virtual Machines, and App Service. For a full list of WordPress options on Azure, see [WordPress on Azure Marketplace](https://azuremarketplace.microsoft.com/marketplace/apps?page=1&search=wordpress). From 512cf609d5a849f63b267d489341b650160764fb Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 22 Sep 2023 14:27:06 -0700 Subject: [PATCH 181/226] [fix] command output checking to set an error when the command outputs don't match. --- internal/engine/engine.go | 2 +- internal/engine/execution.go | 6 +++++- internal/utils/json_test.go | 1 + 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 internal/utils/json_test.go diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 77f2defa..7c656398 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -72,7 +72,7 @@ func refreshAccessToken() error { return err } - logging.GlobalLogger.Infof("Login successful.") + logging.GlobalLogger.Info("Login successful.") return nil } diff --git a/internal/engine/execution.go b/internal/engine/execution.go index d8776b9b..f16e3e8c 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -210,7 +210,11 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { moveCursorPositionDown(lines) fmt.Printf(" %s\n", errorMessageStyle.Render(err.Error())) fmt.Printf(" %s\n", utils.GetDifferenceBetweenStrings(block.ExpectedOutput.Content, commandOutput.StdOut)) - break loop + + ocdStatus.SetError(err) + reportOCDStatus(ocdStatus, e.Configuration.Environment) + + os.Exit(1) } fmt.Printf("\r %s \n", checkStyle.Render("✔")) diff --git a/internal/utils/json_test.go b/internal/utils/json_test.go new file mode 100644 index 00000000..d4b585bf --- /dev/null +++ b/internal/utils/json_test.go @@ -0,0 +1 @@ +package utils From 90310855862e3d0d4398ba7a716e72674d71ac1f Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 22 Sep 2023 14:29:32 -0700 Subject: [PATCH 182/226] [update] loop -> renderingLoop. --- internal/engine/execution.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index f16e3e8c..7e7f9d95 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -186,7 +186,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { commandOutput = output done <- err }(block) - loop: + renderingLoop: // While the command is executing, render the spinner. for { select { @@ -252,7 +252,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { os.Exit(1) } - break loop + break renderingLoop default: frame = (frame + 1) % len(spinnerFrames) fmt.Printf("\r %s", spinnerStyle.Render(string(spinnerFrames[frame]))) From 2b0e4186a67537309b432ced0aaca4b01d4880ad Mon Sep 17 00:00:00 2001 From: Rahul Gupta Date: Fri, 22 Sep 2023 14:35:12 -0700 Subject: [PATCH 183/226] Exclude liveness check for AKS scenario --- scenarios/ocd/CreateAKSDeployment/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/scenarios/ocd/CreateAKSDeployment/README.md b/scenarios/ocd/CreateAKSDeployment/README.md index a587c396..d5d465c6 100644 --- a/scenarios/ocd/CreateAKSDeployment/README.md +++ b/scenarios/ocd/CreateAKSDeployment/README.md @@ -323,15 +323,15 @@ Cert-manager provides Helm charts as a first-class method of installation on Kub Wait for SSL certificate to issue. The following command will query the status of the SSL certificate for 3 minutes. In rare occasions it may take up to 15 minutes for Lets Encrypt to issue a successful challenge and the ready state to be 'True' -```bash + Validate SSL certificate is True by running the follow command: -```bash + Results: @@ -347,11 +347,11 @@ Run the following command to get the HTTPS endpoint for your application: >[!Note] > It often takes 2-3 minutes for the SSL certificate to propogate and the site to be reachable via https. -```bash + Results: From c916d25a7b1a458931e4e6f2efbd67c086bffc5b Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 22 Sep 2023 14:36:25 -0700 Subject: [PATCH 184/226] [update] json response to be properly formatted. --- .../ocd/CreateLinuxVMSecureWebServer/README.md | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/scenarios/ocd/CreateLinuxVMSecureWebServer/README.md b/scenarios/ocd/CreateLinuxVMSecureWebServer/README.md index 8677db83..18309e57 100644 --- a/scenarios/ocd/CreateLinuxVMSecureWebServer/README.md +++ b/scenarios/ocd/CreateLinuxVMSecureWebServer/README.md @@ -237,14 +237,15 @@ Results: "sourcePortRange": "*", "sourcePortRanges": [], "type": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules" - }, - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroupb1404f/providers/Microsoft.Network/networkSecurityGroups/myNSGNameb1404f", - "location": "eastus", - "name": "myNSGNameb1404f", - "provisioningState": "Succeeded", - "resourceGroup": "myResourceGroupb1404f", - "securityRules": [], - "type": "Microsoft.Network/networkSecurityGroups" + } + ], + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroupb1404f/providers/Microsoft.Network/networkSecurityGroups/myNSGNameb1404f", + "location": "eastus", + "name": "myNSGNameb1404f", + "provisioningState": "Succeeded", + "resourceGroup": "myResourceGroupb1404f", + "securityRules": [], + "type": "Microsoft.Network/networkSecurityGroups" } } ``` From cef7a1a1a2ea65d3023b5e68e35ab06e7b6e3d70 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sat, 23 Sep 2023 14:35:15 -0700 Subject: [PATCH 185/226] [update] scenario to not use az role assignment and then simplify the json comparison process. --- internal/engine/common.go | 11 +++----- internal/utils/json.go | 33 +++++++++++------------ scenarios/ocd/CreateLinuxVMLAMP/README.md | 6 +++-- 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/internal/engine/common.go b/internal/engine/common.go index f28f24fe..1c100bf8 100644 --- a/internal/engine/common.go +++ b/internal/engine/common.go @@ -61,22 +61,17 @@ func indentMultiLineCommand(content string, indentation int) string { func compareCommandOutputs(actualOutput string, expectedOutput string, expectedSimilarity float64, expectedOutputLanguage string) error { if strings.ToLower(expectedOutputLanguage) == "json" { logging.GlobalLogger.Debugf("Comparing JSON strings:\nExpected: %s\nActual%s", expectedOutput, actualOutput) - meetsThreshold, err := utils.CompareJsonStrings(actualOutput, expectedOutput, expectedSimilarity) + results, err := utils.CompareJsonStrings(actualOutput, expectedOutput, expectedSimilarity) if err != nil { return err } - if !meetsThreshold { + if !results.AboveThreshold { return fmt.Errorf(errorMessageStyle.Render("Expected output does not match actual output.")) } - score, _ := utils.ComputeJsonStringSimilarity(actualOutput, expectedOutput) - - actual, _ := utils.OrderJsonFields(actualOutput) - expected, _ := utils.OrderJsonFields(expectedOutput) - - logging.GlobalLogger.WithField("actual", actual).WithField("expected", expected).Debugf("Jaro score: %f Expected Similarity: %f", score, expectedSimilarity) + logging.GlobalLogger.Debugf("Expected Similarity: %f, Actual Similarity: %f", expectedSimilarity, results.Score) } else { score := smetrics.JaroWinkler(expectedOutput, actualOutput, 0.7, 4) diff --git a/internal/utils/json.go b/internal/utils/json.go index 8fa82c38..f1032c89 100644 --- a/internal/utils/json.go +++ b/internal/utils/json.go @@ -20,30 +20,29 @@ func OrderJsonFields(jsonStr string) (string, error) { return string(orderedJson), nil } -// Compute the Jaro-Winkler score for two JSON strings. The score is computed -// by ordering the fields alphabetically and then comparing the strings using -// the Jaro-Winkler algorithm. -func ComputeJsonStringSimilarity(actualJson string, expectedJson string) (float64, error) { +type ComparisonResult struct { + AboveThreshold bool + Score float64 +} + +// Compare two JSON strings by ordering the fields alphabetically and then +// comparing the strings using the Jaro-Winkler algorithm to compute a score. +// If the score is greater than the threshold, return true. +func CompareJsonStrings(actualJson string, expectedJson string, threshold float64) (ComparisonResult, error) { actualOutput, err := OrderJsonFields(actualJson) if err != nil { - return 0, err + return ComparisonResult{}, err } expectedOutput, err := OrderJsonFields(expectedJson) if err != nil { - return 0, err + return ComparisonResult{}, err } - return smetrics.Jaro(actualOutput, expectedOutput), nil -} + score := smetrics.Jaro(actualOutput, expectedOutput) -// Compare two JSON strings by ordering the fields alphabetically and then -// comparing the strings using the Jaro-Winkler algorithm to compute a score. -// If the score is greater than the threshold, return true. -func CompareJsonStrings(actualJson string, expectedJson string, threshold float64) (bool, error) { - score, err := ComputeJsonStringSimilarity(actualJson, expectedJson) - if err != nil { - return false, err - } - return score > threshold, nil + return ComparisonResult{ + AboveThreshold: score >= threshold, + Score: score, + }, nil } diff --git a/scenarios/ocd/CreateLinuxVMLAMP/README.md b/scenarios/ocd/CreateLinuxVMLAMP/README.md index fb7796a1..5c09f691 100644 --- a/scenarios/ocd/CreateLinuxVMLAMP/README.md +++ b/scenarios/ocd/CreateLinuxVMLAMP/README.md @@ -717,6 +717,7 @@ Results: } ``` + Results: - - + ## Export the SSH configuration for use with SSH clients that support OpenSSH From 76305ab826c5a7f78f883558377c51f9fa10b33e Mon Sep 17 00:00:00 2001 From: vmarcella Date: Sat, 23 Sep 2023 14:51:02 -0700 Subject: [PATCH 186/226] [update] utils -> lib and refactor some functions out of the engine. --- internal/az/account.go | 42 +++++++++++++++++ internal/engine/common.go | 4 +- internal/engine/engine.go | 67 +++++----------------------- internal/engine/execution.go | 8 ++-- internal/engine/scenario.go | 9 ++-- internal/engine/testing.go | 6 +-- internal/kube/deployments.go | 4 +- internal/{utils => lib}/diff.go | 2 +- internal/lib/fs/directories.go | 21 +++++++++ internal/{utils => lib/fs}/file.go | 2 +- internal/{utils => lib}/ints.go | 2 +- internal/{utils => lib}/json.go | 2 +- internal/lib/json_test.go | 1 + internal/{utils => lib}/maps.go | 2 +- internal/{utils => lib}/maps_test.go | 2 +- internal/{utils => lib}/user.go | 2 +- internal/shells/bash.go | 9 ++-- internal/utils/json_test.go | 1 - 18 files changed, 102 insertions(+), 84 deletions(-) create mode 100644 internal/az/account.go rename internal/{utils => lib}/diff.go (94%) create mode 100644 internal/lib/fs/directories.go rename internal/{utils => lib/fs}/file.go (90%) rename internal/{utils => lib}/ints.go (76%) rename internal/{utils => lib}/json.go (98%) create mode 100644 internal/lib/json_test.go rename internal/{utils => lib}/maps.go (96%) rename internal/{utils => lib}/maps_test.go (98%) rename internal/{utils => lib}/user.go (97%) delete mode 100644 internal/utils/json_test.go diff --git a/internal/az/account.go b/internal/az/account.go new file mode 100644 index 00000000..ef18c09d --- /dev/null +++ b/internal/az/account.go @@ -0,0 +1,42 @@ +package az + +import ( + "fmt" + + "github.com/Azure/InnovationEngine/internal/logging" + "github.com/Azure/InnovationEngine/internal/shells" +) + +func RefreshAccessToken() error { + // Login + command := "az account get-access-token > ~/.azure/accessTokens.json" + logging.GlobalLogger.Info("Logging into the azure cli.") + output, err := shells.ExecuteBashCommand(command, shells.BashCommandConfiguration{EnvironmentVariables: map[string]string{}, InteractiveCommand: true, WriteToHistory: false, InheritEnvironment: false}) + + logging.GlobalLogger.Debugf("Login stdout: %s", output.StdOut) + logging.GlobalLogger.Debugf("Login stderr: %s", output.StdErr) + + if err != nil { + logging.GlobalLogger.Errorf("Failed to login %s", err) + return err + } + + logging.GlobalLogger.Info("Login successful.") + return nil +} + +func SetSubscription(subscription string) error { + if subscription != "" { + command := fmt.Sprintf("az account set --subscription %s", subscription) + _, err := shells.ExecuteBashCommand(command, shells.BashCommandConfiguration{EnvironmentVariables: map[string]string{}, InteractiveCommand: false, WriteToHistory: false, InheritEnvironment: false}) + + if err != nil { + logging.GlobalLogger.Errorf("Failed to set subscription: %s", err) + return err + } + + logging.GlobalLogger.Infof("Set subscription to %s", subscription) + } + + return nil +} diff --git a/internal/engine/common.go b/internal/engine/common.go index 1c100bf8..6a542f9c 100644 --- a/internal/engine/common.go +++ b/internal/engine/common.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" + "github.com/Azure/InnovationEngine/internal/lib" "github.com/Azure/InnovationEngine/internal/logging" - "github.com/Azure/InnovationEngine/internal/utils" "github.com/charmbracelet/lipgloss" "github.com/xrash/smetrics" ) @@ -61,7 +61,7 @@ func indentMultiLineCommand(content string, indentation int) string { func compareCommandOutputs(actualOutput string, expectedOutput string, expectedSimilarity float64, expectedOutputLanguage string) error { if strings.ToLower(expectedOutputLanguage) == "json" { logging.GlobalLogger.Debugf("Comparing JSON strings:\nExpected: %s\nActual%s", expectedOutput, actualOutput) - results, err := utils.CompareJsonStrings(actualOutput, expectedOutput, expectedSimilarity) + results, err := lib.CompareJsonStrings(actualOutput, expectedOutput, expectedSimilarity) if err != nil { return err diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 7c656398..23541312 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -4,9 +4,10 @@ import ( "fmt" "os" + "github.com/Azure/InnovationEngine/internal/az" + "github.com/Azure/InnovationEngine/internal/lib" + "github.com/Azure/InnovationEngine/internal/lib/fs" "github.com/Azure/InnovationEngine/internal/logging" - "github.com/Azure/InnovationEngine/internal/shells" - "github.com/Azure/InnovationEngine/internal/utils" "github.com/charmbracelet/lipgloss" ) @@ -47,7 +48,7 @@ func NewEngine(configuration EngineConfiguration) (*Engine, error) { // return nil, err // } - err := setSubscription(configuration.Subscription) + err := az.SetSubscription(configuration.Subscription) if err != nil { logging.GlobalLogger.Errorf("Invalid Config: Failed to set subscription: %s", err) return nil, err @@ -58,54 +59,6 @@ func NewEngine(configuration EngineConfiguration) (*Engine, error) { }, nil } -func refreshAccessToken() error { - // Login - command := "az account get-access-token > ~/.azure/accessTokens.json" - logging.GlobalLogger.Info("Logging into the azure cli.") - output, err := shells.ExecuteBashCommand(command, shells.BashCommandConfiguration{EnvironmentVariables: map[string]string{}, InteractiveCommand: true, WriteToHistory: false, InheritEnvironment: false}) - - logging.GlobalLogger.Debugf("Login stdout: %s", output.StdOut) - logging.GlobalLogger.Debugf("Login stderr: %s", output.StdErr) - - if err != nil { - logging.GlobalLogger.Errorf("Failed to login %s", err) - return err - } - - logging.GlobalLogger.Info("Login successful.") - return nil -} - -func setSubscription(subscription string) error { - if subscription != "" { - command := fmt.Sprintf("az account set --subscription %s", subscription) - _, err := shells.ExecuteBashCommand(command, shells.BashCommandConfiguration{EnvironmentVariables: map[string]string{}, InteractiveCommand: false, WriteToHistory: false, InheritEnvironment: false}) - - if err != nil { - logging.GlobalLogger.Errorf("Failed to set subscription: %s", err) - return err - } - - logging.GlobalLogger.Infof("Set subscription to %s", subscription) - } - - return nil -} - -func setWorkingDirectory(directory string) error { - // Change working directory if specified - if directory != "" { - err := os.Chdir(directory) - if err != nil { - logging.GlobalLogger.Error("Failed to change working directory", err) - return err - } - - logging.GlobalLogger.Infof("Changed directory to %s", directory) - } - return nil -} - // If the correlation ID is set, we need to set the AZURE_HTTP_USER_AGENT // environment variable so that the Azure CLI will send the correlation ID // with Azure Resource Manager requests. @@ -125,15 +78,15 @@ func (e *Engine) ExecuteScenario(scenario *Scenario) error { return err } - setWorkingDirectory(e.Configuration.WorkingDirectory) + fs.SetWorkingDirectory(e.Configuration.WorkingDirectory) setCorrelationId(e.Configuration.CorrelationId, scenario.Environment) // Execute the steps fmt.Println(scenarioTitleStyle.Render(scenario.Name)) - e.ExecuteAndRenderSteps(scenario.Steps, utils.CopyMap(scenario.Environment)) + e.ExecuteAndRenderSteps(scenario.Steps, lib.CopyMap(scenario.Environment)) fmt.Printf(scriptHeader.Render("# Generated bash to replicate what just happened:")+"\n%s\n", scriptText.Render(scenario.ToShellScript())) - setWorkingDirectory(originalDirectory) + fs.SetWorkingDirectory(originalDirectory) return nil } @@ -147,12 +100,12 @@ func (e *Engine) TestScenario(scenario *Scenario) error { return err } - setWorkingDirectory(e.Configuration.WorkingDirectory) + fs.SetWorkingDirectory(e.Configuration.WorkingDirectory) setCorrelationId(e.Configuration.CorrelationId, scenario.Environment) fmt.Println(scenarioTitleStyle.Render(scenario.Name)) - e.TestSteps(scenario.Steps, utils.CopyMap(scenario.Environment)) + e.TestSteps(scenario.Steps, lib.CopyMap(scenario.Environment)) - setWorkingDirectory(originalDirectory) + fs.SetWorkingDirectory(originalDirectory) return nil } diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 7e7f9d95..871a5f9a 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -7,11 +7,11 @@ import ( "strings" "time" + "github.com/Azure/InnovationEngine/internal/lib" "github.com/Azure/InnovationEngine/internal/logging" "github.com/Azure/InnovationEngine/internal/ocd" "github.com/Azure/InnovationEngine/internal/parsers" "github.com/Azure/InnovationEngine/internal/shells" - "github.com/Azure/InnovationEngine/internal/utils" ) const ( @@ -180,7 +180,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { hideCursor() go func(block parsers.CodeBlock) { - output, err := shells.ExecuteBashCommand(block.Content, shells.BashCommandConfiguration{EnvironmentVariables: utils.CopyMap(env), InheritEnvironment: true, InteractiveCommand: false, WriteToHistory: true}) + output, err := shells.ExecuteBashCommand(block.Content, shells.BashCommandConfiguration{EnvironmentVariables: lib.CopyMap(env), InheritEnvironment: true, InteractiveCommand: false, WriteToHistory: true}) logging.GlobalLogger.Infof("Command output to stdout:\n %s", output.StdOut) logging.GlobalLogger.Infof("Command output to stderr:\n %s", output.StdErr) commandOutput = output @@ -209,7 +209,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { fmt.Printf("\r %s \n", errorStyle.Render("✗")) moveCursorPositionDown(lines) fmt.Printf(" %s\n", errorMessageStyle.Render(err.Error())) - fmt.Printf(" %s\n", utils.GetDifferenceBetweenStrings(block.ExpectedOutput.Content, commandOutput.StdOut)) + fmt.Printf(" %s\n", lib.GetDifferenceBetweenStrings(block.ExpectedOutput.Content, commandOutput.StdOut)) ocdStatus.SetError(err) reportOCDStatus(ocdStatus, e.Configuration.Environment) @@ -271,7 +271,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { reportOCDStatus(ocdStatus, e.Configuration.Environment) } - output, err := shells.ExecuteBashCommand(block.Content, shells.BashCommandConfiguration{EnvironmentVariables: utils.CopyMap(env), InheritEnvironment: true, InteractiveCommand: true, WriteToHistory: false}) + output, err := shells.ExecuteBashCommand(block.Content, shells.BashCommandConfiguration{EnvironmentVariables: lib.CopyMap(env), InheritEnvironment: true, InteractiveCommand: true, WriteToHistory: false}) if err == nil { showCursor() diff --git a/internal/engine/scenario.go b/internal/engine/scenario.go index 2de02b55..8481367c 100644 --- a/internal/engine/scenario.go +++ b/internal/engine/scenario.go @@ -7,9 +7,10 @@ import ( "regexp" "strings" + "github.com/Azure/InnovationEngine/internal/lib" + "github.com/Azure/InnovationEngine/internal/lib/fs" "github.com/Azure/InnovationEngine/internal/logging" "github.com/Azure/InnovationEngine/internal/parsers" - "github.com/Azure/InnovationEngine/internal/utils" "github.com/yuin/goldmark/ast" ) @@ -53,7 +54,7 @@ func groupCodeBlocksIntoSteps(blocks []parsers.CodeBlock) []Step { // used to filter out code blocks that should not be parsed out of the markdown // file. func CreateScenarioFromMarkdown(path string, languagesToExecute []string, environmentVariableOverrides map[string]string) (*Scenario, error) { - if !utils.FileExists(path) { + if !fs.FileExists(path) { return nil, fmt.Errorf("markdown file '%s' does not exist", path) } @@ -67,7 +68,7 @@ func CreateScenarioFromMarkdown(path string, languagesToExecute []string, enviro environmentVariables := make(map[string]string) // Check if the INI file exists & load it. - if !utils.FileExists(markdownINI) { + if !fs.FileExists(markdownINI) { logging.GlobalLogger.Infof("INI file '%s' does not exist, skipping...", markdownINI) } else { logging.GlobalLogger.Infof("INI file '%s' exists, loading...", markdownINI) @@ -93,7 +94,7 @@ func CreateScenarioFromMarkdown(path string, languagesToExecute []string, enviro codeBlocks := parsers.ExtractCodeBlocksFromAst(markdown, source, languagesToExecute) logging.GlobalLogger.WithField("CodeBlocks", codeBlocks).Debugf("Found %d code blocks", len(codeBlocks)) - varsToExport := utils.CopyMap(environmentVariableOverrides) + varsToExport := lib.CopyMap(environmentVariableOverrides) for key, value := range environmentVariableOverrides { logging.GlobalLogger.Debugf("Attempting to override %s with %s", key, value) exportRegex := regexp.MustCompile(fmt.Sprintf(`export %s=["']?([a-z-A-Z0-9_]+)["']?`, key)) diff --git a/internal/engine/testing.go b/internal/engine/testing.go index 3da4aa80..cb6e2d0b 100644 --- a/internal/engine/testing.go +++ b/internal/engine/testing.go @@ -4,10 +4,10 @@ import ( "fmt" "time" + "github.com/Azure/InnovationEngine/internal/lib" "github.com/Azure/InnovationEngine/internal/logging" "github.com/Azure/InnovationEngine/internal/parsers" "github.com/Azure/InnovationEngine/internal/shells" - "github.com/Azure/InnovationEngine/internal/utils" ) func (e *Engine) TestSteps(steps []Step, env map[string]string) { @@ -28,7 +28,7 @@ testRunner: var commandOutput shells.CommandOutput go func(block parsers.CodeBlock) { logging.GlobalLogger.Infof("Executing command: %s", block.Content) - output, err := shells.ExecuteBashCommand(block.Content, shells.BashCommandConfiguration{EnvironmentVariables: utils.CopyMap(env), InheritEnvironment: true, InteractiveCommand: false, WriteToHistory: true}) + output, err := shells.ExecuteBashCommand(block.Content, shells.BashCommandConfiguration{EnvironmentVariables: lib.CopyMap(env), InheritEnvironment: true, InteractiveCommand: false, WriteToHistory: true}) logging.GlobalLogger.Infof("Command stdout: %s", output.StdOut) logging.GlobalLogger.Infof("Command stderr: %s", output.StdErr) commandOutput = output @@ -96,7 +96,7 @@ testRunner: fmt.Printf("\n") fmt.Printf("Deleting resource group: %s\n", resourceGroupName) command := fmt.Sprintf("az group delete --name %s --yes", resourceGroupName) - output, err := shells.ExecuteBashCommand(command, shells.BashCommandConfiguration{EnvironmentVariables: utils.CopyMap(env), InheritEnvironment: true, InteractiveCommand: false, WriteToHistory: true}) + output, err := shells.ExecuteBashCommand(command, shells.BashCommandConfiguration{EnvironmentVariables: lib.CopyMap(env), InheritEnvironment: true, InteractiveCommand: false, WriteToHistory: true}) if err != nil { fmt.Print(errorStyle.Render("Error deleting resource group: %s\n", err.Error())) logging.GlobalLogger.Errorf("Error deleting resource group: %s", err.Error()) diff --git a/internal/kube/deployments.go b/internal/kube/deployments.go index 98db6afd..89ccc3cf 100644 --- a/internal/kube/deployments.go +++ b/internal/kube/deployments.go @@ -3,7 +3,7 @@ package kube import ( "context" - "github.com/Azure/InnovationEngine/internal/utils" + "github.com/Azure/InnovationEngine/internal/lib" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -17,7 +17,7 @@ func GetAgentDeployment(id string) *appsv1.Deployment { Name: "runner-" + id, }, Spec: appsv1.DeploymentSpec{ - Replicas: utils.Int32Ptr(1), + Replicas: lib.Int32Ptr(1), Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "app": "runner", diff --git a/internal/utils/diff.go b/internal/lib/diff.go similarity index 94% rename from internal/utils/diff.go rename to internal/lib/diff.go index 4bf9ddfd..44745b69 100644 --- a/internal/utils/diff.go +++ b/internal/lib/diff.go @@ -1,4 +1,4 @@ -package utils +package lib import ( "fmt" diff --git a/internal/lib/fs/directories.go b/internal/lib/fs/directories.go new file mode 100644 index 00000000..89c8faed --- /dev/null +++ b/internal/lib/fs/directories.go @@ -0,0 +1,21 @@ +package fs + +import ( + "os" + + "github.com/Azure/InnovationEngine/internal/logging" +) + +func SetWorkingDirectory(directory string) error { + // Change working directory if specified + if directory != "" { + err := os.Chdir(directory) + if err != nil { + logging.GlobalLogger.Error("Failed to change working directory", err) + return err + } + + logging.GlobalLogger.Infof("Changed directory to %s", directory) + } + return nil +} diff --git a/internal/utils/file.go b/internal/lib/fs/file.go similarity index 90% rename from internal/utils/file.go rename to internal/lib/fs/file.go index 2f53d0d1..36f44860 100644 --- a/internal/utils/file.go +++ b/internal/lib/fs/file.go @@ -1,4 +1,4 @@ -package utils +package fs import "os" diff --git a/internal/utils/ints.go b/internal/lib/ints.go similarity index 76% rename from internal/utils/ints.go rename to internal/lib/ints.go index eaae8c30..5b0d834c 100644 --- a/internal/utils/ints.go +++ b/internal/lib/ints.go @@ -1,3 +1,3 @@ -package utils +package lib func Int32Ptr(i int32) *int32 { return &i } diff --git a/internal/utils/json.go b/internal/lib/json.go similarity index 98% rename from internal/utils/json.go rename to internal/lib/json.go index f1032c89..d96cb835 100644 --- a/internal/utils/json.go +++ b/internal/lib/json.go @@ -1,4 +1,4 @@ -package utils +package lib import ( "encoding/json" diff --git a/internal/lib/json_test.go b/internal/lib/json_test.go new file mode 100644 index 00000000..55c21f80 --- /dev/null +++ b/internal/lib/json_test.go @@ -0,0 +1 @@ +package lib diff --git a/internal/utils/maps.go b/internal/lib/maps.go similarity index 96% rename from internal/utils/maps.go rename to internal/lib/maps.go index a12bae61..7e4388a5 100644 --- a/internal/utils/maps.go +++ b/internal/lib/maps.go @@ -1,4 +1,4 @@ -package utils +package lib // Makes a copy of a map func CopyMap(m map[string]string) map[string]string { diff --git a/internal/utils/maps_test.go b/internal/lib/maps_test.go similarity index 98% rename from internal/utils/maps_test.go rename to internal/lib/maps_test.go index f313160e..fe68eec5 100644 --- a/internal/utils/maps_test.go +++ b/internal/lib/maps_test.go @@ -1,4 +1,4 @@ -package utils +package lib import ( "testing" diff --git a/internal/utils/user.go b/internal/lib/user.go similarity index 97% rename from internal/utils/user.go rename to internal/lib/user.go index 309d408d..e8589ae5 100644 --- a/internal/utils/user.go +++ b/internal/lib/user.go @@ -1,4 +1,4 @@ -package utils +package lib import ( "fmt" diff --git a/internal/shells/bash.go b/internal/shells/bash.go index eca54b91..adc5d694 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -10,7 +10,8 @@ import ( "golang.org/x/sys/unix" - "github.com/Azure/InnovationEngine/internal/utils" + "github.com/Azure/InnovationEngine/internal/lib" + "github.com/Azure/InnovationEngine/internal/lib/fs" ) // Location where the environment state from commands is captured and sent to @@ -18,7 +19,7 @@ import ( var environmentStateFile = "/tmp/env.txt" func loadEnvFile(path string) (map[string]string, error) { - if !utils.FileExists(path) { + if !fs.FileExists(path) { return nil, fmt.Errorf("env file '%s' does not exist", path) } @@ -117,7 +118,7 @@ func ExecuteBashCommand(command string, config BashCommandConfiguration) (Comman // isolated command calls. envFromPreviousStep, err := loadEnvFile(environmentStateFile) if err == nil { - merged := utils.MergeMaps(config.EnvironmentVariables, envFromPreviousStep) + merged := lib.MergeMaps(config.EnvironmentVariables, envFromPreviousStep) for k, v := range merged { commandToExecute.Env = append(commandToExecute.Env, fmt.Sprintf("%s=%s", k, v)) } @@ -129,7 +130,7 @@ func ExecuteBashCommand(command string, config BashCommandConfiguration) (Comman if config.WriteToHistory { - homeDir, err := utils.GetHomeDirectory() + homeDir, err := lib.GetHomeDirectory() if err != nil { return CommandOutput{}, fmt.Errorf("failed to get home directory: %w", err) diff --git a/internal/utils/json_test.go b/internal/utils/json_test.go deleted file mode 100644 index d4b585bf..00000000 --- a/internal/utils/json_test.go +++ /dev/null @@ -1 +0,0 @@ -package utils From cd3e3bdf794af07b9a7e92de3ad470b7674635d3 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 25 Sep 2023 12:43:55 -0700 Subject: [PATCH 187/226] [remove] az ssh command from LEMP. --- scenarios/ocd/CreateLinuxVMLAMP/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/scenarios/ocd/CreateLinuxVMLAMP/README.md b/scenarios/ocd/CreateLinuxVMLAMP/README.md index 5c09f691..29c6cabf 100644 --- a/scenarios/ocd/CreateLinuxVMLAMP/README.md +++ b/scenarios/ocd/CreateLinuxVMLAMP/README.md @@ -756,6 +756,7 @@ Results: ``` --> + ## Browse your WordPress website From 8266409c1971321a5b9811601f6f3e67b8c21f6b Mon Sep 17 00:00:00 2001 From: vmarcella Date: Mon, 25 Sep 2023 15:43:20 -0700 Subject: [PATCH 188/226] [update] random postfix to have a bit more entropy. --- scenarios/ocd/CreateLinuxVMAndSSH/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenarios/ocd/CreateLinuxVMAndSSH/README.md b/scenarios/ocd/CreateLinuxVMAndSSH/README.md index 8f9402f1..a0e2518a 100644 --- a/scenarios/ocd/CreateLinuxVMAndSSH/README.md +++ b/scenarios/ocd/CreateLinuxVMAndSSH/README.md @@ -5,7 +5,7 @@ The First step in this tutorial is to define environment variables ```bash -export UNIQUE_POSTFIX="$(($RANDOM % 100 + 1))" +export UNIQUE_POSTFIX="$(($RANDOM % 100000 + 1))" export MY_RESOURCE_GROUP_NAME="myResourceGroup$UNIQUE_POSTFIX" export REGION=EastUS export MY_VM_NAME="myVM$UNIQUE_POSTFIX" From 633bec4589a82f4d79b198cf8234d04f6506671c Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 26 Sep 2023 13:04:12 -0700 Subject: [PATCH 189/226] [update] random postfix to have even more entropy. --- scenarios/ocd/CreateLinuxVMAndSSH/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scenarios/ocd/CreateLinuxVMAndSSH/README.md b/scenarios/ocd/CreateLinuxVMAndSSH/README.md index a0e2518a..4ba4a903 100644 --- a/scenarios/ocd/CreateLinuxVMAndSSH/README.md +++ b/scenarios/ocd/CreateLinuxVMAndSSH/README.md @@ -5,7 +5,7 @@ The First step in this tutorial is to define environment variables ```bash -export UNIQUE_POSTFIX="$(($RANDOM % 100000 + 1))" +export UNIQUE_POSTFIX="$(date +%M%S)$$" export MY_RESOURCE_GROUP_NAME="myResourceGroup$UNIQUE_POSTFIX" export REGION=EastUS export MY_VM_NAME="myVM$UNIQUE_POSTFIX" From b08f28d32862c29c3c599e4df8ebd1d905bd6093 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Tue, 26 Sep 2023 13:21:38 -0700 Subject: [PATCH 190/226] [add] to-bash command to generate scripts for both local & portal environments. --- cmd/ie/commands/to-bash.go | 79 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 cmd/ie/commands/to-bash.go diff --git a/cmd/ie/commands/to-bash.go b/cmd/ie/commands/to-bash.go new file mode 100644 index 00000000..044aad65 --- /dev/null +++ b/cmd/ie/commands/to-bash.go @@ -0,0 +1,79 @@ +package commands + +import ( + "encoding/json" + "fmt" + "os" + "strings" + + "github.com/Azure/InnovationEngine/internal/engine" + "github.com/Azure/InnovationEngine/internal/logging" + "github.com/spf13/cobra" +) + +type OcdScript struct { + Script string `json:"script"` +} + +var toBashCommand = &cobra.Command{ + Use: "to-bash", + Short: "Convert a markdown scenario into a bash script.", + Run: func(cmd *cobra.Command, args []string) { + markdownFile := args[0] + if markdownFile == "" { + logging.GlobalLogger.Errorf("Error: No markdown file specified.") + cmd.Help() + os.Exit(1) + } + + environment, _ := cmd.Flags().GetString("environment") + environmentVariables, _ := cmd.Flags().GetStringArray("var") + + // Parse the environment variables + cliEnvironmentVariables := make(map[string]string) + for _, environmentVariable := range environmentVariables { + keyValuePair := strings.SplitN(environmentVariable, "=", 2) + if len(keyValuePair) != 2 { + logging.GlobalLogger.Errorf("Error: Invalid environment variable format: %s", environmentVariable) + fmt.Printf("Error: Invalid environment variable format: %s", environmentVariable) + cmd.Help() + os.Exit(1) + } + + cliEnvironmentVariables[keyValuePair[0]] = keyValuePair[1] + } + + // Parse the markdown file and create a scenario + scenario, err := engine.CreateScenarioFromMarkdown( + markdownFile, + []string{"bash", "azurecli", "azurecli-interactive", "terraform"}, + cliEnvironmentVariables) + + if err != nil { + logging.GlobalLogger.Errorf("Error creating scenario: %s", err) + fmt.Printf("Error creating scenario: %s", err) + os.Exit(0) + } + + if environment == "ocd" { + script := OcdScript{Script: scenario.ToShellScript()} + scriptJson, err := json.Marshal(script) + + if err != nil { + logging.GlobalLogger.Errorf("Error converting to json: %s", err) + fmt.Printf("Error converting to json: %s", err) + os.Exit(1) + } + + fmt.Printf("ie_us%sie_ue\n", scriptJson) + } else { + fmt.Printf("%s", scenario.ToShellScript()) + } + + }, +} + +func init() { + rootCommand.AddCommand(toBashCommand) + toBashCommand.PersistentFlags().StringArray("var", []string{}, "Sets an environment variable for the scenario. Format: --var =") +} From 1340ab7d6a4ef2a2289ec58efb4f21abbd91b659 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Wed, 27 Sep 2023 13:32:03 -0700 Subject: [PATCH 191/226] [add] identity login for the portal, update environment state file name. --- internal/az/account.go | 4 ++-- internal/engine/engine.go | 13 +++++++------ internal/engine/execution.go | 4 +++- internal/shells/bash.go | 4 ++-- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/internal/az/account.go b/internal/az/account.go index ef18c09d..9edd0700 100644 --- a/internal/az/account.go +++ b/internal/az/account.go @@ -7,9 +7,9 @@ import ( "github.com/Azure/InnovationEngine/internal/shells" ) -func RefreshAccessToken() error { +func LoginWithMSI() error { // Login - command := "az account get-access-token > ~/.azure/accessTokens.json" + command := "az login --identity" logging.GlobalLogger.Info("Logging into the azure cli.") output, err := shells.ExecuteBashCommand(command, shells.BashCommandConfiguration{EnvironmentVariables: map[string]string{}, InteractiveCommand: true, WriteToHistory: false, InheritEnvironment: false}) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 23541312..9032fe7d 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -41,12 +41,13 @@ type Engine struct { // / Create a new engine instance. func NewEngine(configuration EngineConfiguration) (*Engine, error) { - // Temporarily disable until login code worked out. - // err := refreshAccessToken() - // if err != nil { - // logging.GlobalLogger.Errorf("Invalid Config: Failed to login: %s", err) - // return nil, err - // } + if configuration.Environment == "ocd" { + err := az.LoginWithMSI() + if err != nil { + logging.GlobalLogger.Errorf("Invalid Config: Failed to login: %s", err) + return nil, err + } + } err := az.SetSubscription(configuration.Subscription) if err != nil { diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 871a5f9a..97878b75 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -303,5 +303,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { attachResourceURIsToOCDStatus(&ocdStatus, resourceGroupName, e.Configuration.Environment) reportOCDStatus(ocdStatus, e.Configuration.Environment) - shells.ResetStoredEnvironmentVariables() + if e.Configuration.Environment != "ocd" { + shells.ResetStoredEnvironmentVariables() + } } diff --git a/internal/shells/bash.go b/internal/shells/bash.go index adc5d694..d179890e 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -16,7 +16,7 @@ import ( // Location where the environment state from commands is captured and sent to // for being able to share state across commands. -var environmentStateFile = "/tmp/env.txt" +var environmentStateFile = "/tmp/env-vars" func loadEnvFile(path string) (map[string]string, error) { if !fs.FileExists(path) { @@ -89,7 +89,7 @@ func ExecuteBashCommand(command string, config BashCommandConfiguration) (Comman var commandWithStateSaved = []string{ command, "IE_LAST_COMMAND_EXIT_CODE=\"$?\"", - "env > /tmp/env.txt", + "env > " + environmentStateFile, "exit $IE_LAST_COMMAND_EXIT_CODE", } From 5ac6e33f17851533241e61047aa32b15fd28fbd8 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Wed, 27 Sep 2023 13:46:23 -0700 Subject: [PATCH 192/226] [update] bash command executed for MSI login. --- internal/az/account.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/az/account.go b/internal/az/account.go index 9edd0700..0c11fb61 100644 --- a/internal/az/account.go +++ b/internal/az/account.go @@ -11,7 +11,7 @@ func LoginWithMSI() error { // Login command := "az login --identity" logging.GlobalLogger.Info("Logging into the azure cli.") - output, err := shells.ExecuteBashCommand(command, shells.BashCommandConfiguration{EnvironmentVariables: map[string]string{}, InteractiveCommand: true, WriteToHistory: false, InheritEnvironment: false}) + output, err := shells.ExecuteBashCommand(command, shells.BashCommandConfiguration{EnvironmentVariables: map[string]string{}, InteractiveCommand: false, WriteToHistory: false, InheritEnvironment: true}) logging.GlobalLogger.Debugf("Login stdout: %s", output.StdOut) logging.GlobalLogger.Debugf("Login stderr: %s", output.StdErr) From 0736eb811d1453652387e11686a880ce81af1250 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 28 Sep 2023 17:45:46 +0000 Subject: [PATCH 193/226] [update] engine creation to be checked in ie execute. --- cmd/ie/commands/execute.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/cmd/ie/commands/execute.go b/cmd/ie/commands/execute.go index a6c94130..613bdeaa 100644 --- a/cmd/ie/commands/execute.go +++ b/cmd/ie/commands/execute.go @@ -63,7 +63,6 @@ var executeCommand = &cobra.Command{ cliEnvironmentVariables[keyValuePair[0]] = keyValuePair[1] } -<<<<<<< HEAD // Parse the markdown file and create a scenario scenario, err := engine.CreateScenarioFromMarkdown(markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}, cliEnvironmentVariables) if err != nil { @@ -72,10 +71,7 @@ var executeCommand = &cobra.Command{ os.Exit(1) } - innovationEngine := engine.NewEngine(engine.EngineConfiguration{ -======= innovationEngine, err := engine.NewEngine(engine.EngineConfiguration{ ->>>>>>> 5ac6e33f17851533241e61047aa32b15fd28fbd8 Verbose: verbose, DoNotDelete: doNotDelete, Subscription: subscription, @@ -84,23 +80,12 @@ var executeCommand = &cobra.Command{ WorkingDirectory: workingDirectory, }) -<<<<<<< HEAD -======= if err != nil { logging.GlobalLogger.Errorf("Error creating engine: %s", err) fmt.Printf("Error creating engine: %s", err) os.Exit(1) } - // Parse the markdown file and create a scenario - scenario, err := engine.CreateScenarioFromMarkdown(markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}, cliEnvironmentVariables) - if err != nil { - logging.GlobalLogger.Errorf("Error creating scenario: %s", err) - fmt.Printf("Error creating scenario: %s", err) - os.Exit(1) - } - ->>>>>>> 5ac6e33f17851533241e61047aa32b15fd28fbd8 // Execute the scenario err = innovationEngine.ExecuteScenario(scenario) if err != nil { From 47220786c7914f4a99cb40dce8d3c856d053ac8d Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 28 Sep 2023 18:47:17 +0000 Subject: [PATCH 194/226] [add] token generation for one click deployments. --- internal/az/account.go | 27 +++++++++++++++++++++++++++ internal/engine/engine.go | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/internal/az/account.go b/internal/az/account.go index 0c11fb61..13910bd6 100644 --- a/internal/az/account.go +++ b/internal/az/account.go @@ -2,6 +2,7 @@ package az import ( "fmt" + "regexp" "github.com/Azure/InnovationEngine/internal/logging" "github.com/Azure/InnovationEngine/internal/shells" @@ -40,3 +41,29 @@ func SetSubscription(subscription string) error { return nil } + +type AzureTokenProvider struct { + Resource string + Regex *regexp.Regexp +} + +var KeyVaultProvider = AzureTokenProvider{ + Resource: "https://vault.azure.net", + Regex: regexp.MustCompile("az keyvault"), +} + +var AzureTokenProviders = []AzureTokenProvider{ + KeyVaultProvider, +} + +func GetAccessToken(provider AzureTokenProvider) (string, error) { + command := fmt.Sprintf("az account get-access-token --resource %s --query accessToken -o tsv", provider.Resource) + output, err := shells.ExecuteBashCommand(command, shells.BashCommandConfiguration{EnvironmentVariables: map[string]string{}, InteractiveCommand: false, WriteToHistory: false, InheritEnvironment: true}) + + if err != nil { + logging.GlobalLogger.Errorf("Failed to get access token: %s", err) + return "", err + } + + return output.StdOut, nil +} diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 9032fe7d..cf2c3192 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -1,6 +1,7 @@ package engine import ( + "encoding/json" "fmt" "os" @@ -70,6 +71,10 @@ func setCorrelationId(correlationId string, env map[string]string) { } } +type AzureTokens struct { + Tokens []string `json:"tokens"` +} + // Executes a deployment scenario. func (e *Engine) ExecuteScenario(scenario *Scenario) error { // Store the current directory so we can restore it later @@ -82,6 +87,39 @@ func (e *Engine) ExecuteScenario(scenario *Scenario) error { fs.SetWorkingDirectory(e.Configuration.WorkingDirectory) setCorrelationId(e.Configuration.CorrelationId, scenario.Environment) + if e.Configuration.Environment == "ocd" { + tokens := make(map[string]string) + + for _, step := range scenario.Steps { + for _, codeblock := range step.CodeBlocks { + for _, provider := range az.AzureTokenProviders { + if provider.Regex.MatchString(codeblock.Content) { + accessToken, err := az.GetAccessToken(provider) + if err != nil { + logging.GlobalLogger.Errorf("Failed to get access token: %s", err) + return err + } + + tokens[provider.Resource] = accessToken + } + } + } + } + + ocdTokens := AzureTokens{ + Tokens: []string{}, + } + for _, token := range tokens { + ocdTokens.Tokens = append(ocdTokens.Tokens, token) + } + json, err := json.Marshal(ocdTokens) + if err != nil { + logging.GlobalLogger.Errorf("Failed to marshal tokens: %s", err) + return err + } + fmt.Printf("ie_us%sie_ue\n", string(json)) + } + // Execute the steps fmt.Println(scenarioTitleStyle.Render(scenario.Name)) e.ExecuteAndRenderSteps(scenario.Steps, lib.CopyMap(scenario.Environment)) From 75b46b8ea0eae0f386a32accb1db8adc148fb135 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Thu, 28 Sep 2023 14:10:57 -0700 Subject: [PATCH 195/226] [disable] access token fetching for now. --- internal/engine/engine.go | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index cf2c3192..5a4e5832 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -88,31 +88,28 @@ func (e *Engine) ExecuteScenario(scenario *Scenario) error { setCorrelationId(e.Configuration.CorrelationId, scenario.Environment) if e.Configuration.Environment == "ocd" { - tokens := make(map[string]string) + tokenURLs := AzureTokens{ + Tokens: []string{}, + } for _, step := range scenario.Steps { for _, codeblock := range step.CodeBlocks { for _, provider := range az.AzureTokenProviders { if provider.Regex.MatchString(codeblock.Content) { - accessToken, err := az.GetAccessToken(provider) - if err != nil { - logging.GlobalLogger.Errorf("Failed to get access token: %s", err) - return err - } + // accessToken, err := az.GetAccessToken(provider) + // if err != nil { + // logging.GlobalLogger.Errorf("Failed to get access token: %s", err) + // return err + // } + + tokenURLs.Tokens = append(tokenURLs.Tokens, provider.Resource) - tokens[provider.Resource] = accessToken } } } } - ocdTokens := AzureTokens{ - Tokens: []string{}, - } - for _, token := range tokens { - ocdTokens.Tokens = append(ocdTokens.Tokens, token) - } - json, err := json.Marshal(ocdTokens) + json, err := json.Marshal(tokenURLs) if err != nil { logging.GlobalLogger.Errorf("Failed to marshal tokens: %s", err) return err From 8bd01fe9c7e2b1cad91a5a4c622f75de9f319992 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 29 Sep 2023 12:44:21 -0700 Subject: [PATCH 196/226] [refactor] SetCorrelationID, add a basic unit test, and then remove the need of verbose for command output rendering. --- internal/az/account.go | 27 ------------------ internal/az/env.go | 17 ++++++++++++ internal/az/env_test.go | 16 +++++++++++ internal/engine/engine.go | 53 ++---------------------------------- internal/engine/execution.go | 9 ++---- 5 files changed, 38 insertions(+), 84 deletions(-) create mode 100644 internal/az/env.go create mode 100644 internal/az/env_test.go diff --git a/internal/az/account.go b/internal/az/account.go index 13910bd6..0c11fb61 100644 --- a/internal/az/account.go +++ b/internal/az/account.go @@ -2,7 +2,6 @@ package az import ( "fmt" - "regexp" "github.com/Azure/InnovationEngine/internal/logging" "github.com/Azure/InnovationEngine/internal/shells" @@ -41,29 +40,3 @@ func SetSubscription(subscription string) error { return nil } - -type AzureTokenProvider struct { - Resource string - Regex *regexp.Regexp -} - -var KeyVaultProvider = AzureTokenProvider{ - Resource: "https://vault.azure.net", - Regex: regexp.MustCompile("az keyvault"), -} - -var AzureTokenProviders = []AzureTokenProvider{ - KeyVaultProvider, -} - -func GetAccessToken(provider AzureTokenProvider) (string, error) { - command := fmt.Sprintf("az account get-access-token --resource %s --query accessToken -o tsv", provider.Resource) - output, err := shells.ExecuteBashCommand(command, shells.BashCommandConfiguration{EnvironmentVariables: map[string]string{}, InteractiveCommand: false, WriteToHistory: false, InheritEnvironment: true}) - - if err != nil { - logging.GlobalLogger.Errorf("Failed to get access token: %s", err) - return "", err - } - - return output.StdOut, nil -} diff --git a/internal/az/env.go b/internal/az/env.go new file mode 100644 index 00000000..e66c3b30 --- /dev/null +++ b/internal/az/env.go @@ -0,0 +1,17 @@ +package az + +import ( + "fmt" + + "github.com/Azure/InnovationEngine/internal/logging" +) + +// If the correlation ID is set, we need to set the AZURE_HTTP_USER_AGENT +// environment variable so that the Azure CLI will send the correlation ID +// with Azure Resource Manager requests. +func SetCorrelationId(correlationId string, env map[string]string) { + if correlationId != "" { + env["AZURE_HTTP_USER_AGENT"] = fmt.Sprintf("innovation-engine-%s", correlationId) + logging.GlobalLogger.Info("Resource tracking enabled. Tracking ID: " + env["AZURE_HTTP_USER_AGENT"]) + } +} diff --git a/internal/az/env_test.go b/internal/az/env_test.go new file mode 100644 index 00000000..2ff973a6 --- /dev/null +++ b/internal/az/env_test.go @@ -0,0 +1,16 @@ +package az + +import ( + "testing" +) + +func TestSetCorrelationId(t *testing.T) { + t.Run("Test setting a custom correlation ID", func(t *testing.T) { + correlationId := "test-correlation-id" + env := map[string]string{} + SetCorrelationId(correlationId, env) + if env["AZURE_HTTP_USER_AGENT"] != "innovation-engine-test-correlation-id" { + t.Errorf("Expected AZURE_HTTP_USER_AGENT to be set to innovation-engine-test-correlation-id, got %s", env["AZURE_HTTP_USER_AGENT"]) + } + }) +} diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 5a4e5832..287c539a 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -1,7 +1,6 @@ package engine import ( - "encoding/json" "fmt" "os" @@ -42,14 +41,6 @@ type Engine struct { // / Create a new engine instance. func NewEngine(configuration EngineConfiguration) (*Engine, error) { - if configuration.Environment == "ocd" { - err := az.LoginWithMSI() - if err != nil { - logging.GlobalLogger.Errorf("Invalid Config: Failed to login: %s", err) - return nil, err - } - } - err := az.SetSubscription(configuration.Subscription) if err != nil { logging.GlobalLogger.Errorf("Invalid Config: Failed to set subscription: %s", err) @@ -61,16 +52,6 @@ func NewEngine(configuration EngineConfiguration) (*Engine, error) { }, nil } -// If the correlation ID is set, we need to set the AZURE_HTTP_USER_AGENT -// environment variable so that the Azure CLI will send the correlation ID -// with Azure Resource Manager requests. -func setCorrelationId(correlationId string, env map[string]string) { - if correlationId != "" { - env["AZURE_HTTP_USER_AGENT"] = fmt.Sprintf("innovation-engine-%s", correlationId) - logging.GlobalLogger.Info("Resource tracking enabled. Tracking ID: " + env["AZURE_HTTP_USER_AGENT"]) - } -} - type AzureTokens struct { Tokens []string `json:"tokens"` } @@ -85,37 +66,7 @@ func (e *Engine) ExecuteScenario(scenario *Scenario) error { } fs.SetWorkingDirectory(e.Configuration.WorkingDirectory) - setCorrelationId(e.Configuration.CorrelationId, scenario.Environment) - - if e.Configuration.Environment == "ocd" { - tokenURLs := AzureTokens{ - Tokens: []string{}, - } - - for _, step := range scenario.Steps { - for _, codeblock := range step.CodeBlocks { - for _, provider := range az.AzureTokenProviders { - if provider.Regex.MatchString(codeblock.Content) { - // accessToken, err := az.GetAccessToken(provider) - // if err != nil { - // logging.GlobalLogger.Errorf("Failed to get access token: %s", err) - // return err - // } - - tokenURLs.Tokens = append(tokenURLs.Tokens, provider.Resource) - - } - } - } - } - - json, err := json.Marshal(tokenURLs) - if err != nil { - logging.GlobalLogger.Errorf("Failed to marshal tokens: %s", err) - return err - } - fmt.Printf("ie_us%sie_ue\n", string(json)) - } + az.SetCorrelationId(e.Configuration.CorrelationId, scenario.Environment) // Execute the steps fmt.Println(scenarioTitleStyle.Render(scenario.Name)) @@ -137,7 +88,7 @@ func (e *Engine) TestScenario(scenario *Scenario) error { } fs.SetWorkingDirectory(e.Configuration.WorkingDirectory) - setCorrelationId(e.Configuration.CorrelationId, scenario.Environment) + az.SetCorrelationId(e.Configuration.CorrelationId, scenario.Environment) fmt.Println(scenarioTitleStyle.Render(scenario.Name)) e.TestSteps(scenario.Steps, lib.CopyMap(scenario.Environment)) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 97878b75..d5f441e4 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -220,9 +220,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { fmt.Printf("\r %s \n", checkStyle.Render("✔")) moveCursorPositionDown(lines) - if e.Configuration.Verbose { - fmt.Printf(" %s\n", verboseStyle.Render(commandOutput.StdOut)) - } + fmt.Printf(" %s\n", verboseStyle.Render(commandOutput.StdOut)) // Extract the resource group name from the command output if // it's not already set. @@ -278,9 +276,8 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { fmt.Printf("\r %s \n", checkStyle.Render("✔")) moveCursorPositionDown(lines) - if e.Configuration.Verbose { - fmt.Printf(" %s\n", verboseStyle.Render(output.StdOut)) - } + fmt.Printf(" %s\n", verboseStyle.Render(output.StdOut)) + if stepNumber != len(stepsToExecute)-1 { reportOCDStatus(ocdStatus, e.Configuration.Environment) } From bdf02b853bb5047ff0503d34ac10c1688e9d1d27 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 29 Sep 2023 13:40:07 -0700 Subject: [PATCH 197/226] [update] scenario testing to only run on main for now. --- .github/workflows/scenario-testing.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/scenario-testing.yaml b/.github/workflows/scenario-testing.yaml index e25c4585..c7d84597 100644 --- a/.github/workflows/scenario-testing.yaml +++ b/.github/workflows/scenario-testing.yaml @@ -6,8 +6,6 @@ on: push: branches: - main - - dev - - vmarcella/port-to-go jobs: test-ocd-scenarios: @@ -17,6 +15,7 @@ jobs: - name: Build all targets. run: | make build-all + make test-all - name: Sign into Azure uses: azure/login@v1 From aa75583faa7d6db26a16f759a5f4b53165d497c8 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 29 Sep 2023 13:50:06 -0700 Subject: [PATCH 198/226] [fix] gitignore. --- .gitignore | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.gitignore b/.gitignore index d61a403e..c76f479a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,13 +4,8 @@ __pycache__ #VS Code .vscode -<<<<<<< HEAD # Ignore all binaries. bin/ # Ignore ie logs ie.log -======= -# Generated Files -bin ->>>>>>> main From 3a3f7d8c7f3f622ff664b50f2fce13b5ecdbbae1 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 29 Sep 2023 13:52:13 -0700 Subject: [PATCH 199/226] [update] workflow to run on prs into main --- .github/workflows/scenario-testing.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/scenario-testing.yaml b/.github/workflows/scenario-testing.yaml index c7d84597..9d3e1807 100644 --- a/.github/workflows/scenario-testing.yaml +++ b/.github/workflows/scenario-testing.yaml @@ -6,6 +6,9 @@ on: push: branches: - main + pull_request: + branches: + - main jobs: test-ocd-scenarios: From 053668ff734e083386539c2dcb77eb8e21486073 Mon Sep 17 00:00:00 2001 From: vmarcella Date: Fri, 29 Sep 2023 13:53:37 -0700 Subject: [PATCH 200/226] [remove] branch from release pipeline. --- .github/workflows/build-test-release.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build-test-release.yaml b/.github/workflows/build-test-release.yaml index 38dd4b2c..e099709b 100644 --- a/.github/workflows/build-test-release.yaml +++ b/.github/workflows/build-test-release.yaml @@ -4,7 +4,6 @@ on: push: branches: - main - - vmarcella/port-to-go jobs: build-test-release: From eb03a26918fb357313e686d0f21337afcadcc2bc Mon Sep 17 00:00:00 2001 From: Vincenzo Marcella <6026326+vmarcella@users.noreply.github.com> Date: Fri, 13 Oct 2023 10:33:27 -0700 Subject: [PATCH 201/226] Collection of small fixes for the engine (#68) * [remove] unused login. * [update] implementation for switching directories to be abstracted into a fs function. * [update] error handling for UsingDirectory so the original directory always tries to be restored. Remove unused variables. * [refactor] cursor manipulations out of engine and add unit tests. * [add] curl to AKS scenario back into steps. * [add] command rendering for scenarios. * [update] names of errors and functions. * [refactor] the engine. * [format] all files. * [refactor] the azure environment code to maintain support for the legacy environment flag. * [update] formatting & echo the web server url at end of aks scenario. --- Makefile | 2 +- cmd/ie/commands/execute.go | 29 ++- cmd/ie/commands/root.go | 10 +- cmd/ie/commands/test.go | 12 +- cmd/ie/commands/to-bash.go | 35 ++- internal/az/account.go | 28 +- internal/az/group.go | 40 +++ internal/engine/common.go | 59 ++--- internal/engine/engine.go | 68 ++--- internal/engine/environments/azure.go | 100 +++++++ internal/engine/environments/environments.go | 22 ++ internal/engine/execution.go | 259 ++++++++----------- internal/engine/execution_test.go | 41 --- internal/engine/scenario.go | 20 +- internal/engine/testing.go | 54 ++-- internal/lib/diff.go | 4 +- internal/lib/fs/directories.go | 20 ++ internal/lib/json.go | 6 +- internal/lib/user.go | 2 +- internal/ocd/status.go | 50 ---- internal/parsers/ini.go | 3 +- internal/parsers/markdown.go | 6 +- internal/parsers/markdown_test.go | 6 +- internal/patterns/regex.go | 18 ++ internal/patterns/regex_test.go | 42 +++ internal/shells/bash.go | 6 +- internal/terminal/cursor.go | 32 +++ internal/terminal/cursor_test.go | 30 +++ internal/ui/text.go | 24 ++ scenarios/ocd/CreateAKSDeployment/README.md | 144 +++++------ 30 files changed, 681 insertions(+), 491 deletions(-) create mode 100644 internal/az/group.go create mode 100644 internal/engine/environments/azure.go create mode 100644 internal/engine/environments/environments.go delete mode 100644 internal/ocd/status.go create mode 100644 internal/patterns/regex.go create mode 100644 internal/patterns/regex_test.go create mode 100644 internal/terminal/cursor.go create mode 100644 internal/terminal/cursor_test.go create mode 100644 internal/ui/text.go diff --git a/Makefile b/Makefile index a192c177..7f58ce47 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: build-ie build-api build-all run-ie run-api clean test-all +.PHONY: build-ie build-api build-all run-ie run-api clean test-all test all BINARY_DIR := bin IE_BINARY := $(BINARY_DIR)/ie diff --git a/cmd/ie/commands/execute.go b/cmd/ie/commands/execute.go index 613bdeaa..65a7838c 100644 --- a/cmd/ie/commands/execute.go +++ b/cmd/ie/commands/execute.go @@ -15,16 +15,22 @@ func init() { rootCommand.AddCommand(executeCommand) // Bool flags - executeCommand.PersistentFlags().Bool("verbose", false, "Enable verbose logging & standard output.") - executeCommand.PersistentFlags().Bool("do-not-delete", false, "Do not delete the Azure resources created by the Azure CLI commands executed.") + executeCommand.PersistentFlags(). + Bool("verbose", false, "Enable verbose logging & standard output.") + executeCommand.PersistentFlags(). + Bool("do-not-delete", false, "Do not delete the Azure resources created by the Azure CLI commands executed.") // String flags - executeCommand.PersistentFlags().String("correlation-id", "", "Adds a correlation ID to the user agent used by a scenarios azure-cli commands.") - executeCommand.PersistentFlags().String("subscription", "", "Sets the subscription ID used by a scenarios azure-cli commands. Will rely on the default subscription if not set.") - executeCommand.PersistentFlags().String("working-directory", ".", "Sets the working directory for innovation engine to operate out of. Restores the current working directory when finished.") + executeCommand.PersistentFlags(). + String("correlation-id", "", "Adds a correlation ID to the user agent used by a scenarios azure-cli commands.") + executeCommand.PersistentFlags(). + String("subscription", "", "Sets the subscription ID used by a scenarios azure-cli commands. Will rely on the default subscription if not set.") + executeCommand.PersistentFlags(). + String("working-directory", ".", "Sets the working directory for innovation engine to operate out of. Restores the current working directory when finished.") // StringArray flags - executeCommand.PersistentFlags().StringArray("var", []string{}, "Sets an environment variable for the scenario. Format: --var =") + executeCommand.PersistentFlags(). + StringArray("var", []string{}, "Sets an environment variable for the scenario. Format: --var =") } var executeCommand = &cobra.Command{ @@ -54,7 +60,10 @@ var executeCommand = &cobra.Command{ for _, environmentVariable := range environmentVariables { keyValuePair := strings.SplitN(environmentVariable, "=", 2) if len(keyValuePair) != 2 { - logging.GlobalLogger.Errorf("Error: Invalid environment variable format: %s", environmentVariable) + logging.GlobalLogger.Errorf( + "Error: Invalid environment variable format: %s", + environmentVariable, + ) fmt.Printf("Error: Invalid environment variable format: %s", environmentVariable) cmd.Help() os.Exit(1) @@ -64,7 +73,11 @@ var executeCommand = &cobra.Command{ } // Parse the markdown file and create a scenario - scenario, err := engine.CreateScenarioFromMarkdown(markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}, cliEnvironmentVariables) + scenario, err := engine.CreateScenarioFromMarkdown( + markdownFile, + []string{"bash", "azurecli", "azurecli-interactive", "terraform"}, + cliEnvironmentVariables, + ) if err != nil { logging.GlobalLogger.Errorf("Error creating scenario: %s", err) fmt.Printf("Error creating scenario: %s", err) diff --git a/cmd/ie/commands/root.go b/cmd/ie/commands/root.go index de20e378..ba37e55a 100644 --- a/cmd/ie/commands/root.go +++ b/cmd/ie/commands/root.go @@ -4,7 +4,7 @@ import ( "fmt" "os" - "github.com/Azure/InnovationEngine/internal/engine" + "github.com/Azure/InnovationEngine/internal/engine/environments" "github.com/Azure/InnovationEngine/internal/logging" "github.com/spf13/cobra" ) @@ -30,7 +30,7 @@ var rootCommand = &cobra.Command{ os.Exit(1) } - if !engine.IsValidEnvironment(environment) { + if !environments.IsValidEnvironment(environment) { fmt.Printf("Invalid environment: %s", environment) logging.GlobalLogger.Errorf("Invalid environment: %s", err) os.Exit(1) @@ -40,8 +40,10 @@ var rootCommand = &cobra.Command{ // Entrypoint into the Innovation Engine CLI. func ExecuteCLI() { - rootCommand.PersistentFlags().String("log-level", string(logging.Debug), "Configure the log level") - rootCommand.PersistentFlags().String("environment", engine.EnvironmentsLocal, "The environment that the CLI is running in. (local, ci, ocd)") + rootCommand.PersistentFlags(). + String("log-level", string(logging.Debug), "Configure the log level") + rootCommand.PersistentFlags(). + String("environment", environments.EnvironmentsLocal, "The environment that the CLI is running in. (local, ci, ocd)") if err := rootCommand.Execute(); err != nil { fmt.Println(err) diff --git a/cmd/ie/commands/test.go b/cmd/ie/commands/test.go index 16c287fb..f1ddcd8c 100644 --- a/cmd/ie/commands/test.go +++ b/cmd/ie/commands/test.go @@ -12,8 +12,10 @@ import ( // / Register the command with our command runner. func init() { rootCommand.AddCommand(testCommand) - testCommand.PersistentFlags().Bool("verbose", false, "Enable verbose logging & standard output.") - testCommand.PersistentFlags().String("subscription", "", "Sets the subscription ID used by a scenarios azure-cli commands. Will rely on the default subscription if not set.") + testCommand.PersistentFlags(). + Bool("verbose", false, "Enable verbose logging & standard output.") + testCommand.PersistentFlags(). + String("subscription", "", "Sets the subscription ID used by a scenarios azure-cli commands. Will rely on the default subscription if not set.") } var testCommand = &cobra.Command{ @@ -44,7 +46,11 @@ var testCommand = &cobra.Command{ os.Exit(1) } - scenario, err := engine.CreateScenarioFromMarkdown(markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}, nil) + scenario, err := engine.CreateScenarioFromMarkdown( + markdownFile, + []string{"bash", "azurecli", "azurecli-interactive", "terraform"}, + nil, + ) if err != nil { logging.GlobalLogger.Errorf("Error creating scenario %s", err) fmt.Printf("Error creating engine %s", err) diff --git a/cmd/ie/commands/to-bash.go b/cmd/ie/commands/to-bash.go index 044aad65..248e1427 100644 --- a/cmd/ie/commands/to-bash.go +++ b/cmd/ie/commands/to-bash.go @@ -2,28 +2,28 @@ package commands import ( "encoding/json" + "errors" "fmt" - "os" "strings" "github.com/Azure/InnovationEngine/internal/engine" + "github.com/Azure/InnovationEngine/internal/engine/environments" "github.com/Azure/InnovationEngine/internal/logging" "github.com/spf13/cobra" ) -type OcdScript struct { +type AzureScript struct { Script string `json:"script"` } var toBashCommand = &cobra.Command{ Use: "to-bash", Short: "Convert a markdown scenario into a bash script.", - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { markdownFile := args[0] if markdownFile == "" { logging.GlobalLogger.Errorf("Error: No markdown file specified.") - cmd.Help() - os.Exit(1) + return errors.New("error: No markdown file specified") } environment, _ := cmd.Flags().GetString("environment") @@ -34,10 +34,16 @@ var toBashCommand = &cobra.Command{ for _, environmentVariable := range environmentVariables { keyValuePair := strings.SplitN(environmentVariable, "=", 2) if len(keyValuePair) != 2 { - logging.GlobalLogger.Errorf("Error: Invalid environment variable format: %s", environmentVariable) + logging.GlobalLogger.Errorf( + "Error: Invalid environment variable format: %s", + environmentVariable, + ) fmt.Printf("Error: Invalid environment variable format: %s", environmentVariable) cmd.Help() - os.Exit(1) + return fmt.Errorf( + "error: Invalid environment variable format, %s", + environmentVariable, + ) } cliEnvironmentVariables[keyValuePair[0]] = keyValuePair[1] @@ -52,17 +58,19 @@ var toBashCommand = &cobra.Command{ if err != nil { logging.GlobalLogger.Errorf("Error creating scenario: %s", err) fmt.Printf("Error creating scenario: %s", err) - os.Exit(0) + return err } - if environment == "ocd" { - script := OcdScript{Script: scenario.ToShellScript()} + // If within cloudshell, we need to wrap the script in a json object to + // communicate it to the portal. + if environments.IsAzureEnvironment(environment) { + script := AzureScript{Script: scenario.ToShellScript()} scriptJson, err := json.Marshal(script) if err != nil { logging.GlobalLogger.Errorf("Error converting to json: %s", err) fmt.Printf("Error converting to json: %s", err) - os.Exit(1) + return err } fmt.Printf("ie_us%sie_ue\n", scriptJson) @@ -70,10 +78,13 @@ var toBashCommand = &cobra.Command{ fmt.Printf("%s", scenario.ToShellScript()) } + return nil + }, } func init() { rootCommand.AddCommand(toBashCommand) - toBashCommand.PersistentFlags().StringArray("var", []string{}, "Sets an environment variable for the scenario. Format: --var =") + toBashCommand.PersistentFlags(). + StringArray("var", []string{}, "Sets an environment variable for the scenario. Format: --var =") } diff --git a/internal/az/account.go b/internal/az/account.go index 0c11fb61..25b6d443 100644 --- a/internal/az/account.go +++ b/internal/az/account.go @@ -7,28 +7,18 @@ import ( "github.com/Azure/InnovationEngine/internal/shells" ) -func LoginWithMSI() error { - // Login - command := "az login --identity" - logging.GlobalLogger.Info("Logging into the azure cli.") - output, err := shells.ExecuteBashCommand(command, shells.BashCommandConfiguration{EnvironmentVariables: map[string]string{}, InteractiveCommand: false, WriteToHistory: false, InheritEnvironment: true}) - - logging.GlobalLogger.Debugf("Login stdout: %s", output.StdOut) - logging.GlobalLogger.Debugf("Login stderr: %s", output.StdErr) - - if err != nil { - logging.GlobalLogger.Errorf("Failed to login %s", err) - return err - } - - logging.GlobalLogger.Info("Login successful.") - return nil -} - func SetSubscription(subscription string) error { if subscription != "" { command := fmt.Sprintf("az account set --subscription %s", subscription) - _, err := shells.ExecuteBashCommand(command, shells.BashCommandConfiguration{EnvironmentVariables: map[string]string{}, InteractiveCommand: false, WriteToHistory: false, InheritEnvironment: false}) + _, err := shells.ExecuteBashCommand( + command, + shells.BashCommandConfiguration{ + EnvironmentVariables: map[string]string{}, + InteractiveCommand: false, + WriteToHistory: false, + InheritEnvironment: false, + }, + ) if err != nil { logging.GlobalLogger.Errorf("Failed to set subscription: %s", err) diff --git a/internal/az/group.go b/internal/az/group.go new file mode 100644 index 00000000..ae602317 --- /dev/null +++ b/internal/az/group.go @@ -0,0 +1,40 @@ +package az + +import ( + "github.com/Azure/InnovationEngine/internal/logging" + "github.com/Azure/InnovationEngine/internal/patterns" + "github.com/Azure/InnovationEngine/internal/shells" +) + +// Find all the deployed resources in a resource group. +func FindAllDeployedResourceURIs(resourceGroup string) []string { + output, err := shells.ExecuteBashCommand( + "az resource list -g"+resourceGroup, + shells.BashCommandConfiguration{ + EnvironmentVariables: map[string]string{}, + InheritEnvironment: true, + InteractiveCommand: false, + WriteToHistory: true, + }, + ) + + if err != nil { + logging.GlobalLogger.Error("Failed to list deployments", err) + } + + matches := patterns.AzResourceURI.FindAllStringSubmatch(output.StdOut, -1) + results := []string{} + for _, match := range matches { + results = append(results, match[1]) + } + return results +} + +// Find the resource group name from the output of an az command. +func FindResourceGroupName(commandOutput string) string { + matches := patterns.AzCommand.FindStringSubmatch(commandOutput) + if len(matches) > 1 { + return matches[1] + } + return "" +} diff --git a/internal/engine/common.go b/internal/engine/common.go index 6a542f9c..23e75d8d 100644 --- a/internal/engine/common.go +++ b/internal/engine/common.go @@ -6,42 +6,10 @@ import ( "github.com/Azure/InnovationEngine/internal/lib" "github.com/Azure/InnovationEngine/internal/logging" - "github.com/charmbracelet/lipgloss" + "github.com/Azure/InnovationEngine/internal/ui" "github.com/xrash/smetrics" ) -// Styles used for rendering output to the terminal. -var ( - scenarioTitleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Align(lipgloss.Center).Bold(true).Underline(true) - stepTitleStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#518BAD")).Align(lipgloss.Left).Bold(true) - spinnerStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#518BAD")) - verboseStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#437684")).Align(lipgloss.Left) - checkStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#32CD32")) - errorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF0000")) - errorMessageStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")) - ocdStatusUpdateStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#000000")) -) - -// Hides the cursor from the CLI using ANSI escape codes. -func hideCursor() { - fmt.Print("\033[?25l") -} - -// Displays the cursor in the CLI using ANSI escape codes. -func showCursor() { - fmt.Print("\033[?25h") -} - -// Moves the cursor up a specified number of lines. -func moveCursorPositionUp(lines int) { - fmt.Printf("\033[%dA", lines) -} - -// Moves the cursor down a specified number of lines. -func moveCursorPositionDown(lines int) { - fmt.Printf("\033[%dB\n", lines) -} - // Indents a multi-line command to be nested under the first line of the // command. func indentMultiLineCommand(content string, indentation int) string { @@ -58,9 +26,18 @@ func indentMultiLineCommand(content string, indentation int) string { } // Compares the actual output of a command to the expected output of a command. -func compareCommandOutputs(actualOutput string, expectedOutput string, expectedSimilarity float64, expectedOutputLanguage string) error { +func compareCommandOutputs( + actualOutput string, + expectedOutput string, + expectedSimilarity float64, + expectedOutputLanguage string, +) error { if strings.ToLower(expectedOutputLanguage) == "json" { - logging.GlobalLogger.Debugf("Comparing JSON strings:\nExpected: %s\nActual%s", expectedOutput, actualOutput) + logging.GlobalLogger.Debugf( + "Comparing JSON strings:\nExpected: %s\nActual%s", + expectedOutput, + actualOutput, + ) results, err := lib.CompareJsonStrings(actualOutput, expectedOutput, expectedSimilarity) if err != nil { @@ -68,15 +45,21 @@ func compareCommandOutputs(actualOutput string, expectedOutput string, expectedS } if !results.AboveThreshold { - return fmt.Errorf(errorMessageStyle.Render("Expected output does not match actual output.")) + return fmt.Errorf( + ui.ErrorMessageStyle.Render("Expected output does not match actual output."), + ) } - logging.GlobalLogger.Debugf("Expected Similarity: %f, Actual Similarity: %f", expectedSimilarity, results.Score) + logging.GlobalLogger.Debugf( + "Expected Similarity: %f, Actual Similarity: %f", + expectedSimilarity, + results.Score, + ) } else { score := smetrics.JaroWinkler(expectedOutput, actualOutput, 0.7, 4) if expectedSimilarity > score { - return fmt.Errorf(errorMessageStyle.Render("Expected output does not match actual output.")) + return fmt.Errorf(ui.ErrorMessageStyle.Render("Expected output does not match actual output.")) } } diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 287c539a..1990186e 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -2,30 +2,15 @@ package engine import ( "fmt" - "os" "github.com/Azure/InnovationEngine/internal/az" "github.com/Azure/InnovationEngine/internal/lib" "github.com/Azure/InnovationEngine/internal/lib/fs" "github.com/Azure/InnovationEngine/internal/logging" - "github.com/charmbracelet/lipgloss" + "github.com/Azure/InnovationEngine/internal/ui" ) -var ( - scriptHeader = lipgloss.NewStyle().Foreground(lipgloss.Color("#6CB6FF")).Bold(true) - scriptText = lipgloss.NewStyle().Foreground(lipgloss.Color("#FFFFFF")) -) - -const ( - EnvironmentsLocal = "local" - EnvironmentsCI = "ci" - EnvironmentsOCD = "ocd" -) - -func IsValidEnvironment(environment string) bool { - return environment == EnvironmentsLocal || environment == EnvironmentsCI || environment == EnvironmentsOCD -} - +// Configuration for the engine. type EngineConfiguration struct { Verbose bool DoNotDelete bool @@ -52,47 +37,26 @@ func NewEngine(configuration EngineConfiguration) (*Engine, error) { }, nil } -type AzureTokens struct { - Tokens []string `json:"tokens"` -} - // Executes a deployment scenario. func (e *Engine) ExecuteScenario(scenario *Scenario) error { - // Store the current directory so we can restore it later - originalDirectory, err := os.Getwd() - if err != nil { - logging.GlobalLogger.Error("Failed to get current directory", err) - return err - } + return fs.UsingDirectory(e.Configuration.WorkingDirectory, func() error { + az.SetCorrelationId(e.Configuration.CorrelationId, scenario.Environment) - fs.SetWorkingDirectory(e.Configuration.WorkingDirectory) - az.SetCorrelationId(e.Configuration.CorrelationId, scenario.Environment) - - // Execute the steps - fmt.Println(scenarioTitleStyle.Render(scenario.Name)) - e.ExecuteAndRenderSteps(scenario.Steps, lib.CopyMap(scenario.Environment)) - fmt.Printf(scriptHeader.Render("# Generated bash to replicate what just happened:")+"\n%s\n", scriptText.Render(scenario.ToShellScript())) - - fs.SetWorkingDirectory(originalDirectory) - - return nil + // Execute the steps + fmt.Println(ui.ScenarioTitleStyle.Render(scenario.Name)) + err := e.ExecuteAndRenderSteps(scenario.Steps, lib.CopyMap(scenario.Environment)) + return err + }) } // Validates a deployment scenario. func (e *Engine) TestScenario(scenario *Scenario) error { - // Store the current directory so we can restore it later - originalDirectory, err := os.Getwd() - if err != nil { - logging.GlobalLogger.Error("Failed to get current directory", err) - return err - } - - fs.SetWorkingDirectory(e.Configuration.WorkingDirectory) - az.SetCorrelationId(e.Configuration.CorrelationId, scenario.Environment) + return fs.UsingDirectory(e.Configuration.WorkingDirectory, func() error { + az.SetCorrelationId(e.Configuration.CorrelationId, scenario.Environment) - fmt.Println(scenarioTitleStyle.Render(scenario.Name)) - e.TestSteps(scenario.Steps, lib.CopyMap(scenario.Environment)) - - fs.SetWorkingDirectory(originalDirectory) - return nil + // Test the steps + fmt.Println(ui.ScenarioTitleStyle.Render(scenario.Name)) + err := e.TestSteps(scenario.Steps, lib.CopyMap(scenario.Environment)) + return err + }) } diff --git a/internal/engine/environments/azure.go b/internal/engine/environments/azure.go new file mode 100644 index 00000000..0526abdd --- /dev/null +++ b/internal/engine/environments/azure.go @@ -0,0 +1,100 @@ +package environments + +import ( + "encoding/json" + "fmt" + + "github.com/Azure/InnovationEngine/internal/az" + "github.com/Azure/InnovationEngine/internal/logging" + "github.com/Azure/InnovationEngine/internal/ui" +) + +// / The status of a one-click deployment. +type AzureDeploymentStatus struct { + Steps []string `json:"steps"` + CurrentStep int `json:"currentStep"` + Status string `json:"status"` + ResourceURIs []string `json:"resourceURIs"` + Error string `json:"error"` +} + +func NewAzureDeploymentStatus() AzureDeploymentStatus { + return AzureDeploymentStatus{ + Steps: []string{}, + CurrentStep: 0, + Status: "Executing", + ResourceURIs: []string{}, + Error: "", + } +} + +// Get the status as a JSON string. +func (status *AzureDeploymentStatus) AsJsonString() (string, error) { + json, err := json.Marshal(status) + if err != nil { + logging.GlobalLogger.Error("Failed to marshal status", err) + return "", err + } + + return string(json), nil +} + +func (status *AzureDeploymentStatus) AddStep(step string) { + status.Steps = append(status.Steps, step) +} + +func (status *AzureDeploymentStatus) AddResourceURI(uri string) { + status.ResourceURIs = append(status.ResourceURIs, uri) +} + +func (status *AzureDeploymentStatus) SetError(err error) { + status.Status = "Failed" + status.Error = err.Error() +} + +// Print out the status JSON for azure/cloudshell if in the correct environment. +func ReportAzureStatus(status AzureDeploymentStatus, environment string) { + if !IsAzureEnvironment(environment) { + return + } + + statusJson, err := status.AsJsonString() + if err != nil { + logging.GlobalLogger.Error("Failed to marshal status", err) + } else { + // We add these strings to the output so that the portal can find and parse + // the JSON status. + ocdStatus := fmt.Sprintf("ie_us%sie_ue\n", statusJson) + fmt.Println(ui.OcdStatusUpdateStyle.Render(ocdStatus)) + } +} + +// Attach deployed resource URIs to the one click deployment status if we're in +// the correct environment & we have a resource group name. +func AttachResourceURIsToAzureStatus( + status *AzureDeploymentStatus, + resourceGroupName string, + environment string, +) { + + if !IsAzureEnvironment(environment) { + logging.GlobalLogger.Info( + "Not fetching resource URIs because we're not in the OCD environment.", + ) + } + + if resourceGroupName == "" { + logging.GlobalLogger.Warn("No resource group name found.") + return + } + + resourceURIs := az.FindAllDeployedResourceURIs(resourceGroupName) + + if len(resourceURIs) > 0 { + logging.GlobalLogger.WithField("resourceURIs", resourceURIs). + Info("Found deployed resources.") + status.ResourceURIs = resourceURIs + } else { + logging.GlobalLogger.Warn("No deployed resources found.") + } +} diff --git a/internal/engine/environments/environments.go b/internal/engine/environments/environments.go new file mode 100644 index 00000000..c6c9dc45 --- /dev/null +++ b/internal/engine/environments/environments.go @@ -0,0 +1,22 @@ +package environments + +const ( + EnvironmentsLocal = "local" + EnvironmentsCI = "ci" + EnvironmentsOCD = "ocd" + EnvironmentsAzure = "azure" +) + +// Check if the environment is valid. +func IsValidEnvironment(environment string) bool { + switch environment { + case EnvironmentsLocal, EnvironmentsCI, EnvironmentsOCD, EnvironmentsAzure: + return true + default: + return false + } +} + +func IsAzureEnvironment(environment string) bool { + return environment == EnvironmentsAzure || environment == EnvironmentsOCD +} diff --git a/internal/engine/execution.go b/internal/engine/execution.go index d5f441e4..05c756e2 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -2,16 +2,18 @@ package engine import ( "fmt" - "os" - "regexp" "strings" "time" + "github.com/Azure/InnovationEngine/internal/az" + "github.com/Azure/InnovationEngine/internal/engine/environments" "github.com/Azure/InnovationEngine/internal/lib" "github.com/Azure/InnovationEngine/internal/logging" - "github.com/Azure/InnovationEngine/internal/ocd" "github.com/Azure/InnovationEngine/internal/parsers" + "github.com/Azure/InnovationEngine/internal/patterns" "github.com/Azure/InnovationEngine/internal/shells" + "github.com/Azure/InnovationEngine/internal/terminal" + "github.com/Azure/InnovationEngine/internal/ui" ) const ( @@ -21,19 +23,6 @@ const ( spinnerRefresh = 100 * time.Millisecond ) -var ( - // An SSH command regex where there must be a username@host somewhere present in the command. - sshCommand = regexp.MustCompile(`(^|\s)\bssh\b\s+([^\s]+(\s+|$))+((?P[a-zA-Z0-9_-]+|\$[A-Z_0-9]+)@(?P[a-zA-Z0-9.-]+|\$[A-Z_0-9]+))`) - - // Az cli command regex - azCommand = regexp.MustCompile(`az\s+([a-z]+)\s+([a-z]+)`) - azGroupDelete = regexp.MustCompile(`az group delete`) - - // ARM regex - azResourceURI = regexp.MustCompile(`\"id\": \"(/subscriptions/[^\"]+)\"`) - azResourceGroupName = regexp.MustCompile(`resourceGroups/([^\"]+)`) -) - // If a scenario has an `az group delete` command and the `--do-not-delete` // flag is set, we remove it from the steps. func filterDeletionCommands(steps []Step, preserveResources bool) []Step { @@ -42,7 +31,7 @@ func filterDeletionCommands(steps []Step, preserveResources bool) []Step { for _, step := range steps { newBlocks := []parsers.CodeBlock{} for _, block := range step.CodeBlocks { - if azGroupDelete.MatchString(block.Content) { + if patterns.AzGroupDelete.MatchString(block.Content) { continue } else { newBlocks = append(newBlocks, block) @@ -61,95 +50,44 @@ func filterDeletionCommands(steps []Step, preserveResources bool) []Step { return filteredSteps } -// Print out the one click deployment status if in the correct environment. -func reportOCDStatus(status ocd.OneClickDeploymentStatus, environment string) { - if environment != EnvironmentsOCD { - return - } - - statusJson, err := status.AsJsonString() - if err != nil { - logging.GlobalLogger.Error("Failed to marshal status", err) - } else { - // We add these strings to the output so that the portal can find and parse - // the JSON status. - ocdStatus := fmt.Sprintf("ie_us%sie_ue\n", statusJson) - fmt.Println(ocdStatusUpdateStyle.Render(ocdStatus)) - } -} - -// Attach deployed resource URIs to the one click deployment status if we're in -// the correct environment & we have a resource group name. -func attachResourceURIsToOCDStatus(status *ocd.OneClickDeploymentStatus, resourceGroupName string, environment string) { - - if environment != EnvironmentsOCD { - logging.GlobalLogger.Info("Not fetching resource URIs because we're not in the OCD environment.") - return - } - - if resourceGroupName == "" { - logging.GlobalLogger.Warn("No resource group name found.") - return - } - - resourceURIs := findAllDeployedResourceURIs(resourceGroupName) - - if len(resourceURIs) > 0 { - logging.GlobalLogger.WithField("resourceURIs", resourceURIs).Info("Found deployed resources.") - status.ResourceURIs = resourceURIs - } else { - logging.GlobalLogger.Warn("No deployed resources found.") - } -} - -// Find the resource group name from the output of an az command. -func findResourceGroupName(commandOutput string) string { - matches := azResourceGroupName.FindStringSubmatch(commandOutput) - if len(matches) > 1 { - return matches[1] - } - return "" -} - -// Find all the deployed resources in a resource group. -func findAllDeployedResourceURIs(resourceGroup string) []string { - output, err := shells.ExecuteBashCommand("az resource list -g"+resourceGroup, shells.BashCommandConfiguration{EnvironmentVariables: map[string]string{}, InheritEnvironment: true, InteractiveCommand: false, WriteToHistory: true}) - - if err != nil { - logging.GlobalLogger.Error("Failed to list deployments", err) - } - - matches := azResourceURI.FindAllStringSubmatch(output.StdOut, -1) - results := []string{} - for _, match := range matches { - results = append(results, match[1]) - } - return results -} - // Executes the steps from a scenario and renders the output to the terminal. -func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { +func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) error { var resourceGroupName string - var ocdStatus = ocd.NewOneClickDeploymentStatus() + var azureStatus = environments.NewAzureDeploymentStatus() stepsToExecute := filterDeletionCommands(steps, e.Configuration.DoNotDelete) for stepNumber, step := range stepsToExecute { - ocdStatus.AddStep(fmt.Sprintf("%d. %s", stepNumber+1, step.Name)) + azureStatus.AddStep(fmt.Sprintf("%d. %s", stepNumber+1, step.Name)) } - reportOCDStatus(ocdStatus, e.Configuration.Environment) + environments.ReportAzureStatus(azureStatus, e.Configuration.Environment) for stepNumber, step := range stepsToExecute { stepTitle := fmt.Sprintf("%d. %s\n", stepNumber+1, step.Name) - fmt.Println(stepTitleStyle.Render(stepTitle)) - ocdStatus.CurrentStep = stepNumber + 1 + fmt.Println(ui.StepTitleStyle.Render(stepTitle)) + azureStatus.CurrentStep = stepNumber + 1 for _, block := range step.CodeBlocks { // Render the codeblock. - indentedBlock := indentMultiLineCommand(block.Content, 4) - fmt.Print(" " + indentedBlock) + escapedCommand := strings.ReplaceAll(block.Content, "\\\n", "\\\\\n") + renderedCommand, err := shells.ExecuteBashCommand( + "echo -e \""+escapedCommand+"\"", + shells.BashCommandConfiguration{ + EnvironmentVariables: map[string]string{}, + InteractiveCommand: false, + WriteToHistory: false, + InheritEnvironment: true, + }, + ) + if err != nil { + logging.GlobalLogger.Errorf("Failed to render command: %s", err.Error()) + return err + } + finalCommandOutput := indentMultiLineCommand(renderedCommand.StdOut, 4) + + fmt.Print(" " + finalCommandOutput) // execute the command as a goroutine to allow for the spinner to be // rendered while the command is executing. @@ -159,11 +97,12 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { // If the command is an SSH command, we need to forward the input and // output interactiveCommand := false - if sshCommand.MatchString(block.Content) { + if patterns.SshCommand.MatchString(block.Content) { interactiveCommand = true } - logging.GlobalLogger.WithField("isInteractive", interactiveCommand).Infof("Executing command: %s", block.Content) + logging.GlobalLogger.WithField("isInteractive", interactiveCommand). + Infof("Executing command: %s", block.Content) var commandErr error var frame int = 0 @@ -172,15 +111,24 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { if !interactiveCommand { // Grab the number of lines it contains & set the cursor to the // beginning of the block. - lines := strings.Count(block.Content, "\n") - moveCursorPositionUp(lines) + + lines := strings.Count(finalCommandOutput, "\n") + terminal.MoveCursorPositionUp(lines) // Render the spinner and hide the cursor. - fmt.Print(spinnerStyle.Render(" "+string(spinnerFrames[0])) + " ") - hideCursor() + fmt.Print(ui.SpinnerStyle.Render(" "+string(spinnerFrames[0])) + " ") + terminal.HideCursor() go func(block parsers.CodeBlock) { - output, err := shells.ExecuteBashCommand(block.Content, shells.BashCommandConfiguration{EnvironmentVariables: lib.CopyMap(env), InheritEnvironment: true, InteractiveCommand: false, WriteToHistory: true}) + output, err := shells.ExecuteBashCommand( + block.Content, + shells.BashCommandConfiguration{ + EnvironmentVariables: lib.CopyMap(env), + InheritEnvironment: true, + InteractiveCommand: false, + WriteToHistory: true, + }, + ) logging.GlobalLogger.Infof("Command output to stdout:\n %s", output.StdOut) logging.GlobalLogger.Infof("Command output to stderr:\n %s", output.StdErr) commandOutput = output @@ -193,7 +141,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { case commandErr = <-done: // Show the cursor, check the result of the command, and display the // final status. - showCursor() + terminal.ShowCursor() if commandErr == nil { @@ -202,30 +150,30 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { expectedSimilarity := block.ExpectedOutput.ExpectedSimilarity expectedOutputLanguage := block.ExpectedOutput.Language - err := compareCommandOutputs(actualOutput, expectedOutput, expectedSimilarity, expectedOutputLanguage) + outputComparisonError := compareCommandOutputs(actualOutput, expectedOutput, expectedSimilarity, expectedOutputLanguage) - if err != nil { - logging.GlobalLogger.Errorf("Error comparing command outputs: %s", err.Error()) - fmt.Printf("\r %s \n", errorStyle.Render("✗")) - moveCursorPositionDown(lines) - fmt.Printf(" %s\n", errorMessageStyle.Render(err.Error())) + if outputComparisonError != nil { + logging.GlobalLogger.Errorf("Error comparing command outputs: %s", outputComparisonError.Error()) + fmt.Printf("\r %s \n", ui.ErrorStyle.Render("✗")) + terminal.MoveCursorPositionDown(lines) + fmt.Printf(" %s\n", ui.ErrorMessageStyle.Render(outputComparisonError.Error())) fmt.Printf(" %s\n", lib.GetDifferenceBetweenStrings(block.ExpectedOutput.Content, commandOutput.StdOut)) - ocdStatus.SetError(err) - reportOCDStatus(ocdStatus, e.Configuration.Environment) + azureStatus.SetError(outputComparisonError) + environments.ReportAzureStatus(azureStatus, e.Configuration.Environment) - os.Exit(1) + return outputComparisonError } - fmt.Printf("\r %s \n", checkStyle.Render("✔")) - moveCursorPositionDown(lines) + fmt.Printf("\r %s \n", ui.CheckStyle.Render("✔")) + terminal.MoveCursorPositionDown(lines) - fmt.Printf(" %s\n", verboseStyle.Render(commandOutput.StdOut)) + fmt.Printf(" %s\n", ui.VerboseStyle.Render(commandOutput.StdOut)) // Extract the resource group name from the command output if // it's not already set. - if resourceGroupName == "" && azCommand.MatchString(block.Content) { - tmpResourceGroup := findResourceGroupName(commandOutput.StdOut) + if resourceGroupName == "" && patterns.AzCommand.MatchString(block.Content) { + tmpResourceGroup := az.FindResourceGroupName(commandOutput.StdOut) if tmpResourceGroup != "" { logging.GlobalLogger.WithField("resourceGroup", tmpResourceGroup).Info("Found resource group") resourceGroupName = tmpResourceGroup @@ -233,27 +181,27 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { } if stepNumber != len(stepsToExecute)-1 { - reportOCDStatus(ocdStatus, e.Configuration.Environment) + environments.ReportAzureStatus(azureStatus, e.Configuration.Environment) } } else { - showCursor() - fmt.Printf("\r %s \n", errorStyle.Render("✗")) - moveCursorPositionDown(lines) - fmt.Printf(" %s\n", errorMessageStyle.Render(commandErr.Error())) + terminal.ShowCursor() + fmt.Printf("\r %s \n", ui.ErrorStyle.Render("✗")) + terminal.MoveCursorPositionDown(lines) + fmt.Printf(" %s\n", ui.ErrorMessageStyle.Render(commandErr.Error())) logging.GlobalLogger.Errorf("Error executing command: %s", commandErr.Error()) - ocdStatus.SetError(commandErr) - reportOCDStatus(ocdStatus, e.Configuration.Environment) + azureStatus.SetError(commandErr) + environments.ReportAzureStatus(azureStatus, e.Configuration.Environment) - os.Exit(1) + return commandErr } break renderingLoop default: frame = (frame + 1) % len(spinnerFrames) - fmt.Printf("\r %s", spinnerStyle.Render(string(spinnerFrames[frame]))) + fmt.Printf("\r %s", ui.SpinnerStyle.Render(string(spinnerFrames[frame]))) time.Sleep(spinnerRefresh) } } @@ -263,44 +211,63 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) { // If we're on the last step and the command is an SSH command, we need // to report the status before executing the command. This is needed for // one click deployments and does not affect the normal execution flow. - if stepNumber == len(stepsToExecute)-1 && sshCommand.MatchString(block.Content) { - ocdStatus.Status = "Succeeded" - attachResourceURIsToOCDStatus(&ocdStatus, resourceGroupName, e.Configuration.Environment) - reportOCDStatus(ocdStatus, e.Configuration.Environment) + if stepNumber == len(stepsToExecute)-1 && patterns.SshCommand.MatchString(block.Content) { + azureStatus.Status = "Succeeded" + environments.AttachResourceURIsToAzureStatus(&azureStatus, resourceGroupName, e.Configuration.Environment) + environments.ReportAzureStatus(azureStatus, e.Configuration.Environment) } - output, err := shells.ExecuteBashCommand(block.Content, shells.BashCommandConfiguration{EnvironmentVariables: lib.CopyMap(env), InheritEnvironment: true, InteractiveCommand: true, WriteToHistory: false}) + output, commandExecutionError := shells.ExecuteBashCommand( + block.Content, + shells.BashCommandConfiguration{ + EnvironmentVariables: lib.CopyMap(env), + InheritEnvironment: true, + InteractiveCommand: true, + WriteToHistory: false, + }, + ) - if err == nil { - showCursor() - fmt.Printf("\r %s \n", checkStyle.Render("✔")) - moveCursorPositionDown(lines) + terminal.ShowCursor() - fmt.Printf(" %s\n", verboseStyle.Render(output.StdOut)) + if commandExecutionError == nil { + fmt.Printf("\r %s \n", ui.CheckStyle.Render("✔")) + terminal.MoveCursorPositionDown(lines) + + fmt.Printf(" %s\n", ui.VerboseStyle.Render(output.StdOut)) if stepNumber != len(stepsToExecute)-1 { - reportOCDStatus(ocdStatus, e.Configuration.Environment) + environments.ReportAzureStatus(azureStatus, e.Configuration.Environment) } } else { - showCursor() - fmt.Printf("\r %s \n", errorStyle.Render("✗")) - moveCursorPositionDown(lines) - fmt.Printf(" %s\n", errorMessageStyle.Render(err.Error())) - - ocdStatus.SetError(err) - reportOCDStatus(ocdStatus, e.Configuration.Environment) - os.Exit(1) + fmt.Printf("\r %s \n", ui.ErrorStyle.Render("✗")) + terminal.MoveCursorPositionDown(lines) + fmt.Printf(" %s\n", ui.ErrorMessageStyle.Render(commandExecutionError.Error())) + + azureStatus.SetError(commandExecutionError) + environments.ReportAzureStatus(azureStatus, e.Configuration.Environment) + return commandExecutionError } } } } // Report the final status of the deployment (Only applies to one click deployments). - ocdStatus.Status = "Succeeded" - attachResourceURIsToOCDStatus(&ocdStatus, resourceGroupName, e.Configuration.Environment) - reportOCDStatus(ocdStatus, e.Configuration.Environment) - - if e.Configuration.Environment != "ocd" { + azureStatus.Status = "Succeeded" + environments.AttachResourceURIsToAzureStatus( + &azureStatus, + resourceGroupName, + e.Configuration.Environment, + ) + environments.ReportAzureStatus(azureStatus, e.Configuration.Environment) + + switch e.Configuration.Environment { + case environments.EnvironmentsAzure, environments.EnvironmentsOCD: + logging.GlobalLogger.Info( + "Not resetting environment variable state to retain for cloudshell.", + ) + default: shells.ResetStoredEnvironmentVariables() } + + return nil } diff --git a/internal/engine/execution_test.go b/internal/engine/execution_test.go index b91627ed..00a22ef6 100644 --- a/internal/engine/execution_test.go +++ b/internal/engine/execution_test.go @@ -1,42 +1 @@ package engine - -import ( - "testing" -) - -func TestRegex(t *testing.T) { - - t.Run("Test ssh command regex", func(t *testing.T) { - testCases := []string{ - "Run ssh -i key.pem username@host to connect", - "ssh -p 22 -L 8080:localhost:8080 username@host", - "ssh -Y username@host", - "Use ssh to connect", - "sshusername@host is not correct", - " ssh username@domain.com", - "Invalid ssh username@@domain.com", - "ssh -o StrictHostKeyChecking=no $MY_USERNAME@$IP_ADDRESS", - } - - testResults := []bool{ - true, - true, - true, - false, - false, - false, - false, - true, - } - - for index, testCase := range testCases { - match := sshCommand.FindString(testCase) - if match == "" && testResults[index] { - t.Errorf("Expected match not found: %s\n", testCase) - } else if match != "" && !testResults[index] { - t.Errorf("Unexpected match found: %s\n", testCase) - } - } - }) - -} diff --git a/internal/engine/scenario.go b/internal/engine/scenario.go index cf7025f3..b847b51c 100644 --- a/internal/engine/scenario.go +++ b/internal/engine/scenario.go @@ -52,7 +52,11 @@ func groupCodeBlocksIntoSteps(blocks []parsers.CodeBlock) []Step { // Creates a scenario object from a given markdown file. languagesToExecute is // used to filter out code blocks that should not be parsed out of the markdown // file. -func CreateScenarioFromMarkdown(path string, languagesToExecute []string, environmentVariableOverrides map[string]string) (*Scenario, error) { +func CreateScenarioFromMarkdown( + path string, + languagesToExecute []string, + environmentVariableOverrides map[string]string, +) (*Scenario, error) { if !fs.FileExists(path) { return nil, fmt.Errorf("markdown file '%s' does not exist", path) } @@ -91,7 +95,8 @@ func CreateScenarioFromMarkdown(path string, languagesToExecute []string, enviro // Extract the code blocks from the markdown file. codeBlocks := parsers.ExtractCodeBlocksFromAst(markdown, source, languagesToExecute) - logging.GlobalLogger.WithField("CodeBlocks", codeBlocks).Debugf("Found %d code blocks", len(codeBlocks)) + logging.GlobalLogger.WithField("CodeBlocks", codeBlocks). + Debugf("Found %d code blocks", len(codeBlocks)) varsToExport := lib.CopyMap(environmentVariableOverrides) for key, value := range environmentVariableOverrides { @@ -102,7 +107,11 @@ func CreateScenarioFromMarkdown(path string, languagesToExecute []string, enviro matches := exportRegex.FindAllStringSubmatch(codeBlock.Content, -1) if len(matches) != 0 { - logging.GlobalLogger.Debugf("Found %d matches for %s, deleting from varsToExport", len(matches), key) + logging.GlobalLogger.Debugf( + "Found %d matches for %s, deleting from varsToExport", + len(matches), + key, + ) delete(varsToExport, key) } else { logging.GlobalLogger.Debugf("Found no matches for %s inside of %s", key, codeBlock.Content) @@ -127,7 +136,10 @@ func CreateScenarioFromMarkdown(path string, languagesToExecute []string, enviro // do not update the scenario // steps. if len(varsToExport) != 0 { - logging.GlobalLogger.Debugf("Found %d variables to add to the scenario as a step.", len(varsToExport)) + logging.GlobalLogger.Debugf( + "Found %d variables to add to the scenario as a step.", + len(varsToExport), + ) exportCodeBlock := parsers.CodeBlock{ Language: "bash", Content: "", diff --git a/internal/engine/testing.go b/internal/engine/testing.go index cb6e2d0b..06fd14e6 100644 --- a/internal/engine/testing.go +++ b/internal/engine/testing.go @@ -1,25 +1,31 @@ package engine import ( + "errors" "fmt" "time" + "github.com/Azure/InnovationEngine/internal/az" "github.com/Azure/InnovationEngine/internal/lib" "github.com/Azure/InnovationEngine/internal/logging" "github.com/Azure/InnovationEngine/internal/parsers" + "github.com/Azure/InnovationEngine/internal/patterns" "github.com/Azure/InnovationEngine/internal/shells" + "github.com/Azure/InnovationEngine/internal/terminal" + "github.com/Azure/InnovationEngine/internal/ui" ) -func (e *Engine) TestSteps(steps []Step, env map[string]string) { +func (e *Engine) TestSteps(steps []Step, env map[string]string) error { var resourceGroupName string stepsToExecute := filterDeletionCommands(steps, true) + var testRunnerError error = nil testRunner: for stepNumber, step := range stepsToExecute { stepTitle := fmt.Sprintf(" %d. %s\n", stepNumber+1, step.Name) - fmt.Println(stepTitleStyle.Render(stepTitle)) - moveCursorPositionUp(1) - hideCursor() + fmt.Println(ui.StepTitleStyle.Render(stepTitle)) + terminal.MoveCursorPositionUp(1) + terminal.HideCursor() for _, block := range step.CodeBlocks { // execute the command as a goroutine to allow for the spinner to be @@ -43,7 +49,7 @@ testRunner: for { select { case err = <-done: - showCursor() + terminal.ShowCursor() if err == nil { actualOutput := commandOutput.StdOut @@ -55,36 +61,37 @@ testRunner: if err != nil { logging.GlobalLogger.Errorf("Error comparing command outputs: %s", err.Error()) - fmt.Print(errorStyle.Render("Error when comparing the command outputs: %s\n", err.Error())) + fmt.Print(ui.ErrorStyle.Render("Error when comparing the command outputs: %s\n", err.Error())) } // Extract the resource group name from the command output if // it's not already set. - if resourceGroupName == "" && azCommand.MatchString(block.Content) { - tmpResourceGroup := findResourceGroupName(commandOutput.StdOut) + if resourceGroupName == "" && patterns.AzCommand.MatchString(block.Content) { + tmpResourceGroup := az.FindResourceGroupName(commandOutput.StdOut) if tmpResourceGroup != "" { logging.GlobalLogger.Infof("Found resource group: %s", tmpResourceGroup) resourceGroupName = tmpResourceGroup } } - fmt.Printf("\r %s \n", checkStyle.Render("✔")) - moveCursorPositionDown(1) + fmt.Printf("\r %s \n", ui.CheckStyle.Render("✔")) + terminal.MoveCursorPositionDown(1) } else { - fmt.Printf("\r %s \n", errorStyle.Render("✗")) - moveCursorPositionDown(1) - fmt.Printf(" %s\n", errorStyle.Render("Error executing command: %s\n", err.Error())) + fmt.Printf("\r %s \n", ui.ErrorStyle.Render("✗")) + terminal.MoveCursorPositionDown(1) + fmt.Printf(" %s\n", ui.ErrorStyle.Render("Error executing command: %s\n", err.Error())) logging.GlobalLogger.Errorf("Error executing command: %s", err.Error()) + testRunnerError = err break testRunner } break loop default: frame = (frame + 1) % len(spinnerFrames) - fmt.Printf("\r %s", spinnerStyle.Render(string(spinnerFrames[frame]))) + fmt.Printf("\r %s", ui.SpinnerStyle.Render(string(spinnerFrames[frame]))) time.Sleep(spinnerRefresh) } } @@ -96,14 +103,25 @@ testRunner: fmt.Printf("\n") fmt.Printf("Deleting resource group: %s\n", resourceGroupName) command := fmt.Sprintf("az group delete --name %s --yes", resourceGroupName) - output, err := shells.ExecuteBashCommand(command, shells.BashCommandConfiguration{EnvironmentVariables: lib.CopyMap(env), InheritEnvironment: true, InteractiveCommand: false, WriteToHistory: true}) + output, err := shells.ExecuteBashCommand( + command, + shells.BashCommandConfiguration{ + EnvironmentVariables: lib.CopyMap(env), + InheritEnvironment: true, + InteractiveCommand: false, + WriteToHistory: true, + }, + ) + if err != nil { - fmt.Print(errorStyle.Render("Error deleting resource group: %s\n", err.Error())) + fmt.Print(ui.ErrorStyle.Render("Error deleting resource group: %s\n", err.Error())) logging.GlobalLogger.Errorf("Error deleting resource group: %s", err.Error()) - } else { - fmt.Print(output.StdOut) + testRunnerError = errors.Join(testRunnerError, err) } + + fmt.Print(output.StdOut) } shells.ResetStoredEnvironmentVariables() + return testRunnerError } diff --git a/internal/lib/diff.go b/internal/lib/diff.go index 44745b69..d595b1bd 100644 --- a/internal/lib/diff.go +++ b/internal/lib/diff.go @@ -1,8 +1,6 @@ package lib import ( - "fmt" - "github.com/sergi/go-diff/diffmatchpatch" ) @@ -10,5 +8,5 @@ func GetDifferenceBetweenStrings(a, b string) string { dmp := diffmatchpatch.New() diffs := dmp.DiffMain(a, b, false) - return fmt.Sprintf("%s", dmp.DiffPrettyText(diffs)) + return dmp.DiffPrettyText(diffs) } diff --git a/internal/lib/fs/directories.go b/internal/lib/fs/directories.go index 89c8faed..f6211761 100644 --- a/internal/lib/fs/directories.go +++ b/internal/lib/fs/directories.go @@ -1,6 +1,7 @@ package fs import ( + "errors" "os" "github.com/Azure/InnovationEngine/internal/logging" @@ -19,3 +20,22 @@ func SetWorkingDirectory(directory string) error { } return nil } + +// Executes a function within a given working directory and restores +// the original working directory when the function completes. +func UsingDirectory(directory string, executor func() error) error { + originalDirectory, err := os.Getwd() + if err != nil { + return err + } + + err = SetWorkingDirectory(directory) + if err != nil { + return err + } + + executionError := executor() + err = SetWorkingDirectory(originalDirectory) + + return errors.Join(executionError, err) +} diff --git a/internal/lib/json.go b/internal/lib/json.go index d96cb835..a5c1f875 100644 --- a/internal/lib/json.go +++ b/internal/lib/json.go @@ -28,7 +28,11 @@ type ComparisonResult struct { // Compare two JSON strings by ordering the fields alphabetically and then // comparing the strings using the Jaro-Winkler algorithm to compute a score. // If the score is greater than the threshold, return true. -func CompareJsonStrings(actualJson string, expectedJson string, threshold float64) (ComparisonResult, error) { +func CompareJsonStrings( + actualJson string, + expectedJson string, + threshold float64, +) (ComparisonResult, error) { actualOutput, err := OrderJsonFields(actualJson) if err != nil { return ComparisonResult{}, err diff --git a/internal/lib/user.go b/internal/lib/user.go index e8589ae5..6066aaa9 100644 --- a/internal/lib/user.go +++ b/internal/lib/user.go @@ -26,5 +26,5 @@ func GetHomeDirectory() (string, error) { return homeDrive + homePath, nil } - return "", fmt.Errorf("Home directory cannot be determined") + return "", fmt.Errorf("home directory cannot be determined") } diff --git a/internal/ocd/status.go b/internal/ocd/status.go deleted file mode 100644 index babeffe5..00000000 --- a/internal/ocd/status.go +++ /dev/null @@ -1,50 +0,0 @@ -package ocd - -import ( - "encoding/json" - - "github.com/Azure/InnovationEngine/internal/logging" -) - -// / The status of a one-click deployment. -type OneClickDeploymentStatus struct { - Steps []string `json:"steps"` - CurrentStep int `json:"currentStep"` - Status string `json:"status"` - ResourceURIs []string `json:"resourceURIs"` - Error string `json:"error"` -} - -func NewOneClickDeploymentStatus() OneClickDeploymentStatus { - return OneClickDeploymentStatus{ - Steps: []string{}, - CurrentStep: 0, - Status: "Executing", - ResourceURIs: []string{}, - Error: "", - } -} - -// Get the status as a JSON string. -func (status *OneClickDeploymentStatus) AsJsonString() (string, error) { - json, err := json.Marshal(status) - if err != nil { - logging.GlobalLogger.Error("Failed to marshal status", err) - return "", err - } - - return string(json), nil -} - -func (status *OneClickDeploymentStatus) AddStep(step string) { - status.Steps = append(status.Steps, step) -} - -func (status *OneClickDeploymentStatus) AddResourceURI(uri string) { - status.ResourceURIs = append(status.ResourceURIs, uri) -} - -func (status *OneClickDeploymentStatus) SetError(err error) { - status.Status = "Failed" - status.Error = err.Error() -} diff --git a/internal/parsers/ini.go b/internal/parsers/ini.go index 4c30f31a..f4cb58ff 100644 --- a/internal/parsers/ini.go +++ b/internal/parsers/ini.go @@ -2,6 +2,7 @@ package parsers import ( "fmt" + "gopkg.in/ini.v1" ) @@ -13,7 +14,7 @@ func ParseINIFile(filePath string) (map[string]string, error) { iniFile, err := ini.Load(filePath) if err != nil { - return nil, fmt.Errorf("Failed to read the INI file %s because %v", filePath, err) + return nil, fmt.Errorf("failed to read the INI file %s because %v", filePath, err) } data := make(map[string]string) diff --git a/internal/parsers/markdown.go b/internal/parsers/markdown.go index 7246b701..d99ddb38 100644 --- a/internal/parsers/markdown.go +++ b/internal/parsers/markdown.go @@ -77,7 +77,11 @@ var expectedSimilarityRegex = regexp.MustCompile(` + ```JSON { "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup210", @@ -64,6 +65,7 @@ az network vnet create \ Results: + ```JSON { "newVNet": { @@ -140,26 +142,27 @@ To manage a Kubernetes cluster, use the Kubernetes command-line client, kubectl. 1. Install az aks CLI locally using the az aks install-cli command - ```bash - if ! [ -x "$(command -v kubectl)" ]; then az aks install-cli; fi - ``` + ```bash + if ! [ -x "$(command -v kubectl)" ]; then az aks install-cli; fi + ``` 2. Configure kubectl to connect to your Kubernetes cluster using the az aks get-credentials command. The following command: - - Downloads credentials and configures the Kubernetes CLI to use them. - - Uses ~/.kube/config, the default location for the Kubernetes configuration file. Specify a different location for your Kubernetes configuration file using --file argument. - > [!WARNING] - > This will overwrite any existing credentials with the same entry + - Downloads credentials and configures the Kubernetes CLI to use them. + - Uses ~/.kube/config, the default location for the Kubernetes configuration file. Specify a different location for your Kubernetes configuration file using --file argument. + + > [!WARNING] + > This will overwrite any existing credentials with the same entry - ```bash - az aks get-credentials --resource-group $MY_RESOURCE_GROUP_NAME --name $MY_AKS_CLUSTER_NAME --overwrite-existing - ``` + ```bash + az aks get-credentials --resource-group $MY_RESOURCE_GROUP_NAME --name $MY_AKS_CLUSTER_NAME --overwrite-existing + ``` 3. Verify the connection to your cluster using the kubectl get command. This command returns a list of the cluster nodes. - ```bash - kubectl get nodes - ``` + ```bash + kubectl get nodes + ``` ## Install NGINX Ingress Controller @@ -203,8 +206,8 @@ kubectl apply -f azure-vote-start.yml Validate that the application is running by either visiting the public ip or the application url. The application url can be found by running the following command: ->[!Note] ->It often takes 2-3 minutes for the PODs to be created and the site to be reachable via http +> [!Note] +> It often takes 2-3 minutes for the PODs to be created and the site to be reachable via http ```bash runtime="5 minute"; endtime=$(date -ud "$runtime" +%s); while [[ $(date -u +%s) -le $endtime ]]; do STATUS=$(kubectl get pods -l app=azure-vote-front -o 'jsonpath={..status.conditions[?(@.type=="Ready")].status}'); echo $STATUS; if [ "$STATUS" = 'True' ]; then break; else sleep 10; fi; done @@ -215,6 +218,7 @@ curl "http://$FQDN" Results: + ```HTML @@ -249,7 +253,7 @@ Results: ## Add HTTPS termination to custom domain -At this point in the tutorial you have an AKS web app with NGINX as the Ingress controller and a custom domain you can use to access your application. The next step is to add an SSL certificate to the domain so that users can reach your application securely via https. +At this point in the tutorial you have an AKS web app with NGINX as the Ingress controller and a custom domain you can use to access your application. The next step is to add an SSL certificate to the domain so that users can reach your application securely via https. ## Set Up Cert Manager @@ -257,21 +261,21 @@ In order to add HTTPS we are going to use Cert Manager. Cert Manager is an open 1. In order to install cert-manager, we must first create a namespace to run it in. This tutorial will install cert-manager into the cert-manager namespace. It is possible to run cert-manager in a different namespace, although you will need to make modifications to the deployment manifests. - ```bash - kubectl create namespace cert-manager - ``` + ```bash + kubectl create namespace cert-manager + ``` 2. We can now install cert-manager. All resources are included in a single YAML manifest file. This can be installed by running the following: - ```bash - kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.7.0/cert-manager.crds.yaml - ``` + ```bash + kubectl apply -f https://github.com/jetstack/cert-manager/releases/download/v1.7.0/cert-manager.crds.yaml + ``` 3. Add the certmanager.k8s.io/disable-validation: "true" label to the cert-manager namespace by running the following. This will allow the system resources that cert-manager requires to bootstrap TLS to be created in its own namespace. - ```bash - kubectl label namespace cert-manager certmanager.k8s.io/disable-validation=true - ``` + ```bash + kubectl label namespace cert-manager certmanager.k8s.io/disable-validation=true + ``` ## Obtain certificate via Helm Charts @@ -281,47 +285,49 @@ Cert-manager provides Helm charts as a first-class method of installation on Kub 1. Add the Jetstack Helm repository - This repository is the only supported source of cert-manager charts. There are some other mirrors and copies across the internet, but those are entirely unofficial and could present a security risk. + This repository is the only supported source of cert-manager charts. There are some other mirrors and copies across the internet, but those are entirely unofficial and could present a security risk. - ```bash - helm repo add jetstack https://charts.jetstack.io - ``` + ```bash + helm repo add jetstack https://charts.jetstack.io + ``` 2. Update local Helm Chart repository cache - ```bash - helm repo update - ``` + ```bash + helm repo update + ``` 3. Install Cert-Manager addon via helm by running the following: - ```bash - helm install cert-manager jetstack/cert-manager --namespace cert-manager --version v1.7.0 - ``` + ```bash + helm install cert-manager jetstack/cert-manager --namespace cert-manager --version v1.7.0 + ``` 4. Apply Certificate Issuer YAML File - ClusterIssuers are Kubernetes resources that represent certificate authorities (CAs) that are able to generate signed certificates by honoring certificate signing requests. All cert-manager certificates require a referenced issuer that is in a ready condition to attempt to honor the request. - The issuer we are using can be found in the `cluster-issuer-prod.yml file` + ClusterIssuers are Kubernetes resources that represent certificate authorities (CAs) that are able to generate signed certificates by honoring certificate signing requests. All cert-manager certificates require a referenced issuer that is in a ready condition to attempt to honor the request. + The issuer we are using can be found in the `cluster-issuer-prod.yml file` - ```bash - cluster_issuer_variables=$( + ```ASCII True ``` @@ -344,48 +351,13 @@ True Run the following command to get the HTTPS endpoint for your application: ->[!Note] +> [!Note] > It often takes 2-3 minutes for the SSL certificate to propogate and the site to be reachable via https. - - -Results: - - -```HTML - - - - - Azure Voting App - - - - - -
-
- -
-
- - - -
-
-
Cats - 0 | Dogs - 0
- -
-
- - +echo "You can now visit your web server at https://$FQDN" ``` ## Next Steps From de7032bf457ea2c09e7b5abd4bc3eaa37d5002d4 Mon Sep 17 00:00:00 2001 From: Vincenzo Marcella <6026326+vmarcella@users.noreply.github.com> Date: Fri, 13 Oct 2023 14:06:43 -0700 Subject: [PATCH 202/226] More minor fixes (#98) * [update] sed statement to use single quotations to avoid expansion during variable rendering and format document. * [update] command rendering failures to be reported. * [fix] resource URIs not being found. --- internal/az/group.go | 2 +- internal/engine/execution.go | 5 ++++- scenarios/ocd/CreateLinuxVMLAMP/README.md | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/az/group.go b/internal/az/group.go index ae602317..c9acd7fa 100644 --- a/internal/az/group.go +++ b/internal/az/group.go @@ -32,7 +32,7 @@ func FindAllDeployedResourceURIs(resourceGroup string) []string { // Find the resource group name from the output of an az command. func FindResourceGroupName(commandOutput string) string { - matches := patterns.AzCommand.FindStringSubmatch(commandOutput) + matches := patterns.AzResourceURI.FindStringSubmatch(commandOutput) if len(matches) > 1 { return matches[1] } diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 05c756e2..a64d4c90 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -53,7 +53,7 @@ func filterDeletionCommands(steps []Step, preserveResources bool) []Step { // Executes the steps from a scenario and renders the output to the terminal. func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) error { - var resourceGroupName string + var resourceGroupName string = "" var azureStatus = environments.NewAzureDeploymentStatus() stepsToExecute := filterDeletionCommands(steps, e.Configuration.DoNotDelete) @@ -83,6 +83,8 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) erro ) if err != nil { logging.GlobalLogger.Errorf("Failed to render command: %s", err.Error()) + azureStatus.SetError(err) + environments.ReportAzureStatus(azureStatus, e.Configuration.Environment) return err } finalCommandOutput := indentMultiLineCommand(renderedCommand.StdOut, 4) @@ -173,6 +175,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) erro // Extract the resource group name from the command output if // it's not already set. if resourceGroupName == "" && patterns.AzCommand.MatchString(block.Content) { + logging.GlobalLogger.Info("Attempting to extract resource group name from command output") tmpResourceGroup := az.FindResourceGroupName(commandOutput.StdOut) if tmpResourceGroup != "" { logging.GlobalLogger.WithField("resourceGroup", tmpResourceGroup).Info("Found resource group") diff --git a/scenarios/ocd/CreateLinuxVMLAMP/README.md b/scenarios/ocd/CreateLinuxVMLAMP/README.md index 29c6cabf..d57296fb 100644 --- a/scenarios/ocd/CreateLinuxVMLAMP/README.md +++ b/scenarios/ocd/CreateLinuxVMLAMP/README.md @@ -447,7 +447,7 @@ write_files: } runcmd: - - sed -i "s/;cgi.fix_pathinfo.*/cgi.fix_pathinfo = 1/" /etc/php/8.1/fpm/php.ini + - sed -i 's/;cgi.fix_pathinfo.*/cgi.fix_pathinfo = 1/' /etc/php/8.1/fpm/php.ini - sed -i 's/^max_execution_time \= .*/max_execution_time \= 300/g' /etc/php/8.1/fpm/php.ini - sed -i 's/^upload_max_filesize \= .*/upload_max_filesize \= 64M/g' /etc/php/8.1/fpm/php.ini - sed -i 's/^post_max_size \= .*/post_max_size \= 64M/g' /etc/php/8.1/fpm/php.ini From 33a771b689014a0b2c7d7642c10ac811c0c5b985 Mon Sep 17 00:00:00 2001 From: Vincenzo Marcella <6026326+vmarcella@users.noreply.github.com> Date: Fri, 13 Oct 2023 16:07:31 -0700 Subject: [PATCH 203/226] [fix] resource group name matching. (#99) --- internal/az/group.go | 4 ++-- internal/engine/execution.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/az/group.go b/internal/az/group.go index c9acd7fa..82cb9b6e 100644 --- a/internal/az/group.go +++ b/internal/az/group.go @@ -9,7 +9,7 @@ import ( // Find all the deployed resources in a resource group. func FindAllDeployedResourceURIs(resourceGroup string) []string { output, err := shells.ExecuteBashCommand( - "az resource list -g"+resourceGroup, + "az resource list -g "+resourceGroup, shells.BashCommandConfiguration{ EnvironmentVariables: map[string]string{}, InheritEnvironment: true, @@ -32,7 +32,7 @@ func FindAllDeployedResourceURIs(resourceGroup string) []string { // Find the resource group name from the output of an az command. func FindResourceGroupName(commandOutput string) string { - matches := patterns.AzResourceURI.FindStringSubmatch(commandOutput) + matches := patterns.AzResourceGroupName.FindStringSubmatch(commandOutput) if len(matches) > 1 { return matches[1] } diff --git a/internal/engine/execution.go b/internal/engine/execution.go index a64d4c90..34d980c0 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -175,7 +175,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) erro // Extract the resource group name from the command output if // it's not already set. if resourceGroupName == "" && patterns.AzCommand.MatchString(block.Content) { - logging.GlobalLogger.Info("Attempting to extract resource group name from command output") + logging.GlobalLogger.Info("Attempting to extract resource group name from command output") tmpResourceGroup := az.FindResourceGroupName(commandOutput.StdOut) if tmpResourceGroup != "" { logging.GlobalLogger.WithField("resourceGroup", tmpResourceGroup).Info("Found resource group") From 48c5ca66aa537b6a5778da5b29dfae4d696ae13f Mon Sep 17 00:00:00 2001 From: Vincenzo Marcella <6026326+vmarcella@users.noreply.github.com> Date: Fri, 13 Oct 2023 23:29:54 -0700 Subject: [PATCH 204/226] [add] functions to strip invalid env vars from the env state file for running in cloud shell. (#101) --- internal/engine/execution.go | 9 ++++- internal/shells/bash.go | 37 ++++++++++++++++++++ internal/shells/bash_test.go | 66 ++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 internal/shells/bash_test.go diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 34d980c0..06e31ef4 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -266,8 +266,15 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) erro switch e.Configuration.Environment { case environments.EnvironmentsAzure, environments.EnvironmentsOCD: logging.GlobalLogger.Info( - "Not resetting environment variable state to retain for cloudshell.", + "Cleaning environment variable file located at /tmp/env-vars", ) + err := shells.CleanEnvironmentStateFile() + + if err != nil { + logging.GlobalLogger.Errorf("Error cleaning environment variables: %s", err.Error()) + return err + } + default: shells.ResetStoredEnvironmentVariables() } diff --git a/internal/shells/bash.go b/internal/shells/bash.go index 5438594e..63842617 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -6,6 +6,7 @@ import ( "fmt" "os" "os/exec" + "regexp" "strings" "golang.org/x/sys/unix" @@ -72,6 +73,42 @@ func ResetStoredEnvironmentVariables() error { return os.Remove(environmentStateFile) } +var environmentVariableName = regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9_]*$") + +func filterInvalidKeys(envMap map[string]string) map[string]string { + validEnvMap := make(map[string]string) + for key, value := range envMap { + if environmentVariableName.MatchString(key) { + validEnvMap[key] = value + } + } + return validEnvMap +} + +func CleanEnvironmentStateFile() error { + env, err := loadEnvFile(environmentStateFile) + + if err != nil { + return err + } + + env = filterInvalidKeys(env) + + file, err := os.Create(environmentStateFile) + if err != nil { + return err + } + + writer := bufio.NewWriter(file) + for k, v := range env { + _, err := fmt.Fprintf(writer, "%s=%s\n", k, v) + if err != nil { + return err + } + } + return writer.Flush() +} + type CommandOutput struct { StdOut string StdErr string diff --git a/internal/shells/bash_test.go b/internal/shells/bash_test.go new file mode 100644 index 00000000..0e0b262f --- /dev/null +++ b/internal/shells/bash_test.go @@ -0,0 +1,66 @@ +package shells + +import ( + "testing" +) + +func TestEnvironmentVariableValidationAndFiltering(t *testing.T) { + // Test key validation + t.Run("Key Validation", func(t *testing.T) { + validCases := []struct { + key string + expected bool + }{ + {"ValidKey", true}, + {"VALID_VARIABLE", true}, + {"_AnotherValidKey", true}, + {"123Key", false}, // Starts with a digit + {"key-with-hyphen", false}, // Contains a hyphen + {"key.with.dot", false}, // Contains a period + {"Fabric_NET-0-[Delegated]", false}, // From cloud shell environment. + } + + for _, tc := range validCases { + t.Run(tc.key, func(t *testing.T) { + result := environmentVariableName.MatchString(tc.key) + if result != tc.expected { + t.Errorf( + "Expected isValidKey(%s) to be %v, got %v", + tc.key, + tc.expected, + result, + ) + } + }) + } + }) + + // Test key filtering + t.Run("Key Filtering", func(t *testing.T) { + envMap := map[string]string{ + "ValidKey": "value1", + "_AnotherValidKey": "value2", + "123Key": "value3", + "key-with-hyphen": "value4", + "key.with.dot": "value5", + "Fabric_NET-0-[Delegated]": "false", // From cloud shell environment. + } + + validEnvMap := filterInvalidKeys(envMap) + + expectedValidEnvMap := map[string]string{ + "ValidKey": "value1", + "_AnotherValidKey": "value2", + } + + if len(validEnvMap) != len(expectedValidEnvMap) { + t.Errorf("Expected validEnvMap to have %d keys, got %d", len(expectedValidEnvMap), len(validEnvMap)) + } + + for key, value := range validEnvMap { + if expectedValue, ok := expectedValidEnvMap[key]; !ok || value != expectedValue { + t.Errorf("Expected validEnvMap[%s] to be %s, got %s", key, expectedValue, value) + } + } + }) +} From fd52367877335805f178f6b3bdf74a42481fe702 Mon Sep 17 00:00:00 2001 From: Vincenzo Marcella <6026326+vmarcella@users.noreply.github.com> Date: Sat, 14 Oct 2023 01:11:13 -0700 Subject: [PATCH 205/226] [add] quotations to all env values. (#102) --- internal/shells/bash.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/shells/bash.go b/internal/shells/bash.go index 63842617..4098c4cb 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -101,7 +101,7 @@ func CleanEnvironmentStateFile() error { writer := bufio.NewWriter(file) for k, v := range env { - _, err := fmt.Fprintf(writer, "%s=%s\n", k, v) + _, err := fmt.Fprintf(writer, "%s=\"%s\"\n", k, v) if err != nil { return err } From 41440e47dab0ccc30482992d4193dd8389811416 Mon Sep 17 00:00:00 2001 From: Rahul Gupta Date: Mon, 16 Oct 2023 14:58:14 -0700 Subject: [PATCH 206/226] [fix] rendering of multi-line strings --- internal/engine/execution.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 06e31ef4..88f2fdd0 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -71,7 +71,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) erro for _, block := range step.CodeBlocks { // Render the codeblock. - escapedCommand := strings.ReplaceAll(block.Content, "\\\n", "\\\\\n") + escapedCommand := strings.ReplaceAll(block.Content, "\\\n", "\\\\\\\n") renderedCommand, err := shells.ExecuteBashCommand( "echo -e \""+escapedCommand+"\"", shells.BashCommandConfiguration{ From 531f3c8bff0c3a231c9e9dea0004ceb8004abb0a Mon Sep 17 00:00:00 2001 From: Rahul Gupta Date: Mon, 16 Oct 2023 15:10:34 -0700 Subject: [PATCH 207/226] [refactor] command rendering --- internal/engine/execution.go | 25 +++++++++++++++---------- internal/engine/execution_test.go | 22 ++++++++++++++++++++++ 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index 88f2fdd0..c9ff8d0b 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -50,6 +50,20 @@ func filterDeletionCommands(steps []Step, preserveResources bool) []Step { return filteredSteps } +func renderCommand(blockContent string) (shells.CommandOutput, error) { + escapedCommand := strings.ReplaceAll(blockContent, "\\\n", "\\\\\\\n") + renderedCommand, err := shells.ExecuteBashCommand( + "echo -e \""+escapedCommand+"\"", + shells.BashCommandConfiguration{ + EnvironmentVariables: map[string]string{}, + InteractiveCommand: false, + WriteToHistory: false, + InheritEnvironment: true, + }, + ) + return renderedCommand, err +} + // Executes the steps from a scenario and renders the output to the terminal. func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) error { @@ -71,16 +85,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) erro for _, block := range step.CodeBlocks { // Render the codeblock. - escapedCommand := strings.ReplaceAll(block.Content, "\\\n", "\\\\\\\n") - renderedCommand, err := shells.ExecuteBashCommand( - "echo -e \""+escapedCommand+"\"", - shells.BashCommandConfiguration{ - EnvironmentVariables: map[string]string{}, - InteractiveCommand: false, - WriteToHistory: false, - InheritEnvironment: true, - }, - ) + renderedCommand, err := renderCommand(block.Content) if err != nil { logging.GlobalLogger.Errorf("Failed to render command: %s", err.Error()) azureStatus.SetError(err) diff --git a/internal/engine/execution_test.go b/internal/engine/execution_test.go index 00a22ef6..fa2cbe8c 100644 --- a/internal/engine/execution_test.go +++ b/internal/engine/execution_test.go @@ -1 +1,23 @@ package engine + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExecuteBlock(t *testing.T) { + blocks := []string{ + "echo \"hello \\\nworld\"", // tutorial.md + "echo hello \\\nworld", + "echo \"hello world\"", + "echo hello world", + } + for _, blockCommand := range blocks { + t.Run("render command", func(t *testing.T) { + _, err := renderCommand(blockCommand) + assert.Equal(t, nil, err) + }) + } + +} From 0a78c146a5c26a2710c409ffd86988c59ed89771 Mon Sep 17 00:00:00 2001 From: Rahul Gupta Date: Mon, 16 Oct 2023 15:14:19 -0700 Subject: [PATCH 208/226] [update] go.mod --- go.mod | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index e2a5b65d..ec1be046 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,10 @@ require ( github.com/sergi/go-diff v1.3.1 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.7.0 + github.com/stretchr/testify v1.8.2 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 github.com/yuin/goldmark v1.5.4 + golang.org/x/sys v0.8.0 gopkg.in/ini.v1 v1.67.0 k8s.io/api v0.27.1 k8s.io/apimachinery v0.27.1 @@ -46,15 +48,14 @@ require ( github.com/muesli/reflow v0.3.0 // indirect github.com/muesli/termenv v0.15.1 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.4 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stretchr/testify v1.8.2 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect golang.org/x/crypto v0.6.0 // indirect golang.org/x/net v0.8.0 // indirect golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect - golang.org/x/sys v0.8.0 // indirect golang.org/x/term v0.6.0 // indirect golang.org/x/text v0.8.0 // indirect golang.org/x/time v0.3.0 // indirect From 6c89a0850d24ceb31fda38764bcefccbf7185eea Mon Sep 17 00:00:00 2001 From: Vincenzo Marcella <6026326+vmarcella@users.noreply.github.com> Date: Mon, 16 Oct 2023 17:46:21 -0700 Subject: [PATCH 209/226] [update] pipeline action for azure cli sign in. (#109) --- .github/workflows/scenario-testing.yaml | 43 +++++++++++-------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/.github/workflows/scenario-testing.yaml b/.github/workflows/scenario-testing.yaml index 9d3e1807..290de5be 100644 --- a/.github/workflows/scenario-testing.yaml +++ b/.github/workflows/scenario-testing.yaml @@ -1,38 +1,31 @@ name: scenario-testing - on: schedule: - cron: "0 */2 * * *" push: branches: - main - pull_request: + pull_request: branches: - main - jobs: test-ocd-scenarios: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Build all targets. - run: | - make build-all - make test-all - - - name: Sign into Azure - uses: azure/login@v1 - with: - creds: ${{ secrets.AZURE_CREDENTIALS }} - - - name: Run all one click deployment scenarios. - uses: azure/CLI@v1 - with: - inlineScript: | - make test-scenarios SUBSCRIPTION=${{ secrets.AZURE_SUBSCRIPTION }} - - - name: Display ie.log file - run: | - cat ie.log - - + - uses: actions/checkout@v2 + - name: Build all targets. + run: | + make build-all + make test-all + - name: Sign into Azure + uses: azure/actions/login@v1 + with: + creds: ${{ secrets.AZURE_CREDENTIALS }} + - name: Run all one click deployment scenarios. + uses: azure/CLI@v1 + with: + inlineScript: | + make test-scenarios SUBSCRIPTION=${{ secrets.AZURE_SUBSCRIPTION }} + - name: Display ie.log file + run: | + cat ie.log From 76e70fc0fa9d6f7faeb55516ae34852aa765906a Mon Sep 17 00:00:00 2001 From: Rahul Gupta Date: Tue, 17 Oct 2023 15:38:31 -0700 Subject: [PATCH 210/226] address multiline quoted case separately --- internal/engine/execution.go | 5 ++++- internal/engine/execution_test.go | 1 + internal/patterns/regex.go | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/engine/execution.go b/internal/engine/execution.go index c9ff8d0b..aa4d8880 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -51,7 +51,10 @@ func filterDeletionCommands(steps []Step, preserveResources bool) []Step { } func renderCommand(blockContent string) (shells.CommandOutput, error) { - escapedCommand := strings.ReplaceAll(blockContent, "\\\n", "\\\\\\\n") + escapedCommand := blockContent + if !patterns.MultilineQuotedStringCommand.MatchString(blockContent) { + escapedCommand = strings.ReplaceAll(blockContent, "\\\n", "\\\\\n") + } renderedCommand, err := shells.ExecuteBashCommand( "echo -e \""+escapedCommand+"\"", shells.BashCommandConfiguration{ diff --git a/internal/engine/execution_test.go b/internal/engine/execution_test.go index fa2cbe8c..875229cd 100644 --- a/internal/engine/execution_test.go +++ b/internal/engine/execution_test.go @@ -12,6 +12,7 @@ func TestExecuteBlock(t *testing.T) { "echo hello \\\nworld", "echo \"hello world\"", "echo hello world", + "ls \\\n-a", } for _, blockCommand := range blocks { t.Run("render command", func(t *testing.T) { diff --git a/internal/patterns/regex.go b/internal/patterns/regex.go index 56635fc9..67e5db8c 100644 --- a/internal/patterns/regex.go +++ b/internal/patterns/regex.go @@ -8,6 +8,9 @@ var ( `(^|\s)\bssh\b\s+([^\s]+(\s+|$))+((?P[a-zA-Z0-9_-]+|\$[A-Z_0-9]+)@(?P[a-zA-Z0-9.-]+|\$[A-Z_0-9]+))`, ) + // Multiline quoted string + MultilineQuotedStringCommand = regexp.MustCompile(`\"(.*\\\n.*)+\"`) + // Az cli command regex AzCommand = regexp.MustCompile(`az\s+([a-z]+)\s+([a-z]+)`) AzGroupDelete = regexp.MustCompile(`az group delete`) From fc5162506ce1e2c080493f32113fcee04acb9421 Mon Sep 17 00:00:00 2001 From: Rahul Gupta Date: Tue, 17 Oct 2023 16:49:16 -0700 Subject: [PATCH 211/226] [add] hide value rendering behind a feature flag --- cmd/ie/commands/execute.go | 20 ++++++++++++++++++++ cmd/ie/commands/root.go | 3 +++ internal/engine/engine.go | 1 + internal/engine/execution.go | 21 +++++++++++++-------- tutorial.md | 6 +++++- 5 files changed, 42 insertions(+), 9 deletions(-) diff --git a/cmd/ie/commands/execute.go b/cmd/ie/commands/execute.go index 65a7838c..b6075928 100644 --- a/cmd/ie/commands/execute.go +++ b/cmd/ie/commands/execute.go @@ -54,6 +54,10 @@ var executeCommand = &cobra.Command{ workingDirectory, _ := cmd.Flags().GetString("working-directory") environmentVariables, _ := cmd.Flags().GetStringArray("var") + features, _ := cmd.Flags().GetStringArray("feature") + + // Known features + renderValues := false // Parse the environment variables from the command line into a map cliEnvironmentVariables := make(map[string]string) @@ -72,6 +76,21 @@ var executeCommand = &cobra.Command{ cliEnvironmentVariables[keyValuePair[0]] = keyValuePair[1] } + for _, feature := range features { + switch feature { + case "render-values": + renderValues = true + default: + logging.GlobalLogger.Errorf( + "Error: Invalid feature: %s", + feature, + ) + fmt.Printf("Error: Invalid feature: %s\n", feature) + cmd.Help() + os.Exit(1) + } + } + // Parse the markdown file and create a scenario scenario, err := engine.CreateScenarioFromMarkdown( markdownFile, @@ -91,6 +110,7 @@ var executeCommand = &cobra.Command{ CorrelationId: correlationId, Environment: environment, WorkingDirectory: workingDirectory, + RenderValues: renderValues, }) if err != nil { diff --git a/cmd/ie/commands/root.go b/cmd/ie/commands/root.go index ba37e55a..9c4dcc4a 100644 --- a/cmd/ie/commands/root.go +++ b/cmd/ie/commands/root.go @@ -45,6 +45,9 @@ func ExecuteCLI() { rootCommand.PersistentFlags(). String("environment", environments.EnvironmentsLocal, "The environment that the CLI is running in. (local, ci, ocd)") + rootCommand.PersistentFlags(). + StringArray("feature", []string{}, "Enables the specified feature. Format: --feature ") + if err := rootCommand.Execute(); err != nil { fmt.Println(err) logging.GlobalLogger.Errorf("Error executing command: %s", err) diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 1990186e..278b90dd 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -18,6 +18,7 @@ type EngineConfiguration struct { Subscription string Environment string WorkingDirectory string + RenderValues bool } type Engine struct { diff --git a/internal/engine/execution.go b/internal/engine/execution.go index aa4d8880..62e2aa61 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -87,15 +87,20 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) erro azureStatus.CurrentStep = stepNumber + 1 for _, block := range step.CodeBlocks { - // Render the codeblock. - renderedCommand, err := renderCommand(block.Content) - if err != nil { - logging.GlobalLogger.Errorf("Failed to render command: %s", err.Error()) - azureStatus.SetError(err) - environments.ReportAzureStatus(azureStatus, e.Configuration.Environment) - return err + var finalCommandOutput string + if e.Configuration.RenderValues { + // Render the codeblock. + renderedCommand, err := renderCommand(block.Content) + if err != nil { + logging.GlobalLogger.Errorf("Failed to render command: %s", err.Error()) + azureStatus.SetError(err) + environments.ReportAzureStatus(azureStatus, e.Configuration.Environment) + return err + } + finalCommandOutput = indentMultiLineCommand(renderedCommand.StdOut, 4) + } else { + finalCommandOutput = indentMultiLineCommand(block.Content, 4) } - finalCommandOutput := indentMultiLineCommand(renderedCommand.StdOut, 4) fmt.Print(" " + finalCommandOutput) diff --git a/tutorial.md b/tutorial.md index 30772fd4..85cc604b 100644 --- a/tutorial.md +++ b/tutorial.md @@ -6,7 +6,11 @@ Innovation engine can execute bash commands. For example ```bash -echo "Hello World" +export VAR="Hello World" +``` + +```bash +echo $VAR ``` # Test Code block with expected output From 85505f07a4197ac7efc1caa5876ac1bff38d2825 Mon Sep 17 00:00:00 2001 From: Vincenzo Marcella <6026326+vmarcella@users.noreply.github.com> Date: Tue, 17 Oct 2023 18:07:44 -0700 Subject: [PATCH 212/226] Fixes for scenarios (#111) * Fixed errors on AKS doc * Fixed errors on AKS doc * Fixed errors on AKS doc * Fixed scenario bugs and cleaned text * Reverted dns name change * Fixed styling issues * Reverting moving around variable declarations and small text changes --------- Co-authored-by: Mitchell Bifeld --- scenarios/ocd/CreateAKSDeployment/README.md | 58 +++++++++++++++------ scenarios/ocd/CreateLinuxVMAndSSH/README.md | 11 ++-- scenarios/ocd/CreateLinuxVMLAMP/README.md | 56 +++++++++++++------- 3 files changed, 85 insertions(+), 40 deletions(-) diff --git a/scenarios/ocd/CreateAKSDeployment/README.md b/scenarios/ocd/CreateAKSDeployment/README.md index 446f60f1..49efb2fc 100644 --- a/scenarios/ocd/CreateAKSDeployment/README.md +++ b/scenarios/ocd/CreateAKSDeployment/README.md @@ -7,10 +7,10 @@ Welcome to this tutorial where we will take you step by step in creating an Azur The First step in this tutorial is to define environment variables. ```bash -export SSL_EMAIL_ADDRESS="$(az account show --query user.name --output tsv)" -export NETWORK_PREFIX="$(($RANDOM % 254 + 1))" export RANDOM_ID="$(openssl rand -hex 3)" -export MY_RESOURCE_GROUP_NAME="myResourceGroup$RANDOM_ID" +export NETWORK_PREFIX="$(($RANDOM % 254 + 1))" +export SSL_EMAIL_ADDRESS="$(az account show --query user.name --output tsv)" +export MY_RESOURCE_GROUP_NAME="myAKSResourceGroup$RANDOM_ID" export REGION="eastus" export MY_AKS_CLUSTER_NAME="myAKSCluster$RANDOM_ID" export MY_PUBLIC_IP_NAME="myPublicIP$RANDOM_ID" @@ -113,11 +113,13 @@ az provider register --namespace Microsoft.OperationalInsights Create an AKS cluster using the az aks create command with the --enable-addons monitoring parameter to enable Container insights. The following example creates an autoscaling, availability zone enabled cluster named myAKSCluster: -This will take a few minutes +This will take a few minutes. ```bash export MY_SN_ID=$(az network vnet subnet list --resource-group $MY_RESOURCE_GROUP_NAME --vnet-name $MY_VNET_NAME --query "[0].id" --output tsv) +``` +```bash az aks create \ --resource-group $MY_RESOURCE_GROUP_NAME \ --name $MY_AKS_CLUSTER_NAME \ @@ -167,8 +169,10 @@ To manage a Kubernetes cluster, use the Kubernetes command-line client, kubectl. ## Install NGINX Ingress Controller ```bash -export MY_STATIC_IP=$(az network public-ip create --resource-group MC_${MY_RESOURCE_GROUP_NAME}_${MY_AKS_CLUSTER_NAME}_${REGION} --location ${MY_LOCATION} --name ${MY_PUBLIC_IP_NAME} --dns-name ${MY_DNS_LABEL} --sku Standard --allocation-method static --version IPv4 --zone 1 2 3 --query publicIp.ipAddress -o tsv) +export MY_STATIC_IP=$(az network public-ip create --resource-group MC_${MY_RESOURCE_GROUP_NAME}_${MY_AKS_CLUSTER_NAME}_${REGION} --location ${REGION} --name ${MY_PUBLIC_IP_NAME} --dns-name ${MY_DNS_LABEL} --sku Standard --allocation-method static --version IPv4 --zone 1 2 3 --query publicIp.ipAddress -o tsv) +``` +```bash helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx helm repo update helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \ @@ -210,8 +214,19 @@ Validate that the application is running by either visiting the public ip or the > It often takes 2-3 minutes for the PODs to be created and the site to be reachable via http ```bash -runtime="5 minute"; endtime=$(date -ud "$runtime" +%s); while [[ $(date -u +%s) -le $endtime ]]; do STATUS=$(kubectl get pods -l app=azure-vote-front -o 'jsonpath={..status.conditions[?(@.type=="Ready")].status}'); echo $STATUS; if [ "$STATUS" = 'True' ]; then break; else sleep 10; fi; done +runtime="5 minute"; +endtime=$(date -ud "$runtime" +%s); +while [[ $(date -u +%s) -le $endtime ]]; do + STATUS=$(kubectl get pods -l app=azure-vote-front -o 'jsonpath={..status.conditions[?(@.type=="Ready")].status}'); echo $STATUS; + if [ "$STATUS" = 'True' ]; then + break; + else + sleep 10; + fi; +done +``` +```bash curl "http://$FQDN" ``` @@ -253,7 +268,7 @@ Results: ## Add HTTPS termination to custom domain -At this point in the tutorial you have an AKS web app with NGINX as the Ingress controller and a custom domain you can use to access your application. The next step is to add an SSL certificate to the domain so that users can reach your application securely via https. +At this point in the tutorial you have an AKS web app with NGINX as the Ingress controller and a custom domain you can use to access your application. The next step is to add an SSL certificate to the domain so that users can reach your application securely via HTTPS. ## Set Up Cert Manager @@ -322,41 +337,54 @@ Cert-manager provides Helm charts as a first-class method of installation on Kub echo "${azure_vote_nginx_ssl_variables//\$FQDN/$FQDN}" | kubectl apply -f - ``` -## Validate application is working + +``` Validate SSL certificate is True by running the follow command: - +``` Results: - + ## Browse your AKS Deployment Secured via HTTPS Run the following command to get the HTTPS endpoint for your application: > [!Note] -> It often takes 2-3 minutes for the SSL certificate to propogate and the site to be reachable via https. +> It often takes 2-3 minutes for the SSL certificate to propogate and the site to be reachable via HTTPS. ```bash -runtime="5 minute"; endtime=$(date -ud "$runtime" +%s); while [[ $(date -u +%s) -le $endtime ]]; do STATUS=$(kubectl get svc --namespace=ingress-nginx ingress-nginx-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}'); echo $STATUS; if [ "$STATUS" = "$MY_STATIC_IP" ]; then break; else sleep 10; fi; done +runtime="5 minute"; +endtime=$(date -ud "$runtime" +%s); +while [[ $(date -u +%s) -le $endtime ]]; do + STATUS=$(kubectl get svc --namespace=ingress-nginx ingress-nginx-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}'); + echo $STATUS; + if [ "$STATUS" = "$MY_STATIC_IP" ]; then + break; + else + sleep 10; + fi; +done +``` +```bash echo "You can now visit your web server at https://$FQDN" ``` diff --git a/scenarios/ocd/CreateLinuxVMAndSSH/README.md b/scenarios/ocd/CreateLinuxVMAndSSH/README.md index 4ba4a903..abfb3f3d 100644 --- a/scenarios/ocd/CreateLinuxVMAndSSH/README.md +++ b/scenarios/ocd/CreateLinuxVMAndSSH/README.md @@ -5,10 +5,10 @@ The First step in this tutorial is to define environment variables ```bash -export UNIQUE_POSTFIX="$(date +%M%S)$$" -export MY_RESOURCE_GROUP_NAME="myResourceGroup$UNIQUE_POSTFIX" +export RANDOM_ID="$(openssl rand -hex 3)" +export MY_RESOURCE_GROUP_NAME="myVMResourceGroup$RANDOM_ID" export REGION=EastUS -export MY_VM_NAME="myVM$UNIQUE_POSTFIX" +export MY_VM_NAME="myVM$RANDOM_ID" export MY_USERNAME=azureuser export MY_VM_IMAGE="Canonical:0001-com-ubuntu-minimal-jammy:minimal-22_04-lts-gen2:latest" ``` @@ -94,10 +94,11 @@ run the following command to get the IP Address of the VM and store it as an env ```bash export IP_ADDRESS=$(az vm show --show-details --resource-group $MY_RESOURCE_GROUP_NAME --name $MY_VM_NAME --query publicIps --output tsv) ``` + # SSH Into VM -## Export the SSH configuration for use with SSH clients that support OpenSSH & SSH into the VM. -Login to Azure Linux VMs with Azure AD supports exporting the OpenSSH certificate and configuration. That means you can use any SSH clients that support OpenSSH-based certificates to sign in through Azure AD. The following example exports the configuration for all IP addresses assigned to the VM: + + +## Create RG + +Create a resource group with the [az group create](https://learn.microsoft.com/cli/azure/group#az-group-create) command. An Azure resource group is a logical container into which Azure resources are deployed and managed. +The following example creates a resource group named `$MY_RESOURCE_GROUP_NAME` in the `eastus` location. + ```bash az group create \ --name $MY_RESOURCE_GROUP_NAME \ @@ -71,7 +74,7 @@ Results: } ``` -## Setup LAMP networking +## Setup LEMP networking ## Create an Azure Virtual Network @@ -351,7 +354,7 @@ We are working with our partners to get cloud-init included and working in the i ### Create cloud-init config file -To see cloud-init in action, create a VM that installs a LAMP stack and runs a simple Wordpress app secured with an SSL certificate. The following cloud-init configuration installs the required packages, creates the Wordpress website, then initialize and starts the website. +To see cloud-init in action, create a VM that installs a LEMP stack and runs a simple Wordpress app secured with an SSL certificate. The following cloud-init configuration installs the required packages, creates the Wordpress website, then initialize and starts the website. ```bash cat << EOF > cloud-init.txt @@ -475,7 +478,7 @@ EOF ## Create an Azure Private DNS Zone for Azure MySQL Flexible Server -Azure Private DNS Zone integration allows you to resolve the private DNS within the current VNET or any in-region peered VNET where the private DNS Zone is linked. You'll use [az network private-dns zone create](https://learn.microsoft.com/cli/azure/network/private-dns/zone#az-network-private-dns-zone-create) to create the private dns zone. +Azure Private DNS Zone integration allows you to resolve the private DNS within the current VNET or any in-region peered VNET where the private DNS Zone is linked. You'll use [az network private-dns zone create](https://learn.microsoft.com/cli/azure/network/private-dns/zone#az-network-private-dns-zone-create) to create the private DNS zone. ```bash az network private-dns zone create \ @@ -567,7 +570,17 @@ If you'd like to change any defaults, please refer to the Azure CLI [reference d It takes a few minutes to create the Azure Database for MySQL - Flexible Server and supporting resources. ```bash -runtime="10 minute"; endtime=$(date -ud "$runtime" +%s); while [[ $(date -u +%s) -le $endtime ]]; do STATUS=$(az mysql flexible-server show -g $MY_RESOURCE_GROUP_NAME -n $MY_MYSQL_DB_NAME --query state -o tsv); echo $STATUS; if [ "$STATUS" = 'Ready' ]; then break; else sleep 10; fi; done +runtime="10 minute"; +endtime=$(date -ud "$runtime" +%s); +while [[ $(date -u +%s) -le $endtime ]]; do + STATUS=$(az mysql flexible-server show -g $MY_RESOURCE_GROUP_NAME -n $MY_MYSQL_DB_NAME --query state -o tsv); + echo $STATUS; + if [ "$STATUS" = 'Ready' ]; then + break; + else + sleep 10; + fi; +done ``` ## Configure server parameters in Azure Database for MySQL - Flexible Server @@ -667,9 +680,8 @@ It takes a few minutes to create the VM and supporting resources. The provisioni ```bash runtime="10 minute"; endtime=$(date -ud "$runtime" +%s); - while [[ $(date -u +%s) -le $endtime ]]; do - STATUS=$(ssh -o StrictHostKeyChecking=no $FQDN "cloud-init status"); + STATUS=$(ssh -o StrictHostKeyChecking=no $MY_VM_USERNAME@$FQDN "cloud-init status"); echo $STATUS; if [ "$STATUS" = 'status: done' ]; then break; @@ -681,7 +693,7 @@ done ## Enable Azure AD login for a Linux Virtual Machine in Azure -The following example deploys a VM and then installs the extension to enable Azure AD login for a Linux VM. VM extensions are small applications that provide post-deployment configuration and automation tasks on Azure virtual machines. +The following installs the extension to enable Azure AD login for a Linux VM. VM extensions are small applications that provide post-deployment configuration and automation tasks on Azure virtual machines. ```bash az vm extension set \ @@ -730,7 +742,7 @@ az role assignment create \ --assignee $MY_AZURE_USER_ID \ --scope $MY_RESOURCE_GROUP_ID -o JSON ``` ---> + Results: ```JSON { - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myLEMPResourceGroupxxxxxx", "location": "eastus", "managedBy": null, - "name": "myResourceGroup6ad2bc", + "name": "myLEMPResourceGroupxxxxxx", "properties": { "provisioningState": "Succeeded" }, @@ -103,21 +103,21 @@ Results: ] }, "enableDdosProtection": false, - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.Network/virtualNetworks/myVNet6ad2bc", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myLEMPResourceGroupxxxxxx/providers/Microsoft.Network/virtualNetworks/myVNetxxxxxx", "location": "eastus", - "name": "myVNet6ad2bc", + "name": "myVNetxxxxxx", "provisioningState": "Succeeded", - "resourceGroup": "myResourceGroup6ad2bc", + "resourceGroup": "myLEMPResourceGroupxxxxxx", "subnets": [ { "addressPrefix": "10.19.0.0/24", "delegations": [], - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.Network/virtualNetworks/myVNet6ad2bc/subnets/mySN6ad2bc", - "name": "mySN6ad2bc", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myLEMPResourceGroupxxxxxx/providers/Microsoft.Network/virtualNetworks/myVNetxxxxxx/subnets/mySNxxxxxx", + "name": "mySNxxxxxx", "privateEndpointNetworkPolicies": "Disabled", "privateLinkServiceNetworkPolicies": "Enabled", "provisioningState": "Succeeded", - "resourceGroup": "myResourceGroup6ad2bc", + "resourceGroup": "myLEMPResourceGroupxxxxxx", "type": "Microsoft.Network/virtualNetworks/subnets" } ], @@ -156,18 +156,18 @@ Results: "protectionMode": "VirtualNetworkInherited" }, "dnsSettings": { - "domainNameLabel": "mydnslabel6ad2bc", - "fqdn": "mydnslabel6ad2bc.eastus.cloudapp.azure.com" + "domainNameLabel": "mydnslabelxxxxxx", + "fqdn": "mydnslabelxxxxxx.eastus.cloudapp.azure.com" }, - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.Network/publicIPAddresses/myPublicIP6ad2bc", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myLEMPResourceGroupxxxxxx/providers/Microsoft.Network/publicIPAddresses/myPublicIPxxxxxx", "idleTimeoutInMinutes": 4, "ipTags": [], "location": "eastus", - "name": "myPublicIP6ad2bc", + "name": "myPublicIPxxxxxx", "provisioningState": "Succeeded", "publicIPAddressVersion": "IPv4", "publicIPAllocationMethod": "Static", - "resourceGroup": "myResourceGroup6ad2bc", + "resourceGroup": "myLEMPResourceGroupxxxxxx", "sku": { "name": "Standard", "tier": "Regional" @@ -208,23 +208,23 @@ Results: "destinationPortRange": "*", "destinationPortRanges": [], "direction": "Inbound", - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup104/providers/Microsoft.Network/networkSecurityGroups/protect-vms/defaultSecurityRules/AllowVnetInBound", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myLEMPResourceGroup104/providers/Microsoft.Network/networkSecurityGroups/protect-vms/defaultSecurityRules/AllowVnetInBound", "name": "AllowVnetInBound", "priority": 65000, "protocol": "*", "provisioningState": "Succeeded", - "resourceGroup": "myResourceGroup104", + "resourceGroup": "myLEMPResourceGroup104", "sourceAddressPrefix": "VirtualNetwork", "sourceAddressPrefixes": [], "sourcePortRange": "*", "sourcePortRanges": [], "type": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules" }, - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup104/providers/Microsoft.Network/networkSecurityGroups/protect-vms", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myLEMPResourceGroup104/providers/Microsoft.Network/networkSecurityGroups/protect-vms", "location": "eastus", "name": "protect-vms", "provisioningState": "Succeeded", - "resourceGroup": "myResourceGroup104", + "resourceGroup": "myLEMPResourceGroup104", "securityRules": [], "type": "Microsoft.Network/networkSecurityGroups" } @@ -264,12 +264,12 @@ Results: "443" ], "direction": "Inbound", - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.Network/networkSecurityGroups/myNSGName6ad2bc/securityRules/Allow-Access6ad2bc", - "name": "Allow-Access6ad2bc", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myLEMPResourceGroupxxxxxx/providers/Microsoft.Network/networkSecurityGroups/myNSGNamexxxxxx/securityRules/Allow-Accessxxxxxx", + "name": "Allow-Accessxxxxxx", "priority": 100, "protocol": "Tcp", "provisioningState": "Succeeded", - "resourceGroup": "myResourceGroup6ad2bc", + "resourceGroup": "myLEMPResourceGroupxxxxxx", "sourceAddressPrefix": "*", "sourceAddressPrefixes": [], "sourcePortRange": "*", @@ -310,33 +310,33 @@ Results: "enableAcceleratedNetworking": false, "enableIPForwarding": false, "hostedWorkloads": [], - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.Network/networkInterfaces/myVMNicName6ad2bc", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myLEMPResourceGroupxxxxxx/providers/Microsoft.Network/networkInterfaces/myVMNicNamexxxxxx", "ipConfigurations": [ { - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.Network/networkInterfaces/myVMNicName6ad2bc/ipConfigurations/ipconfig1", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myLEMPResourceGroupxxxxxx/providers/Microsoft.Network/networkInterfaces/myVMNicNamexxxxxx/ipConfigurations/ipconfig1", "name": "ipconfig1", "primary": true, "privateIPAddress": "10.19.0.4", "privateIPAddressVersion": "IPv4", "privateIPAllocationMethod": "Dynamic", "provisioningState": "Succeeded", - "resourceGroup": "myResourceGroup6ad2bc", + "resourceGroup": "myLEMPResourceGroupxxxxxx", "subnet": { - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.Network/virtualNetworks/myVNet6ad2bc/subnets/mySN6ad2bc", - "resourceGroup": "myResourceGroup6ad2bc" + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myLEMPResourceGroupxxxxxx/providers/Microsoft.Network/virtualNetworks/myVNetxxxxxx/subnets/mySNxxxxxx", + "resourceGroup": "myLEMPResourceGroupxxxxxx" }, "type": "Microsoft.Network/networkInterfaces/ipConfigurations" } ], "location": "eastus", - "name": "myVMNicName6ad2bc", + "name": "myVMNicNamexxxxxx", "networkSecurityGroup": { - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.Network/networkSecurityGroups/myNSGName6ad2bc", - "resourceGroup": "myResourceGroup6ad2bc" + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myLEMPResourceGroupxxxxxx/providers/Microsoft.Network/networkSecurityGroups/myNSGNamexxxxxx", + "resourceGroup": "myLEMPResourceGroupxxxxxx" }, "nicType": "Standard", "provisioningState": "Succeeded", - "resourceGroup": "myResourceGroup6ad2bc", + "resourceGroup": "myLEMPResourceGroupxxxxxx", "tapConfigurations": [], "type": "Microsoft.Network/networkInterfaces", "vnetEncryptionSupported": false @@ -491,17 +491,17 @@ Results: ```JSON { - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myresourcegroup6ad2bc/providers/Microsoft.Network/privateDnsZones/mydnslabel6ad2bc.private.mysql.database.azure.com", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myLEMPResourceGroupxxxxxx/providers/Microsoft.Network/privateDnsZones/mydnslabelxxxxxx.private.mysql.database.azure.com", "location": "global", "maxNumberOfRecordSets": 25000, "maxNumberOfVirtualNetworkLinks": 1000, "maxNumberOfVirtualNetworkLinksWithRegistration": 100, - "name": "mydnslabel6ad2bc.private.mysql.database.azure.com", + "name": "mydnslabelxxxxxx.private.mysql.database.azure.com", "numberOfRecordSets": 1, "numberOfVirtualNetworkLinks": 0, "numberOfVirtualNetworkLinksWithRegistration": 0, "provisioningState": "Succeeded", - "resourceGroup": "myresourcegroup6ad2bc", + "resourceGroup": "myLEMPResourceGroupxxxxxx", "tags": null, "type": "Microsoft.Network/privateDnsZones" } @@ -511,10 +511,6 @@ Results: Azure Database for MySQL - Flexible Server is a managed service that you can use to run, manage, and scale highly available MySQL servers in the cloud. Create a flexible server with the [az mysql flexible-server create](https://learn.microsoft.com/cli/azure/mysql/flexible-server#az-mysql-flexible-server-create) command. A server can contain multiple databases. The following command creates a server using service defaults and variable values from your Azure CLI's local environment: -```bash -echo "Your MySQL user $MY_MYSQL_ADMIN_USERNAME password is: $MY_WP_ADMIN_PW" -``` - ```bash az mysql flexible-server create \ --admin-password $MY_MYSQL_ADMIN_PW \ @@ -543,17 +539,21 @@ Results: ```JSON { "databaseName": "wp001", - "host": "mydb6ad2bc.mysql.database.azure.com", - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.DBforMySQL/flexibleServers/mydb6ad2bc", + "host": "mydbxxxxxx.mysql.database.azure.com", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myLEMPResourceGroupxxxxxx/providers/Microsoft.DBforMySQL/flexibleServers/mydbxxxxxx", "location": "East US", - "resourceGroup": "myResourceGroup6ad2bc", + "resourceGroup": "myLEMPResourceGroupxxxxxx", "skuname": "Standard_B2s", - "subnetId": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.Network/virtualNetworks/myVNet6ad2bc/subnets/myMySQLSN6ad2bc", - "username": "dbadmin6ad2bc", + "subnetId": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myLEMPResourceGroupxxxxxx/providers/Microsoft.Network/virtualNetworks/myVNetxxxxxx/subnets/myMySQLSNxxxxxx", + "username": "dbadminxxxxxx", "version": "8.0.21" } ``` +```bash +echo "Your MySQL user $MY_MYSQL_ADMIN_USERNAME password is: $MY_WP_ADMIN_PW" +``` + The server created has the below attributes: * The server name, admin username, admin password, resource group name, location are already specified in local context environment of the cloud shell, and will be created in the same location as your the resource group and the other Azure components. @@ -575,7 +575,7 @@ endtime=$(date -ud "$runtime" +%s); while [[ $(date -u +%s) -le $endtime ]]; do STATUS=$(az mysql flexible-server show -g $MY_RESOURCE_GROUP_NAME -n $MY_MYSQL_DB_NAME --query state -o tsv); echo $STATUS; - if [ "$STATUS" = 'Ready' ]; then + if [ "$STATUS" == 'Ready' ]; then break; else sleep 10; @@ -611,12 +611,12 @@ Results: "currentValue": "OFF", "dataType": "Enumeration", "defaultValue": "ON", - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.DBforMySQL/flexibleServers/mydb6ad2bc/configurations/require_secure_transport", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myLEMPResourceGroupxxxxxx/providers/Microsoft.DBforMySQL/flexibleServers/mydbxxxxxx/configurations/require_secure_transport", "isConfigPendingRestart": "False", "isDynamicConfig": "True", "isReadOnly": "False", "name": "require_secure_transport", - "resourceGroup": "myResourceGroup6ad2bc", + "resourceGroup": "myLEMPResourceGroupxxxxxx", "source": "user-override", "systemData": null, "type": "Microsoft.DBforMySQL/flexibleServers/configurations", @@ -656,8 +656,8 @@ Results: ```JSON { - "fqdns": "mydnslabel6ad2bc.eastus.cloudapp.azure.com", - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup6ad2bc/providers/Microsoft.Compute/virtualMachines/myVMName6ad2bc", + "fqdns": "mydnslabelxxxxxx.eastus.cloudapp.azure.com", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myLEMPResourceGroupxxxxxx/providers/Microsoft.Compute/virtualMachines/myVMNamexxxxxx", "identity": { "principalId": "yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy", "tenantId": "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz", @@ -668,7 +668,7 @@ Results: "macAddress": "60-45-BD-D8-1D-84", "powerState": "VM running", "privateIpAddress": "10.19.0.4", - "resourceGroup": "myResourceGroup6ad2bc", + "resourceGroup": "myLEMPResourceGroupxxxxxx", "zones": "" } ``` @@ -678,57 +678,19 @@ Results: It takes a few minutes to create the VM and supporting resources. The provisioningState value of Succeeded appears when the extension is successfully installed on the VM. The VM must have a running [VM agent](https://learn.microsoft.com/azure/virtual-machines/extensions/agent-linux) to install the extension. ```bash -runtime="10 minute"; +runtime="5 minute"; endtime=$(date -ud "$runtime" +%s); while [[ $(date -u +%s) -le $endtime ]]; do - STATUS=$(ssh -o StrictHostKeyChecking=no $MY_VM_USERNAME@$FQDN "cloud-init status"); + STATUS=$(ssh -o StrictHostKeyChecking=no $MY_VM_USERNAME@$FQDN "cloud-init status --wait"); echo $STATUS; - if [ "$STATUS" = 'status: done' ]; then + if [[ "$STATUS" == *'status: done'* ]]; then break; else - sleep 10; + sleep 10; fi; done ``` -## Enable Azure AD login for a Linux Virtual Machine in Azure - -The following installs the extension to enable Azure AD login for a Linux VM. VM extensions are small applications that provide post-deployment configuration and automation tasks on Azure virtual machines. - -```bash -az vm extension set \ - --publisher Microsoft.Azure.ActiveDirectory \ - --name AADSSHLoginForLinux \ - --resource-group $MY_RESOURCE_GROUP_NAME \ - --vm-name $MY_VM_NAME -o JSON -``` - -Results: - - -```JSON -{ - "autoUpgradeMinorVersion": true, - "enableAutomaticUpgrade": null, - "forceUpdateTag": null, - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroupfa636b/providers/Microsoft.Compute/virtualMachines/myVMNamefa636b/extensions/AADSSHLoginForLinux", - "instanceView": null, - "location": "eastus", - "name": "AADSSHLoginForLinux", - "protectedSettings": null, - "protectedSettingsFromKeyVault": null, - "provisioningState": "Succeeded", - "publisher": "Microsoft.Azure.ActiveDirectory", - "resourceGroup": "myResourceGroupfa636b", - "settings": null, - "suppressFailures": null, - "tags": null, - "type": "Microsoft.Compute/virtualMachines/extensions", - "typeHandlerVersion": "1.0", - "typePropertiesType": "AADSSHLoginForLinux" -} -``` - -## Browse your WordPress website +## Enable Azure AD login for a Linux Virtual Machine in Azure + +The following installs the extension to enable Azure AD login for a Linux VM. VM extensions are small applications that provide post-deployment configuration and automation tasks on Azure virtual machines. + +```bash +az vm extension set \ + --publisher Microsoft.Azure.ActiveDirectory \ + --name AADSSHLoginForLinux \ + --resource-group $MY_RESOURCE_GROUP_NAME \ + --vm-name $MY_VM_NAME -o JSON +``` + +Results: + + +```JSON +{ + "autoUpgradeMinorVersion": true, + "enableAutomaticUpgrade": null, + "forceUpdateTag": null, + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myLEMPResourceGroupxxxxxx/providers/Microsoft.Compute/virtualMachines/myVMNamexxxxxx/extensions/AADSSHLoginForLinux", + "instanceView": null, + "location": "eastus", + "name": "AADSSHLoginForLinux", + "protectedSettings": null, + "protectedSettingsFromKeyVault": null, + "provisioningState": "Succeeded", + "publisher": "Microsoft.Azure.ActiveDirectory", + "resourceGroup": "myLEMPResourceGroupxxxxxx", + "settings": null, + "suppressFailures": null, + "tags": null, + "type": "Microsoft.Compute/virtualMachines/extensions", + "typeHandlerVersion": "1.0", + "typePropertiesType": "AADSSHLoginForLinux" +} +``` + +## Check and browse your WordPress website [WordPress](https://www.wordpress.org) is an open source content management system (CMS) used by over 40% of the web to create websites, blogs, and other applications. WordPress can be run on a few different Azure services: [AKS](https://learn.microsoft.com/azure/mysql/flexible-server/tutorial-deploy-wordpress-on-aks), Virtual Machines, and App Service. For a full list of WordPress options on Azure, see [WordPress on Azure Marketplace](https://azuremarketplace.microsoft.com/marketplace/apps?page=1&search=wordpress). This WordPress setup is only for proof of concept. To install the latest WordPress in production with recommended security settings, see the [WordPress documentation](https://codex.wordpress.org/Main_Page). -Validate that the application is running by visiting the application url: +Validate that the application is running by curling the application url: ```bash -curl --max-time 120 "https://$FQDN" +runtime="5 minute"; +endtime=$(date -ud "$runtime" +%s); +while [[ $(date -u +%s) -le $endtime ]]; do + if curl -I -s -f $FQDN > /dev/null ; then + curl -L -s -f $FQDN 2> /dev/null | head -n 9 + break + else + sleep 10 + fi; +done ``` Results: @@ -801,8 +810,8 @@ Results: Azure hosted blog - - + + ``` ```bash From 69929c8aac9e17ae064bba85fde76575768ef327 Mon Sep 17 00:00:00 2001 From: Mitchell Bifeld <55719566+mbifeld@users.noreply.github.com> Date: Fri, 20 Oct 2023 16:19:43 -0700 Subject: [PATCH 215/226] VM SSH Fixes --- scenarios/ocd/CreateLinuxVMAndSSH/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scenarios/ocd/CreateLinuxVMAndSSH/README.md b/scenarios/ocd/CreateLinuxVMAndSSH/README.md index abfb3f3d..20fc414b 100644 --- a/scenarios/ocd/CreateLinuxVMAndSSH/README.md +++ b/scenarios/ocd/CreateLinuxVMAndSSH/README.md @@ -2,7 +2,7 @@ ## Define Environment Variables -The First step in this tutorial is to define environment variables +The First step in this tutorial is to define environment variables. ```bash export RANDOM_ID="$(openssl rand -hex 3)" @@ -30,10 +30,10 @@ Results: ```json { - "id": "/subscriptions/325e7c34-99fb-4190-aa87-1df746c67705/resourceGroups/myResourceGroup", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myVMResourceGroup", "location": "eastus", "managedBy": null, - "name": "myResourceGroup", + "name": "myVMResourceGroup", "properties": { "provisioningState": "Succeeded" }, @@ -65,13 +65,13 @@ Results: ```json { "fqdns": "", - "id": "/subscriptions/325e7c34-99fb-4190-aa87-1df746c67705/resourceGroups/myResourceGroup/providers/Microsoft.Compute/virtualMachines/myVM", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myVMResourceGroup/providers/Microsoft.Compute/virtualMachines/myVM", "location": "eastus", "macAddress": "00-0D-3A-10-4F-70", "powerState": "VM running", "privateIpAddress": "10.0.0.4", "publicIpAddress": "52.147.208.85", - "resourceGroup": "myResourceGroup", + "resourceGroup": "myVMResourceGroup", "zones": "" } ``` From c6aaa7d1814be117e5f0799af3016f7655a9ce79 Mon Sep 17 00:00:00 2001 From: Mitchell Bifeld <55719566+mbifeld@users.noreply.github.com> Date: Fri, 20 Oct 2023 16:20:15 -0700 Subject: [PATCH 216/226] AKS Fixes --- scenarios/ocd/CreateAKSDeployment/README.md | 34 +++++++++------------ 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/scenarios/ocd/CreateAKSDeployment/README.md b/scenarios/ocd/CreateAKSDeployment/README.md index 49efb2fc..abb781b9 100644 --- a/scenarios/ocd/CreateAKSDeployment/README.md +++ b/scenarios/ocd/CreateAKSDeployment/README.md @@ -4,7 +4,7 @@ Welcome to this tutorial where we will take you step by step in creating an Azur ## Define Environment Variables -The First step in this tutorial is to define environment variables. +The first step in this tutorial is to define environment variables. ```bash export RANDOM_ID="$(openssl rand -hex 3)" @@ -36,7 +36,7 @@ Results: ```JSON { - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myResourceGroup210", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/myAKSResourceGroupxxxxxx", "location": "eastus", "managedBy": null, "name": "testResourceGroup", @@ -71,25 +71,25 @@ Results: "newVNet": { "addressSpace": { "addressPrefixes": [ - "10.210.0.0/16" + "10.xxx.0.0/16" ] }, "enableDdosProtection": false, - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/myResourceGroup210/providers/Microsoft.Network/virtualNetworks/myVNet210", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/myAKSResourceGroupxxxxxx/providers/Microsoft.Network/virtualNetworks/myVNetxxx", "location": "eastus", - "name": "myVNet210", + "name": "myVNetxxx", "provisioningState": "Succeeded", - "resourceGroup": "myResourceGroup210", + "resourceGroup": "myAKSResourceGroupxxxxxx", "subnets": [ { - "addressPrefix": "10.210.0.0/22", + "addressPrefix": "10.xxx.0.0/22", "delegations": [], - "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/myResourceGroup210/providers/Microsoft.Network/virtualNetworks/myVNet210/subnets/mySN210", - "name": "mySN210", + "id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/myAKSResourceGroupxxxxxx/providers/Microsoft.Network/virtualNetworks/myVNetxxx/subnets/mySNxxx", + "name": "mySNxxx", "privateEndpointNetworkPolicies": "Disabled", "privateLinkServiceNetworkPolicies": "Enabled", "provisioningState": "Succeeded", - "resourceGroup": "myResourceGroup210", + "resourceGroup": "myAKSResourceGroupxxxxxx", "type": "Microsoft.Network/virtualNetworks/subnets" } ], @@ -111,15 +111,12 @@ az provider register --namespace Microsoft.OperationalInsights ## Create AKS Cluster -Create an AKS cluster using the az aks create command with the --enable-addons monitoring parameter to enable Container insights. The following example creates an autoscaling, availability zone enabled cluster named myAKSCluster: +Create an AKS cluster using the az aks create command with the --enable-addons monitoring parameter to enable Container insights. The following example creates an autoscaling, availability zone enabled cluster. This will take a few minutes. ```bash export MY_SN_ID=$(az network vnet subnet list --resource-group $MY_RESOURCE_GROUP_NAME --vnet-name $MY_VNET_NAME --query "[0].id" --output tsv) -``` - -```bash az aks create \ --resource-group $MY_RESOURCE_GROUP_NAME \ --name $MY_AKS_CLUSTER_NAME \ @@ -170,9 +167,6 @@ To manage a Kubernetes cluster, use the Kubernetes command-line client, kubectl. ```bash export MY_STATIC_IP=$(az network public-ip create --resource-group MC_${MY_RESOURCE_GROUP_NAME}_${MY_AKS_CLUSTER_NAME}_${REGION} --location ${REGION} --name ${MY_PUBLIC_IP_NAME} --dns-name ${MY_DNS_LABEL} --sku Standard --allocation-method static --version IPv4 --zone 1 2 3 --query publicIp.ipAddress -o tsv) -``` - -```bash helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx helm repo update helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \ @@ -211,14 +205,14 @@ kubectl apply -f azure-vote-start.yml Validate that the application is running by either visiting the public ip or the application url. The application url can be found by running the following command: > [!Note] -> It often takes 2-3 minutes for the PODs to be created and the site to be reachable via http +> It often takes 2-3 minutes for the PODs to be created and the site to be reachable via HTTP ```bash runtime="5 minute"; endtime=$(date -ud "$runtime" +%s); while [[ $(date -u +%s) -le $endtime ]]; do STATUS=$(kubectl get pods -l app=azure-vote-front -o 'jsonpath={..status.conditions[?(@.type=="Ready")].status}'); echo $STATUS; - if [ "$STATUS" = 'True' ]; then + if [ "$STATUS" == 'True' ]; then break; else sleep 10; @@ -376,7 +370,7 @@ endtime=$(date -ud "$runtime" +%s); while [[ $(date -u +%s) -le $endtime ]]; do STATUS=$(kubectl get svc --namespace=ingress-nginx ingress-nginx-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}'); echo $STATUS; - if [ "$STATUS" = "$MY_STATIC_IP" ]; then + if [ "$STATUS" == "$MY_STATIC_IP" ]; then break; else sleep 10; From b6098e8e2b98ee2eb2729bc1cff4cba067cd8020 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Oct 2023 23:21:59 +0000 Subject: [PATCH 217/226] Bump golang.org/x/net from 0.8.0 to 0.17.0 Bumps [golang.org/x/net](https://github.com/golang/net) from 0.8.0 to 0.17.0. - [Commits](https://github.com/golang/net/compare/v0.8.0...v0.17.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: indirect ... Signed-off-by: dependabot[bot] --- go.mod | 10 +++++----- go.sum | 20 ++++++++++---------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/go.mod b/go.mod index ec1be046..06e39089 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,7 @@ require ( github.com/stretchr/testify v1.8.2 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 github.com/yuin/goldmark v1.5.4 - golang.org/x/sys v0.8.0 + golang.org/x/sys v0.13.0 gopkg.in/ini.v1 v1.67.0 k8s.io/api v0.27.1 k8s.io/apimachinery v0.27.1 @@ -53,11 +53,11 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect - golang.org/x/crypto v0.6.0 // indirect - golang.org/x/net v0.8.0 // indirect + golang.org/x/crypto v0.14.0 // indirect + golang.org/x/net v0.17.0 // indirect golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect - golang.org/x/term v0.6.0 // indirect - golang.org/x/text v0.8.0 // indirect + golang.org/x/term v0.13.0 // indirect + golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/protobuf v1.28.1 // indirect diff --git a/go.sum b/go.sum index 0fdfcfb1..30e33ca1 100644 --- a/go.sum +++ b/go.sum @@ -240,8 +240,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= +golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -300,8 +300,8 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= +golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= +golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -352,19 +352,19 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= From f476afcbeee0e051b8e3420dc2b6c70d183b9157 Mon Sep 17 00:00:00 2001 From: Rahul Gupta Date: Tue, 24 Oct 2023 17:22:06 -0700 Subject: [PATCH 218/226] [fix] restoration of env vars --- internal/shells/bash.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/shells/bash.go b/internal/shells/bash.go index 4098c4cb..2c11e103 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -38,7 +38,12 @@ func loadEnvFile(path string) (map[string]string, error) { line := scanner.Text() if strings.Contains(line, "=") { parts := strings.SplitN(line, "=", 2) // Split at the first "=" only - env[parts[0]] = parts[1] + v := parts[1] + if len(v) >= 2 && v[0] == '"' && v[len(v)-1] == '"' { + // Remove leading and trailing quotes + v = v[1 : len(v)-1] + } + env[parts[0]] = v } } return env, nil From 03880d11fadcf98e39c465248aa1d5b64f99cdb7 Mon Sep 17 00:00:00 2001 From: Rahul Gupta Date: Wed, 25 Oct 2023 09:42:00 -0700 Subject: [PATCH 219/226] [refactor] rename v to value --- internal/shells/bash.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/shells/bash.go b/internal/shells/bash.go index 2c11e103..8ca35e2c 100644 --- a/internal/shells/bash.go +++ b/internal/shells/bash.go @@ -38,12 +38,12 @@ func loadEnvFile(path string) (map[string]string, error) { line := scanner.Text() if strings.Contains(line, "=") { parts := strings.SplitN(line, "=", 2) // Split at the first "=" only - v := parts[1] - if len(v) >= 2 && v[0] == '"' && v[len(v)-1] == '"' { + value := parts[1] + if len(value) >= 2 && value[0] == '"' && value[len(value)-1] == '"' { // Remove leading and trailing quotes - v = v[1 : len(v)-1] + value = value[1 : len(value)-1] } - env[parts[0]] = v + env[parts[0]] = value } } return env, nil From 40e125e61516ab401084da65eed36095b2b3850a Mon Sep 17 00:00:00 2001 From: Qasim Sarfraz Date: Fri, 27 Oct 2023 21:53:30 +0200 Subject: [PATCH 220/226] readme: Fix path for tutorial.md (#120) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aa764f47..b9a5d9a0 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ Now you can run the Innovation Engine tutorial with the following command: ```bash -./bin/ie execute scenarios/demos/tutorial.md +./bin/ie execute tutorial.md ``` The general format to run an executable document is: From c08329c86c99ec754514b13e967ca61e1f929513 Mon Sep 17 00:00:00 2001 From: rguptar <69279773+rguptar@users.noreply.github.com> Date: Mon, 30 Oct 2023 10:08:30 -0700 Subject: [PATCH 221/226] [fix] remove horizontal align (#121) * [fix] remove horizontal align * [update] command output to not be tabbed --- internal/engine/execution.go | 6 +++--- internal/ui/text.go | 24 +++++++++++++++++++++++- internal/ui/text_test.go | 17 +++++++++++++++++ 3 files changed, 43 insertions(+), 4 deletions(-) create mode 100644 internal/ui/text_test.go diff --git a/internal/engine/execution.go b/internal/engine/execution.go index cf2678e1..a72d2d00 100644 --- a/internal/engine/execution.go +++ b/internal/engine/execution.go @@ -76,8 +76,8 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) erro err := az.SetSubscription(e.Configuration.Subscription) if err != nil { logging.GlobalLogger.Errorf("Invalid Config: Failed to set subscription: %s", err) - azureStatus.SetError(err) - environments.ReportAzureStatus(azureStatus, e.Configuration.Environment) + azureStatus.SetError(err) + environments.ReportAzureStatus(azureStatus, e.Configuration.Environment) return err } @@ -191,7 +191,7 @@ func (e *Engine) ExecuteAndRenderSteps(steps []Step, env map[string]string) erro fmt.Printf("\r %s \n", ui.CheckStyle.Render("✔")) terminal.MoveCursorPositionDown(lines) - fmt.Printf(" %s\n", ui.VerboseStyle.Render(commandOutput.StdOut)) + fmt.Printf("%s\n", ui.RemoveHorizontalAlign(ui.VerboseStyle.Render(commandOutput.StdOut))) // Extract the resource group name from the command output if // it's not already set. diff --git a/internal/ui/text.go b/internal/ui/text.go index 278638ce..3e5094c5 100644 --- a/internal/ui/text.go +++ b/internal/ui/text.go @@ -1,6 +1,10 @@ package ui -import "github.com/charmbracelet/lipgloss" +import ( + "strings" + + "github.com/charmbracelet/lipgloss" +) // Styles used for rendering output to the terminal. var ( @@ -22,3 +26,21 @@ var ( ErrorMessageStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#FF5733")) OcdStatusUpdateStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("#000000")) ) + +func RemoveHorizontalAlign(s string) string { + return strings.Join( + mapSliceString( + strings.Split(s, "\n"), + func(s string) string { return strings.TrimRight(s, " ") }, + ), + "\n", + ) +} + +func mapSliceString(slice []string, apply func(string) string) []string { + var result []string + for _, s := range slice { + result = append(result, apply(s)) + } + return result +} diff --git a/internal/ui/text_test.go b/internal/ui/text_test.go new file mode 100644 index 00000000..885b5e6d --- /dev/null +++ b/internal/ui/text_test.go @@ -0,0 +1,17 @@ +package ui + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestVerboseStyle(t *testing.T) { + text := `aaaa + b` + styledText := VerboseStyle.Render(text) + expectedStyledText := `aaaa + b ` + assert.Equal(t, expectedStyledText, styledText) + assert.Equal(t, text, RemoveHorizontalAlign(styledText)) +} From 94727a39b7c695decfdd3fa6e84510d061eefbb2 Mon Sep 17 00:00:00 2001 From: Jose Blanquicet Date: Thu, 5 Oct 2023 11:48:53 +0200 Subject: [PATCH 222/226] cmd/ie/commands: Improve usage description for test command Signed-off-by: Jose Blanquicet --- cmd/ie/commands/test.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/cmd/ie/commands/test.go b/cmd/ie/commands/test.go index f1ddcd8c..5915b364 100644 --- a/cmd/ie/commands/test.go +++ b/cmd/ie/commands/test.go @@ -19,11 +19,10 @@ func init() { } var testCommand = &cobra.Command{ - Use: "test", + Use: "test [markdown file]", Args: cobra.MinimumNArgs(1), Short: "Test document commands against it's expected outputs.", Run: func(cmd *cobra.Command, args []string) { - markdownFile := args[0] if markdownFile == "" { cmd.Help() @@ -39,7 +38,6 @@ var testCommand = &cobra.Command{ Subscription: subscription, CorrelationId: "", }) - if err != nil { logging.GlobalLogger.Errorf("Error creating engine %s", err) fmt.Printf("Error creating engine %s", err) @@ -58,6 +56,5 @@ var testCommand = &cobra.Command{ } innovationEngine.TestScenario(scenario) - }, } From 5e116e5df062fa66fbaa5cde92c4d9c05850ae90 Mon Sep 17 00:00:00 2001 From: Jose Blanquicet Date: Thu, 5 Oct 2023 11:50:07 +0200 Subject: [PATCH 223/226] cmd/ie/commands/root: Propagate error instead of exiting Signed-off-by: Jose Blanquicet --- cmd/ie/commands/root.go | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/cmd/ie/commands/root.go b/cmd/ie/commands/root.go index 9c4dcc4a..b6865764 100644 --- a/cmd/ie/commands/root.go +++ b/cmd/ie/commands/root.go @@ -14,27 +14,24 @@ import ( var rootCommand = &cobra.Command{ Use: "ie", Short: "The innovation engine.", - PersistentPreRun: func(cmd *cobra.Command, args []string) { + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { logLevel, err := cmd.Flags().GetString("log-level") if err != nil { - fmt.Printf("Error getting log level: %s", err) - os.Exit(1) + return fmt.Errorf("getting log level: %w", err) } logging.Init(logging.LevelFromString(logLevel)) // Check environment environment, err := cmd.Flags().GetString("environment") if err != nil { - fmt.Printf("Error getting environment: %s", err) - logging.GlobalLogger.Errorf("Error getting environment: %s", err) - os.Exit(1) + return fmt.Errorf("getting environment: %w", err) } if !environments.IsValidEnvironment(environment) { - fmt.Printf("Invalid environment: %s", environment) - logging.GlobalLogger.Errorf("Invalid environment: %s", err) - os.Exit(1) + return fmt.Errorf("validating environment: %w", err) } + + return nil }, } @@ -49,7 +46,7 @@ func ExecuteCLI() { StringArray("feature", []string{}, "Enables the specified feature. Format: --feature ") if err := rootCommand.Execute(); err != nil { - fmt.Println(err) + fmt.Printf("Error executing command: %s\n", err) logging.GlobalLogger.Errorf("Error executing command: %s", err) os.Exit(1) } From 5291a2c399de53b29d7095117a9a5c909068f452 Mon Sep 17 00:00:00 2001 From: Jose Blanquicet Date: Thu, 5 Oct 2023 12:44:25 +0200 Subject: [PATCH 224/226] cmd/ie/commands/execute: Propagate error instead of exiting Signed-off-by: Jose Blanquicet --- cmd/ie/commands/execute.go | 43 ++++++++++---------------------------- 1 file changed, 11 insertions(+), 32 deletions(-) diff --git a/cmd/ie/commands/execute.go b/cmd/ie/commands/execute.go index b6075928..27514edf 100644 --- a/cmd/ie/commands/execute.go +++ b/cmd/ie/commands/execute.go @@ -1,12 +1,11 @@ package commands import ( + "errors" "fmt" - "os" "strings" "github.com/Azure/InnovationEngine/internal/engine" - "github.com/Azure/InnovationEngine/internal/logging" "github.com/spf13/cobra" ) @@ -37,12 +36,10 @@ var executeCommand = &cobra.Command{ Use: "execute [markdown file]", Args: cobra.MinimumNArgs(1), Short: "Execute the commands for an Azure deployment scenario.", - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { markdownFile := args[0] if markdownFile == "" { - logging.GlobalLogger.Errorf("Error: No markdown file specified.") - cmd.Help() - os.Exit(1) + return errors.New("no markdown file specified") } verbose, _ := cmd.Flags().GetBool("verbose") @@ -64,13 +61,7 @@ var executeCommand = &cobra.Command{ for _, environmentVariable := range environmentVariables { keyValuePair := strings.SplitN(environmentVariable, "=", 2) if len(keyValuePair) != 2 { - logging.GlobalLogger.Errorf( - "Error: Invalid environment variable format: %s", - environmentVariable, - ) - fmt.Printf("Error: Invalid environment variable format: %s", environmentVariable) - cmd.Help() - os.Exit(1) + return fmt.Errorf("invalid environment variable format: %s", environmentVariable) } cliEnvironmentVariables[keyValuePair[0]] = keyValuePair[1] @@ -81,13 +72,7 @@ var executeCommand = &cobra.Command{ case "render-values": renderValues = true default: - logging.GlobalLogger.Errorf( - "Error: Invalid feature: %s", - feature, - ) - fmt.Printf("Error: Invalid feature: %s\n", feature) - cmd.Help() - os.Exit(1) + return fmt.Errorf("invalid feature: %s", feature) } } @@ -98,9 +83,7 @@ var executeCommand = &cobra.Command{ cliEnvironmentVariables, ) if err != nil { - logging.GlobalLogger.Errorf("Error creating scenario: %s", err) - fmt.Printf("Error creating scenario: %s", err) - os.Exit(1) + return fmt.Errorf("creating scenario: %w", err) } innovationEngine, err := engine.NewEngine(engine.EngineConfiguration{ @@ -112,19 +95,15 @@ var executeCommand = &cobra.Command{ WorkingDirectory: workingDirectory, RenderValues: renderValues, }) - if err != nil { - logging.GlobalLogger.Errorf("Error creating engine: %s", err) - fmt.Printf("Error creating engine: %s", err) - os.Exit(1) + return fmt.Errorf("creating engine: %w", err) } // Execute the scenario - err = innovationEngine.ExecuteScenario(scenario) - if err != nil { - logging.GlobalLogger.Errorf("Error executing scenario: %s", err) - fmt.Printf("Error executing scenario: %s", err) - os.Exit(1) + if err = innovationEngine.ExecuteScenario(scenario); err != nil { + return fmt.Errorf("executing scenario: %w", err) } + + return nil }, } From 459ffe338e7e87d826840f10b9be1f2fb96898bf Mon Sep 17 00:00:00 2001 From: Jose Blanquicet Date: Thu, 5 Oct 2023 12:44:34 +0200 Subject: [PATCH 225/226] cmd/ie/commands/test: Propagate error instead of exiting Signed-off-by: Jose Blanquicet --- cmd/ie/commands/test.go | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/cmd/ie/commands/test.go b/cmd/ie/commands/test.go index 5915b364..9198fd4d 100644 --- a/cmd/ie/commands/test.go +++ b/cmd/ie/commands/test.go @@ -1,11 +1,10 @@ package commands import ( + "errors" "fmt" - "os" "github.com/Azure/InnovationEngine/internal/engine" - "github.com/Azure/InnovationEngine/internal/logging" "github.com/spf13/cobra" ) @@ -22,11 +21,10 @@ var testCommand = &cobra.Command{ Use: "test [markdown file]", Args: cobra.MinimumNArgs(1), Short: "Test document commands against it's expected outputs.", - Run: func(cmd *cobra.Command, args []string) { + RunE: func(cmd *cobra.Command, args []string) error { markdownFile := args[0] if markdownFile == "" { - cmd.Help() - return + return errors.New("no markdown file specified") } verbose, _ := cmd.Flags().GetBool("verbose") @@ -39,9 +37,7 @@ var testCommand = &cobra.Command{ CorrelationId: "", }) if err != nil { - logging.GlobalLogger.Errorf("Error creating engine %s", err) - fmt.Printf("Error creating engine %s", err) - os.Exit(1) + return fmt.Errorf("creating engine: %w", err) } scenario, err := engine.CreateScenarioFromMarkdown( @@ -50,11 +46,13 @@ var testCommand = &cobra.Command{ nil, ) if err != nil { - logging.GlobalLogger.Errorf("Error creating scenario %s", err) - fmt.Printf("Error creating engine %s", err) - os.Exit(1) + return fmt.Errorf("creating scenario: %w", err) } - innovationEngine.TestScenario(scenario) + if err := innovationEngine.TestScenario(scenario); err != nil { + return fmt.Errorf("testing scenario: %w", err) + } + + return nil }, } From ecf041a53ba9bc6454ee63cb689865c68b5d3e48 Mon Sep 17 00:00:00 2001 From: Jose Blanquicet Date: Thu, 5 Oct 2023 12:44:43 +0200 Subject: [PATCH 226/226] cmd/ie/commands/to-bash: Propagate error instead of exiting Signed-off-by: Jose Blanquicet --- cmd/ie/commands/to-bash.go | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/cmd/ie/commands/to-bash.go b/cmd/ie/commands/to-bash.go index 248e1427..9dfa94bd 100644 --- a/cmd/ie/commands/to-bash.go +++ b/cmd/ie/commands/to-bash.go @@ -8,7 +8,6 @@ import ( "github.com/Azure/InnovationEngine/internal/engine" "github.com/Azure/InnovationEngine/internal/engine/environments" - "github.com/Azure/InnovationEngine/internal/logging" "github.com/spf13/cobra" ) @@ -22,8 +21,7 @@ var toBashCommand = &cobra.Command{ RunE: func(cmd *cobra.Command, args []string) error { markdownFile := args[0] if markdownFile == "" { - logging.GlobalLogger.Errorf("Error: No markdown file specified.") - return errors.New("error: No markdown file specified") + return errors.New("no markdown file specified") } environment, _ := cmd.Flags().GetString("environment") @@ -34,16 +32,7 @@ var toBashCommand = &cobra.Command{ for _, environmentVariable := range environmentVariables { keyValuePair := strings.SplitN(environmentVariable, "=", 2) if len(keyValuePair) != 2 { - logging.GlobalLogger.Errorf( - "Error: Invalid environment variable format: %s", - environmentVariable, - ) - fmt.Printf("Error: Invalid environment variable format: %s", environmentVariable) - cmd.Help() - return fmt.Errorf( - "error: Invalid environment variable format, %s", - environmentVariable, - ) + return fmt.Errorf("invalid environment variable format: %s", environmentVariable) } cliEnvironmentVariables[keyValuePair[0]] = keyValuePair[1] @@ -54,11 +43,8 @@ var toBashCommand = &cobra.Command{ markdownFile, []string{"bash", "azurecli", "azurecli-interactive", "terraform"}, cliEnvironmentVariables) - if err != nil { - logging.GlobalLogger.Errorf("Error creating scenario: %s", err) - fmt.Printf("Error creating scenario: %s", err) - return err + return fmt.Errorf("creating scenario: %w", err) } // If within cloudshell, we need to wrap the script in a json object to @@ -66,11 +52,8 @@ var toBashCommand = &cobra.Command{ if environments.IsAzureEnvironment(environment) { script := AzureScript{Script: scenario.ToShellScript()} scriptJson, err := json.Marshal(script) - if err != nil { - logging.GlobalLogger.Errorf("Error converting to json: %s", err) - fmt.Printf("Error converting to json: %s", err) - return err + return fmt.Errorf("converting to json: %w", err) } fmt.Printf("ie_us%sie_ue\n", scriptJson) @@ -79,7 +62,6 @@ var toBashCommand = &cobra.Command{ } return nil - }, }