From abdc8391442a39e08cf146fb07977e762c4eef78 Mon Sep 17 00:00:00 2001 From: Patrick Kremer Date: Mon, 28 Feb 2022 12:12:32 -0600 Subject: [PATCH 01/54] feat: Slack alert when VMs are on unapproved portgroups Closes #812 Signed-off-by: Patrick Kremer --- docs/site/examples.md | 11 +- .../powercli/kn-pcli-pg-check/Dockerfile | 5 + .../powercli/kn-pcli-pg-check/README.md | 297 +++++++++++++++++ .../powercli/kn-pcli-pg-check/function.yaml | 41 +++ .../powercli/kn-pcli-pg-check/handler.ps1 | 315 ++++++++++++++++++ .../kn-pcli-pg-check/kn-pcli-pg-slack.png | Bin 0 -> 78734 bytes .../kn-pcli-pg-check/pg_check_secret.json | 10 + .../test/docker-test-env-variable | 1 + .../test/send-cloudevent-test.ps1 | 26 ++ .../test/send-cloudevent-test.sh | 23 ++ .../kn-pcli-pg-check/test/test-payload.json | 158 +++++++++ 11 files changed, 886 insertions(+), 1 deletion(-) create mode 100644 examples/knative/powercli/kn-pcli-pg-check/Dockerfile create mode 100644 examples/knative/powercli/kn-pcli-pg-check/README.md create mode 100644 examples/knative/powercli/kn-pcli-pg-check/function.yaml create mode 100644 examples/knative/powercli/kn-pcli-pg-check/handler.ps1 create mode 100644 examples/knative/powercli/kn-pcli-pg-check/kn-pcli-pg-slack.png create mode 100644 examples/knative/powercli/kn-pcli-pg-check/pg_check_secret.json create mode 100644 examples/knative/powercli/kn-pcli-pg-check/test/docker-test-env-variable create mode 100644 examples/knative/powercli/kn-pcli-pg-check/test/send-cloudevent-test.ps1 create mode 100644 examples/knative/powercli/kn-pcli-pg-check/test/send-cloudevent-test.sh create mode 100644 examples/knative/powercli/kn-pcli-pg-check/test/test-payload.json diff --git a/docs/site/examples.md b/docs/site/examples.md index 83ab326f..06848694 100644 --- a/docs/site/examples.md +++ b/docs/site/examples.md @@ -112,7 +112,16 @@ examples: description: Function to enforce the `Notify Switches` value on a distributed virtual switch portgroup. Any changes to the `Notify Switches` value will be intercepted by the function and reset to the desired value. links: - language: powercli - url: "/tree/master/knative/powercli/kn-pcli-vds-pg-config" + url: "/tree/master/knative/powercli/kn-pcli-vds-pg-config" + + - title: Portgroup Compliance Check + usecases: + - item: notification + id: kn-pcli-pg-check + description: Creates a Slack notification when VM network portgroups are out of compliance. Tag VMs as `PCI`, tag portgroups as `PCI`, receive a Slack notification any time a tagged VM is moved off of a `PCI` portgroup. + links: + - language: powercli + url: "/tree/master/knative/powercli/kn-pcli-pg-check" - title: Enhancing vSphere Alarm Actions usecases: diff --git a/examples/knative/powercli/kn-pcli-pg-check/Dockerfile b/examples/knative/powercli/kn-pcli-pg-check/Dockerfile new file mode 100644 index 00000000..49b2c980 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-pg-check/Dockerfile @@ -0,0 +1,5 @@ +FROM us.gcr.io/daisy-284300/veba/ce-pcli-base:1.4 + +COPY handler.ps1 handler.ps1 + +CMD ["pwsh","./server.ps1"] diff --git a/examples/knative/powercli/kn-pcli-pg-check/README.md b/examples/knative/powercli/kn-pcli-pg-check/README.md new file mode 100644 index 00000000..26ea34fa --- /dev/null +++ b/examples/knative/powercli/kn-pcli-pg-check/README.md @@ -0,0 +1,297 @@ +# kn-pcli-pg-check +Example Knative PowerCLI function kn-pcli-pg-check + +This function creates a Slack notification when VM network portgroups are out of compliance. The original intent was to ensure that VMs tagged as PCI VMs remain on portgroups tagged as PCI portgroups. Any time the VM is edited and a portgroup is changed to a non-PCI portgroup, the function alerts via Slack. + +# Step 1 - Build + +> **Note:** This step is only required if you made code changes to `handler.ps1` +> or `Dockerfile`. + +Create the container image locally to test your function logic. + +Mac/Linux +``` +# change the IMAGE name accordingly, example below for Docker +export IMAGE=/kn-pcli-pg-check:1.0 +docker build -t ${IMAGE} . +``` + +Windows +``` +# change the IMAGE name accordingly, example below for Docker +$IMAGE="/kn-pcli-pg-check:1.0" +docker build -t ${IMAGE} . +``` +# Step 2 - Test + +## Configure your vCenter + +In order for this function to work, you must assign at least one vCenter tag to a virtual machine, +and one vCenter tag to a virtual network. +This function supports multiple tags. You can configure your tags in any way suitable for your +environment. You do not have to configure multiple portgroups, nor a mix of standard and +distributed portgroups. +This function was tested with the folowing configuration: + +Tags +- New Tag Category named `PCI` +- New Tag named `PCI-VM` in Category `PCI` +- New Tag named `PCI-VM2` in Category `PCI` +- New Tag named `PCI-Network` in Category `PCI` + +Port Groups +- New Distributed Virtual Portgroup named `DVS_PG_PCI_01`, tagged with `PCI/PCI-Network` +- New Distributed Virtual Portgroup named `DVS_PG_PCI_02`, tagged with `PCI/PCI-Network` +- New Distributed Virtual Portgroup named `DVS_PG_NOT_PCI`, no vSphere tag +- Existing Standard Virtual Portgroup named `VSS_VM_VLAN_203`, no vSphere tag + +Virtual Machine +- New Virtual Machine named `tinycore-2`, tagged with `PCI/PCI-VM` +- Network Adapter 1 assigned to `DVS_PG_PCI_01` +- Network Adapter 2 assigned to `VSS_VM_VLAN_203` +- Network Adapter 3 assigned to `DVS_PG_NOT_PCI` +## Test your container image + +Verify the container image works by executing it locally. + +Change into the `test` directory +```console +cd test +``` + +Update the following variable names within the `docker-test-env-variable` file: + +* `VCENTER_SERVER` - IP Address or FQDN of the vCenter Server to connect to +* `VCENTER_USERNAME` - vCenter account with permission to reconfigure distributed virtual switches +* `VCENTER_PASSWORD` - vCenter password associated with the username +* `VCENTER_CERTIFCATE_ACTION` - Set-PowerCLIConfiguration Action to configure when connection fails due to certificate error, default is Fail. (Possible values: Fail, Ignore or Warn) +* `VM_WATCH_TAGS` - vCenter tags indicating a VM belongs on the PCI segment. In this example, `PCI/PCI-VM` and `PCI/PCI-VM2`. +* `PG_WATCH_TAGS` - vCenter tags indicating a portgroup is a valid PCI segment. In this example, `PCI/PCI-Network` and `PCI/PCI-Network2` +* `SLACK_WEBHOOK_URL` - Slack webhook URL + +If you built a custom image in Step 1, comment out the default `IMAGE` command below - the `docker run` command will then use use the value previously stored in the `IMAGE` variable. Otherwise, use the default image as shown below. Start the container image by running the following commands: + +Mac/Linux +```console +export IMAGE=us.gcr.io/daisy-284300/veba/kn-pg-check:1.0 +docker run -e FUNCTION_DEBUG=true -e PORT=8080 --env-file docker-test-env-variable -it --rm -p 8080:8080 ${IMAGE} +``` +Windows +```console +$IMAGE="us.gcr.io/daisy-284300/veba/kn-pcli-pg-check:1.0" +docker run -e FUNCTION_DEBUG=true -e PORT=8080 --env-file docker-test-env-variable -it --rm -p 8080:8080 ${IMAGE} +``` +# Configure the payload file + +In the `test` directory, edit `test-payload.json`. Locate the `Vm` section of the JSON file. Change the `Name:` property from `REPLACE-ME` to the name of a the VM you configured above. Locate the `Vm.Vm` section of the JSON file. Change the `Value` property to the object ID of the VM. One way to retrieve the object ID is using the PowerCLI `Get-VM` cmdlet + +```powershell +C:\> (Get-VM "tinycore-2").id +VirtualMachine-vm-6001 +``` + +```json + "Vm": { + "Name": "REPLACE-ME", + "Vm": { + "Type": "VirtualMachine", + "Value": "REPLACE-ME" + } +``` +For our example VM named `tinycore-2`, the relevant JSON looks like this: +```json + "Vm": { + "Name": "tinycore-2", + "Vm": { + "Type": "VirtualMachine", + "Value": "vm-6001" + } +``` + +If you do not make this change, the function will fail because it will be unable to locate the virtual machine in inventory. + +--- + +In a separate terminal, run either `send-cloudevent-test.ps1` (PowerShell Script) or `send-cloudevent-test.sh` (Bash Script) to simulate a CloudEvent payload being sent to the local container image. When run with no arguments, the scripts will send the contents of `test-payload.json` as the payload. If you pass the scripts a different filename as an argument, they will send the contents of the specified file instead. Example: `send-cloudevent-test.ps1 test-payload2.json`. This testing technique is useful when writing complex functions with varying payloads. + +```console +Testing Function ... +See docker container console for output + +# Output from docker container console +03/03/2022 22:23:05 - PowerShell HTTP server start listening on 'http://*:8080/' +03/03/2022 22:23:05 - Processing Init + +03/03/2022 22:23:05 - Configuring PowerCLI Configuration Settings +03/03/2022 22:23:06 - Connecting to vCenter Server vc02.lab.local + +IsConnected : True +Id : /VIServer=vsphere.local\administrator@vc02.lab.local:443/ +ServiceUri : https://vc02.lab.local/sdk +SessionSecret : "58d1ce3755ad6f35af5135c1139e03ea52898d5a" +Name : vc02.lab.local +Port : 443 +SessionId : "58d1ce3755ad6f35af5135c1139e03ea52898d5a" +User : VSPHERE.LOCAL\Administrator +Uid : /VIServer=vsphere.local\administrator@vc02.lab.local:443/ +Version : 7.0.2 +Build : 17920168 +ProductLine : vpx +InstanceUuid : 9db92255-bc00-4e91-9777-0e37928c5771 +RefCount : 1 +ExtensionData : VMware.Vim.ServiceInstance + +03/03/2022 22:23:08 - Successfully connected to vc02.lab.local + +03/03/2022 22:23:08 - Init Processing Completed + +03/03/2022 22:23:08 - Starting HTTP CloudEvent listener +03/03/2022 22:23:10 - DEBUG: ConfigSpec.DeviceChange Is Null? False + +03/03/2022 22:23:10 - DEBUG: Vm.Vm.Type Is Null? False + +03/03/2022 22:23:10 - DEBUG: Vm.Vm.Value Is Null? False + +03/03/2022 22:23:10 - DEBUG: vmID is VirtualMachine-vm-6001 + +03/03/2022 22:23:10 - Retrieving tags on tinycore-2 + +03/03/2022 22:23:12 - DEBUG: Tags found on VM: PCI/PCI-VM TestBedVMs/TestBed1 + +03/03/2022 22:23:12 - DEBUG: Tags to monitor: PCI/PCI-VM PCI/PCI-VM2 + +03/03/2022 22:23:12 - DEBUG: Comparing VM tag: PCI/PCI-VM on VM tinycore-2 + +03/03/2022 22:23:12 - DEBUG: Comparing watch tag: PCI/PCI-VM + +03/03/2022 22:23:12 - DEBUG: Match found for: PCI/PCI-VM, breaking out of loop + +03/03/2022 22:23:12 - Match found for tinycore-2, checking portgroups + +03/03/2022 22:23:12 - DEBUG: VM tinycore-2 - NIC Network adapter 1 - PortGroup DVS_PG_PCI_01 + +03/03/2022 22:23:12 - DEBUG: DVS Port Group Key is dvportgroup-8004 + +03/03/2022 22:23:12 - DEBUG: Virtual Network ID: DistributedVirtualPortgroup-dvportgroup-8004 + +03/03/2022 22:23:12 - DEBUG: PortGroup Tag: PCI/PCI-Network + +03/03/2022 22:23:12 - INFO: Found a match on PCI/PCI-Network + +03/03/2022 22:23:12 - DEBUG: VM tinycore-2 - NIC Network adapter 2 - PortGroup VSS_VM_VLAN_203 + +03/03/2022 22:23:12 - DEBUG: VSS Backing network is Network-network-63 + +03/03/2022 22:23:12 - DEBUG: Virtual Network ID: Network-network-63 + +03/03/2022 22:23:12 - DEBUG: VM tinycore-2 - NIC Network adapter 3 - PortGroup DVS_PG_NOT_PCI + +03/03/2022 22:23:12 - DEBUG: DVS Port Group Key is dvportgroup-9002 + +03/03/2022 22:23:12 - DEBUG: Virtual Network ID: DistributedVirtualPortgroup-dvportgroup-9002 + +03/03/2022 22:23:12 - DEBUG: PortGroup Tag: PCI/Non-PCI-Networks + +03/03/2022 22:23:12 - INFO: No permitted tags were found on the portgroup + +03/03/2022 22:23:12 - NICs using unapproved portgroups: + +Network adapter 2 VSS_VM_VLAN_203 +Network adapter 3 DVS_PG_NOT_PCI +03/03/2022 22:23:12 - DEBUG: "{ + "attachments": [ + { + "pretext": "Virtual Machine - Portgroup Alert", + "fields": [ + { + "short": "false", + "title": "EventType", + "value": "VmReconfiguredEvent" + }, + { + "short": "false", + "title": "Username", + "value": "VSPHERE.LOCAL\\Administrator" + }, + { + "short": "false", + "title": "DateTime", + "value": "2022-02-24T20:01:17.280999Z" + }, + { + "short": "false", + "title": "Full Message", + "value": "NICs using unapproved portgroups:\nNetwork adapter 2 - VSS_VM_VLAN_203\nNetwork adapter 3 - DVS_PG_NOT_PCI\n\n\nReconfigured tinycore-2 on esx02.lab.local in HomeLab. \n \nModified: \n \nconfig.hardware.device(4000).backing.port.portgroupKey: \"dvportgroup-1006\" -> \"dvportgroup-8004\"; \n\nconfig.hardware.device(4000).backing.port.portKey: \"4\" -> \"65\"; \n\nconfig.hardware.device(4000).backing.port.connectionCookie: 174899823 -> 193390024; \n\n Added: \n \n Deleted: \n \n" + } + ] + } + ] +}" +03/03/2022 22:23:12 - Sending Webhook payload to Slack ... +03/03/2022 22:23:13 - Successfully sent Webhook ... +03/03/2022 22:23:13 - PG Check operation complete ... + +03/03/2022 22:23:13 - Handler Processing Completed ... +``` + +You should see a Slack alert similar to this. In our test setup, Network Adapter 1 was connected to a portgroup tagged as PCI. It does not show up on the unapproved list in the Slack message below. The other two NICs do show up. + +![Img](kn-pcli-pg-slack.png) + +> Pro Tip - If you are rapidly iterating on the code and want to easily rebuild and launch the container, +> you can chain all of the commands together with ampersands. This will allow you to re-run +> the commands by simply pressing the `up` arrow and `Enter`. + +```console +cd .. && docker build -t ${IMAGE} . && cd test && docker run -e FUNCTION_DEBUG=true -e PORT=8080 --env-file docker-test-env-variable -it --rm -p 8080:8080 ${IMAGE} +``` +# Step 3 - Deploy + +> **Note:** The following steps assume a working Knative environment using the +`default` Rabbit `broker`. The Knative `service` and `trigger` will be installed in the +`vmware-functions` Kubernetes namespace, assuming that the `broker` is also available there. + +If you built a custom image, push it to an accessible registry such as Docker once you're done developing and testing your function logic. + +```console +docker push ${IMAGE} +``` + +Update the `pg_check_secret.json` file with the same vCenter Server credentials and configurations that you used in `test/docker-test-env-variable` and then create the kubernetes secret which can then be accessed from within the function by using the environment variable named called `PG_CHECK_SECRET`. + +```console +# create secret +kubectl -n vmware-functions create secret generic pg-check-secret --from-file=PG_CHECK_SECRET=pg_check_secret.json + +# update label for secret to show up in VEBA UI +kubectl -n vmware-functions label secret pg-check-secret app=veba-ui +``` + +Edit the `function.yaml` file with the name of the container image from Step 1 if you made any changes. If not, the default VMware container image will suffice. By default, the function deployment will filter on the `VmReconfiguredEvent` vCenter Server Event. If you wish to change this, update the `subject` field within `function.yaml` to the desired event type. + +Deploy the function to the VMware Event Broker Appliance (VEBA). + +```console +# deploy function +kubectl -n vmware-functions apply -f function.yaml +``` + +For testing purposes, the `function.yaml` contains the following annotations, which will ensure the Knative Service Pod will always run **exactly** one instance for debugging purposes. Functions deployed through through the VMware Event Broker Appliance UI defaults to scale to 0, which means the pods will only run when it is triggered by an vCenter Event. + +```yaml +annotations: + autoscaling.knative.dev/maxScale: "1" + autoscaling.knative.dev/minScale: "1" +``` + +# Step 4 - Undeploy + +```console +# undeploy function + +kubectl -n vmware-functions delete -f function.yaml + +# delete secret +kubectl -n vmware-functions delete secret function-secret +``` diff --git a/examples/knative/powercli/kn-pcli-pg-check/function.yaml b/examples/knative/powercli/kn-pcli-pg-check/function.yaml new file mode 100644 index 00000000..7f501ddb --- /dev/null +++ b/examples/knative/powercli/kn-pcli-pg-check/function.yaml @@ -0,0 +1,41 @@ +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: kn-pcli-pg-check + labels: + app: veba-ui +spec: + template: + metadata: + annotations: + autoscaling.knative.dev/maxScale: "1" + autoscaling.knative.dev/minScale: "1" + spec: + containers: + - image: kremerpatrick/kn-pcli-pg-check:1.0 + envFrom: + - secretRef: + name: pg-check-secret + env: + - name: FUNCTION_DEBUG + value: "true" +--- +apiVersion: eventing.knative.dev/v1 +kind: Trigger +metadata: + name: veba-pcli-pg-check-trigger + labels: + app: veba-ui +spec: + broker: default + filter: + attributes: + type: com.vmware.event.router/event + # Replace this subject with the event you need to trigger on + # Then, edit send-cloudevent-test.ps1 and send-cloudevent-test.sh in the /test folder + subject: VmReconfiguredEvent + subscriber: + ref: + apiVersion: serving.knative.dev/v1 + kind: Service + name: kn-pcli-pg-check diff --git a/examples/knative/powercli/kn-pcli-pg-check/handler.ps1 b/examples/knative/powercli/kn-pcli-pg-check/handler.ps1 new file mode 100644 index 00000000..928ef913 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-pg-check/handler.ps1 @@ -0,0 +1,315 @@ +Function Process-Init { + [CmdletBinding()] + param() + Write-Host "$(Get-Date) - Processing Init`n" + + try { + $jsonSecrets = ${env:PG_CHECK_SECRET} | ConvertFrom-Json + } + catch { + throw "`nK8s secret `$env:PG_CHECK_SECRET does not look to be defined" + } + + # Extract all tag secrets for ease of use in function + $VCENTER_SERVER = ${jsonSecrets}.VCENTER_SERVER + $VCENTER_USERNAME = ${jsonSecrets}.VCENTER_USERNAME + $VCENTER_PASSWORD = ${jsonSecrets}.VCENTER_PASSWORD + $VCENTER_CERTIFICATE_ACTION = ${jsonSecrets}.VCENTER_CERTIFICATE_ACTION + + # Configure TLS 1.2/1.3 support as this is required for latest vSphere release + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 -bor [System.Net.SecurityProtocolType]::Tls13 + + Write-Host "$(Get-Date) - Configuring PowerCLI Configuration Settings`n" + Set-PowerCLIConfiguration -InvalidCertificateAction:${VCENTER_CERTIFICATE_ACTION} -ParticipateInCeip:$true -Confirm:$false + + Write-Host "$(Get-Date) - Connecting to vCenter Server $VCENTER_SERVER`n" + + try { + Connect-VIServer -Server $VCENTER_SERVER -User $VCENTER_USERNAME -Password $VCENTER_PASSWORD + } + catch { + Write-Error "$(Get-Date) - ERROR: Failed to connect to vCenter Server" + throw $_ + } + + Write-Host "$(Get-Date) - Successfully connected to $VCENTER_SERVER`n" + + Write-Host "$(Get-Date) - Init Processing Completed`n" +} + +Function Process-Shutdown { + [CmdletBinding()] + param() + Write-Host "$(Get-Date) - Processing Shutdown`n" + + Write-Host "$(Get-Date) - Disconnecting from vCenter Server`n" + + try { + Disconnect-VIServer * -Confirm:$false + } + catch { + Write-Error "$(Get-Date) - Error: Failed to Disconnect from vCenter Server" + } + + Write-Host "$(Get-Date) - Shutdown Processing Completed`n" +} + +Function Process-Handler { + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=$true)][CloudNative.CloudEvents.CloudEvent]$CloudEvent + ) + + # Decode CloudEvent + try { + $cloudEventData = $cloudEvent | Read-CloudEventJsonData -Depth 10 + } + catch { + throw "`nPayload must be JSON encoded" + } + + try { + $jsonSecrets = ${env:PG_CHECK_SECRET} | ConvertFrom-Json + } + catch { + throw "`nK8s secret `$env:PG_CHECK_SECRET does not look to be defined" + } + + $VM_WATCH_TAGS = ${jsonSecrets}.VM_WATCH_TAGS + $PG_WATCH_TAGS = ${jsonSecrets}.PG_WATCH_TAGS + + $deviceUnchanged = ($NULL -eq $cloudEventData.ConfigSpec.DeviceChange ) + if(${env:FUNCTION_DEBUG} -eq "true") { + Write-Host "$(Get-Date) - DEBUG: ConfigSpec.DeviceChange Is Null? $($deviceUnchanged)`n" + } + + # If no devices changed, then the NIC could not have been updated to a different portgroup + if ($deviceUnchanged) + { + Write-Host "$(Get-Date) - No devices changed.`n" + return + } + + # Build the MoRef ID + $vmID = $cloudEventData.Vm.Vm.Type + "-" + $cloudEventData.Vm.Vm.Value + if(${env:FUNCTION_DEBUG} -eq "true") { + Write-Host "$(Get-Date) - DEBUG: Vm.Vm.Type Is Null? $($NULL -eq $cloudEventData.Vm.Vm.Type)`n" + Write-Host "$(Get-Date) - DEBUG: Vm.Vm.Value Is Null? $($NULL -eq $cloudEventData.Vm.Vm.Value)`n" + Write-Host "$(Get-Date) - DEBUG: vmID is $vmID`n" + } + + # Retrieve the VM object by MoRef ID + try { + $vm = Get-VM -id $vmID + } + catch { + Write-Host "$(Get-Date) - ERROR: unable to retrieve VM ID $vmID`n" + throw $_ + } + + # Retrieve all tags on the VM + Write-Host "$(Get-Date) - Retrieving tags on $($vm.Name)`n" + try { + $vmTags = $vm | Get-TagAssignment + } + catch { + Write-Host "$(Get-Date) - ERROR: unable to retrieve tags for $($vm.Name)`n" + throw $_ + } + + if(${env:FUNCTION_DEBUG} -eq "true") { + Write-Host "$(Get-Date) - DEBUG: Tags found on VM: $($vmTags.tag)`n" + Write-Host "$(Get-Date) - DEBUG: Tags to monitor: $($VM_WATCH_TAGS)`n" + } + + # Search through the tags found on the VM and compare them to the VM tags specified in the secret. + # If any match is found, break out of the loop - finding any match means the VM is tagged for further inspection + $checkVM = $false + :outer foreach ($tag in $vmTags) { + if(${env:FUNCTION_DEBUG} -eq "true") { + Write-Host "$(Get-Date) - DEBUG: Comparing VM tag: $($tag.Tag) on VM $($vm.Name)`n" + } + foreach ($watchTag in $VM_WATCH_TAGS) { + if(${env:FUNCTION_DEBUG} -eq "true") { + Write-Host "$(Get-Date) - DEBUG: Comparing watch tag: $($watchTag)`n" + } + if ($watchTag -eq $tag.Tag) { + if(${env:FUNCTION_DEBUG} -eq "true") { + Write-Host "$(Get-Date) - DEBUG: Match found for: $($watchTag), breaking outer loop`n" + } + $checkVM = $true + break outer + } + } + } + + # If the VM isn't tagged, no further inspection is necessary + if ($checkVM -eq $false) { + Write-Host "$(Get-Date) - VM not tagged for monitoring, no inspection required.`n" + return + } + + Write-Host "$(Get-Date) - Match found for $($vm.Name), checking portgroups`n" + + # Try retrieving the VM's network adapters + try { + $networkAdapters = $vm | Get-NetworkAdapter + } + catch { + Write-Host "$(Get-Date) - ERROR: unable to retrieve retrieve network adapters for $($vm.Name)`n" + throw $_ + } + + # This hash will store any network adapters that are attached to unapproved portgroups + $networkAdapterHash = @{} + + foreach ($nic in $networkAdapters) { + if(${env:FUNCTION_DEBUG} -eq "true") { + Write-Host "$(Get-Date) - DEBUG: VM $($vm.Name) - NIC $($nic.Name) - PortGroup $($nic.NetworkName)`n" + } + # If the portgroup is a VSS portgroup, it will have a backing network property + $vssBackingNetwork = $nic.extensiondata.Backing.Network + # If the portgroup is a DVS portgroup, it will have a PortgroupKey property + $dvPortGroupKey = $nic.extensiondata.Backing.Port.PortgroupKey + $vNetworkID = $null + + if ($NULL -ne $vssBackingNetwork) { + if(${env:FUNCTION_DEBUG} -eq "true") { + Write-Host "$(Get-Date) - DEBUG: VSS Backing network is $($vssBackingNetwork)`n" + } + # vssBackingNetwork is already in MoRef ID format + $vNetworkID = $vssBackingNetwork + } + elseif ($NULL -ne $dvPortGroupKey) { + if (${env:FUNCTION_DEBUG} -eq "true") { + Write-Host "$(Get-Date) - DEBUG: DVS Port Group Key is $($dvPortGroupKey)`n" + } + # Unlike the VSS above, We have to build the MoRef ID for VDS + $vNetworkID = "DistributedVirtualPortgroup-" + $dvPortGroupKey + } else { + Write-Host "$(Get-Date) - ERROR: Could not determine network type`n" + throw "$(Get-Date) - ERROR: Could not determine network type for $($nic.Name)`n" + } + + if(${env:FUNCTION_DEBUG} -eq "true") { + Write-Host "$(Get-Date) - DEBUG: Virtual Network ID: $($vNetworkID)`n" + } + + # Now that we've determined the ID, we can try retrieving the portgroup virtual network information + try { + $pg = Get-VirtualNetwork -Id $vNetworkID + } + catch { + Write-Host "$(Get-Date) - ERROR: unable to retrieve retrieve virtual network information for $($vNetworkID)`n" + throw $_ + } + + # Retrieve the tag assignments for the port group + try { + $pgTags = $pg | Get-TagAssignment + } + catch { + Write-Host "$(Get-Date) - ERROR: unable to retrieve retrieve tag assignments for $($pg.Name)`n" + throw $_ + } + + # If $null, the NIC is on a pg with no tags - add it to the hash table + if ($null -eq $pgTags) { + $networkAdapterHash[$nic.id] = "$($pg.Name)" + } else { + $pgMatch = $false + # Search through all tags on the portgroup, looking for match on any of the watch tags + # provided in the secret. If there is a match on any tag, stop processing - we only need + # one tag match for the portgroup to be marked as OK + :outer foreach ($pgTag in $pgTags) { + $fullTag = $pgTag.Tag.Category.ToString() + "/" + $pgTag.Tag.Name.ToString() + if(${env:FUNCTION_DEBUG} -eq "true") { + Write-Host "$(Get-Date) - DEBUG: PortGroup Tag: $($fullTag)`n" + } + + foreach ($watchTag in $PG_WATCH_TAGS) { + if ($fullTag -eq $watchTag) { + Write-Host "$(Get-Date) - INFO: Found a match on $($watchTag)`n" + $pgMatch = $true + break outer + } + } + } + + # If none of the portgroup's tags are a match, the vNIC is added to the hash table + if ($pgMatch -eq $false ) { + Write-Host "$(Get-Date) - INFO: No permitted tags were found on the portgroup`n" + $networkAdapterHash[$nic.id] = "$($pg.Name)" + } + } + } + + # Check to see if the hash is empty + if ( $networkAdapterHash.Count -eq 0 ) { + Write-Host "$(Get-Date) - INFO: All NICs are on approved portgroups" + return + } + + $msg = "NICs using unapproved portgroups:`n" + Write-Host "$(Get-Date) - $($msg)" + # Build a list of NICs and unapproved portgroups + foreach ($nic in $networkAdapters) { + if ($networkAdapterHash.ContainsKey($nic.Id)) { + Write-Host "$(Get-Date) - $($nic.Name): on unapproved portgroup $($networkAdapterHash[$nic.Id])" + $msg += $nic.Name + " - " + $networkAdapterHash[$nic.Id] + "`n" + } + } + + # Payload for Slack + $payload = @{ + attachments = @( + @{ + pretext = $(${jsonSecrets}.SLACK_MESSAGE_PRETEXT); + fields = @( + @{ + title = "EventType"; + value = $cloudEvent.Subject; + short = "false"; + } + @{ + title = "Username"; + value = $cloudEventData.UserName; + short = "false"; + } + @{ + title = "DateTime"; + value = $cloudEventData.CreatedTime; + short = "false"; + } + @{ + title = "Full Message"; + value = $msg + "`n`n" + $cloudEventData.FullFormattedMessage ; + short = "false"; + } + ) + } + ) + } + + # Convert Slack message object into JSON + $body = $payload | ConvertTo-Json -Depth 5 + + if(${env:FUNCTION_DEBUG} -eq "true") { + Write-Host "$(Get-Date) - DEBUG: `"$body`"" + } + + Write-Host "$(Get-Date) - Sending Webhook payload to Slack ..." + $ProgressPreference = "SilentlyContinue" + + try { + Invoke-WebRequest -Uri $(${jsonSecrets}.SLACK_WEBHOOK_URL) -Method POST -ContentType "application/json" -Body $body + } catch { + throw "$(Get-Date) - Failed to send Slack Message: $($_)" + } + + Write-Host "$(Get-Date) - Successfully sent Webhook ..." + + Write-Host "$(Get-Date) - PG Check operation complete ...`n" + + Write-Host "$(Get-Date) - Handler Processing Completed ...`n" +} diff --git a/examples/knative/powercli/kn-pcli-pg-check/kn-pcli-pg-slack.png b/examples/knative/powercli/kn-pcli-pg-check/kn-pcli-pg-slack.png new file mode 100644 index 0000000000000000000000000000000000000000..fd06399e7eaeb723722508b9bc407161fa9bfb7e GIT binary patch literal 78734 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 literal 0 HcmV?d00001 diff --git a/examples/knative/powercli/kn-pcli-pg-check/pg_check_secret.json b/examples/knative/powercli/kn-pcli-pg-check/pg_check_secret.json new file mode 100644 index 00000000..a92ce566 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-pg-check/pg_check_secret.json @@ -0,0 +1,10 @@ +{ + "VCENTER_SERVER": "FILL-ME-IN", + "VCENTER_USERNAME" : "FILL-ME-IN", + "VCENTER_PASSWORD" : "FILL-ME-IN", + "VCENTER_CERTIFICATE_ACTION" : "Ignore", + "VM_WATCH_TAGS":["FILL-ME-IN","FILL-ME-IN"], + "PG_WATCH_TAGS":["FILL-ME-IN","FILL-ME-IN"], + "SLACK_WEBHOOK_URL" : "FILL-ME-IN", + "SLACK_MESSAGE_PRETEXT":"Virtual Machine - Portgroup Alert" +} \ No newline at end of file diff --git a/examples/knative/powercli/kn-pcli-pg-check/test/docker-test-env-variable b/examples/knative/powercli/kn-pcli-pg-check/test/docker-test-env-variable new file mode 100644 index 00000000..c55ee64d --- /dev/null +++ b/examples/knative/powercli/kn-pcli-pg-check/test/docker-test-env-variable @@ -0,0 +1 @@ +PG_CHECK_SECRET={"VCENTER_SERVER":"FILL-ME-IN","VCENTER_USERNAME":"FILL-ME-IN","VCENTER_PASSWORD":"FILL-ME-IN","VCENTER_CERTIFICATE_ACTION":"Ignore","VM_WATCH_TAGS":["FILL-ME-IN","FILL-ME-IN"],"PG_WATCH_TAGS":["FILL-ME-IN","FILL-ME-IN"],"SLACK_WEBHOOK_URL":"FILL-ME-IN","SLACK_MESSAGE_PRETEXT":"Virtual Machine - Portgroup Alert"} \ No newline at end of file diff --git a/examples/knative/powercli/kn-pcli-pg-check/test/send-cloudevent-test.ps1 b/examples/knative/powercli/kn-pcli-pg-check/test/send-cloudevent-test.ps1 new file mode 100644 index 00000000..afd37fe7 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-pg-check/test/send-cloudevent-test.ps1 @@ -0,0 +1,26 @@ +# The ce-subject value should match the event router subject in function.yaml +$headers = @{ + "Content-Type" = "application/json"; + "ce-specversion" = "1.0"; + "ce-id" = "id-123"; + "ce-source" = "source-123"; + "ce-type" = "com.vmware.event.router/event"; + "ce-subject" = "VmReconfiguredEvent"; +} + +$payloadPath = "./test-payload.json" +if ( $args.Count -gt 0 ) { + if ( Test-Path $args[0] ) { + $payloadPath = $args[0] + } + else { + Write-Host "$(Get-Date) - ERROR: Invalid path"$args[0]"`n" + exit + } +} +$body = Get-Content -Raw -Path $payloadPath + +Write-Host "Testing Function ..." +Invoke-WebRequest -Uri http://localhost:8080 -Method POST -Headers $headers -Body $body + +Write-host "See docker container console for output" \ No newline at end of file diff --git a/examples/knative/powercli/kn-pcli-pg-check/test/send-cloudevent-test.sh b/examples/knative/powercli/kn-pcli-pg-check/test/send-cloudevent-test.sh new file mode 100644 index 00000000..c660e374 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-pg-check/test/send-cloudevent-test.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# The ce-subject value should match the event router subject in function.yaml +echo "Testing Function ..." +PAYLOAD_PATH="test-payload.json" +if [ $# -gt 0 ]; then + if test -f "$1"; then + PAYLOAD_PATH=$1 + else + echo "$1 not found" + exit 1 + fi +fi +curl -d@$PAYLOAD_PATH \ + -H "Content-Type: application/json" \ + -H 'ce-specversion: 1.0' \ + -H 'ce-id: d70079f9-fddd-4b7f-aa76-1193f28b0611' \ + -H 'ce-source: https://vcenter.local/sdk' \ + -H 'ce-type: com.vmware.event.router/event' \ + -H 'ce-subject: VmReconfiguredEvent' \ + -X POST localhost:8080 + +echo "See docker container console for output" diff --git a/examples/knative/powercli/kn-pcli-pg-check/test/test-payload.json b/examples/knative/powercli/kn-pcli-pg-check/test/test-payload.json new file mode 100644 index 00000000..925c439b --- /dev/null +++ b/examples/knative/powercli/kn-pcli-pg-check/test/test-payload.json @@ -0,0 +1,158 @@ +{ + "Key": 689459, + "ChainId": 689456, + "CreatedTime": "2022-02-24T20:01:17.280999Z", + "UserName": "VSPHERE.LOCAL\\Administrator", + "Datacenter": { + "Name": "HomeLab", + "Datacenter": { + "Type": "Datacenter", + "Value": "datacenter-47" + } + }, + "ComputeResource": { + "Name": "CL1", + "ComputeResource": { + "Type": "ClusterComputeResource", + "Value": "domain-c52" + } + }, + "Host": { + "Name": "esx02.lab.local", + "Host": { + "Type": "HostSystem", + "Value": "host-58" + } + }, + "Vm": { + "Name": "REPLACE-ME", + "Vm": { + "Type": "VirtualMachine", + "Value": "REPLACE-ME" + } + }, + "Ds": null, + "Net": null, + "Dvs": null, + "FullFormattedMessage": "Reconfigured tinycore-2 on esx02.lab.local in HomeLab. \n \nModified: \n \nconfig.hardware.device(4000).backing.port.portgroupKey: \"dvportgroup-1006\" -> \"dvportgroup-8004\"; \n\nconfig.hardware.device(4000).backing.port.portKey: \"4\" -> \"65\"; \n\nconfig.hardware.device(4000).backing.port.connectionCookie: 174899823 -> 193390024; \n\n Added: \n \n Deleted: \n \n", + "ChangeTag": "", + "Template": false, + "ConfigSpec": { + "ChangeVersion": "2022-02-24T20:00:58.726047Z", + "Name": "", + "Version": "", + "CreateDate": "2021-05-25T20:15:49.026445Z", + "Uuid": "", + "InstanceUuid": "", + "NpivNodeWorldWideName": null, + "NpivPortWorldWideName": null, + "NpivWorldWideNameType": "", + "NpivDesiredNodeWwns": 0, + "NpivDesiredPortWwns": 0, + "NpivTemporaryDisabled": null, + "NpivOnNonRdmDisks": null, + "NpivWorldWideNameOp": "", + "LocationId": "", + "GuestId": "", + "AlternateGuestName": "", + "Annotation": "", + "Files": { + "VmPathName": "ds:///vmfs/volumes/60883b4b-6596cf2d-0179-001b21a69dd8/tinycore-2/tinycore-2.vmx", + "SnapshotDirectory": "", + "SuspendDirectory": "", + "LogDirectory": "", + "FtMetadataDirectory": "" + }, + "Tools": null, + "Flags": null, + "ConsolePreferences": null, + "PowerOpInfo": null, + "NumCPUs": 0, + "VcpuConfig": null, + "NumCoresPerSocket": 0, + "MemoryMB": 0, + "MemoryHotAddEnabled": null, + "CpuHotAddEnabled": null, + "CpuHotRemoveEnabled": null, + "VirtualICH7MPresent": null, + "VirtualSMCPresent": null, + "DeviceChange": [ + { + "Operation": "edit", + "FileOperation": "", + "Device": { + "Key": 4000, + "DeviceInfo": { + "Label": "Network adapter 1", + "Summary": "DVSwitch: 50 17 d5 a1 f0 17 6c 1d-d7 f9 b4 a8 21 62 1e 6b" + }, + "Backing": { + "Port": { + "SwitchUuid": "50 17 d5 a1 f0 17 6c 1d-d7 f9 b4 a8 21 62 1e 6b", + "PortgroupKey": "dvportgroup-8004", + "PortKey": "65", + "ConnectionCookie": 193390024 + } + }, + "Connectable": null, + "SlotInfo": { + "PciSlotNumber": 32 + }, + "ControllerKey": 100, + "UnitNumber": 7, + "AddressType": "assigned", + "MacAddress": "00:50:56:97:6f:a8", + "WakeOnLanEnabled": true, + "ResourceAllocation": { + "Reservation": 0, + "Share": { + "Shares": 50, + "Level": "normal" + }, + "Limit": -1 + }, + "ExternalId": "", + "UptCompatibilityEnabled": false + }, + "Profile": null, + "Backing": null + } + ], + "CpuAllocation": null, + "MemoryAllocation": null, + "LatencySensitivity": null, + "CpuAffinity": null, + "MemoryAffinity": null, + "NetworkShaper": null, + "CpuFeatureMask": null, + "ExtraConfig": null, + "SwapPlacement": "", + "BootOptions": null, + "VAppConfig": null, + "FtInfo": null, + "RepConfig": null, + "VAppConfigRemoved": null, + "VAssertsEnabled": null, + "ChangeTrackingEnabled": null, + "Firmware": "", + "MaxMksConnections": 0, + "GuestAutoLockEnabled": null, + "ManagedBy": null, + "MemoryReservationLockedToMax": null, + "NestedHVEnabled": null, + "VPMCEnabled": null, + "ScheduledHardwareUpgradeInfo": null, + "VmProfile": null, + "MessageBusTunnelEnabled": null, + "Crypto": null, + "MigrateEncryption": "", + "SgxInfo": null, + "GuestMonitoringModeInfo": null, + "SevEnabled": null + }, + "ConfigChanges": { + "Modified": "config.hardware.device(4000).backing.port.portgroupKey: \"dvportgroup-1006\" -> \"dvportgroup-8004\"; \n\nconfig.hardware.device(4000).backing.port.portKey: \"4\" -> \"65\"; \n\nconfig.hardware.device(4000).backing.port.connectionCookie: 174899823 -> 193390024; \n\n", + "Added": "", + "Deleted": "" + } + } \ No newline at end of file From 67e0e1da8fe52f38cc2fde56efe20cbd0e4ed383 Mon Sep 17 00:00:00 2001 From: Andrew Tauber Date: Thu, 24 Mar 2022 15:00:29 -0400 Subject: [PATCH 02/54] bug: fix website out of sync Closes: #821 Signed-off-by: Andrew Tauber --- docs/Gemfile | 1 - docs/_config.yml | 3 +- docs/_includes/head.html | 3 -- docs/_layouts/default.html | 1 - docs/assets/css/custom-tabs.css | 48 ----------------------------- docs/kb/function-tutorial-deploy.md | 13 ++------ docs/kb/function-tutorial-intro.md | 17 +++------- docs/kb/install-knative.md | 7 ++--- 8 files changed, 12 insertions(+), 81 deletions(-) delete mode 100644 docs/assets/css/custom-tabs.css diff --git a/docs/Gemfile b/docs/Gemfile index d8b8f2f5..7e99e3a1 100644 --- a/docs/Gemfile +++ b/docs/Gemfile @@ -14,4 +14,3 @@ gem 'jekyll-titles-from-headings', '~> 0.5.3' gem 'jekyll-seo-tag', '~> 2.6', '>= 2.6.1' gem 'redcarpet', '~> 3.5' gem "jekyll-github-metadata", '~> 2.13.0' -gem "jekyll-simple-tab", '~> 0.1.6' diff --git a/docs/_config.yml b/docs/_config.yml index b8f0577f..ca0a5d2d 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -90,8 +90,7 @@ plugins: - jekyll-redirect-from - jekyll-seo-tag - jekyll-github-metadata - - jekyll-simple-tab - + # Include these subdirectories include: - CONTRIBUTING.md diff --git a/docs/_includes/head.html b/docs/_includes/head.html index 6a77fca6..9117d1d0 100644 --- a/docs/_includes/head.html +++ b/docs/_includes/head.html @@ -5,9 +5,6 @@ - - - {{ site.title }} {{page.title}} {% seo %} {% include google-analytics.html %} diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html index 1e889cfe..94addefe 100644 --- a/docs/_layouts/default.html +++ b/docs/_layouts/default.html @@ -13,5 +13,4 @@ - diff --git a/docs/assets/css/custom-tabs.css b/docs/assets/css/custom-tabs.css deleted file mode 100644 index e9d72f2f..00000000 --- a/docs/assets/css/custom-tabs.css +++ /dev/null @@ -1,48 +0,0 @@ -.tab { - display: flex; - flex-wrap: wrap; - margin-left: -20px; - padding: 0; - list-style: none; - position: relative; -} - -.tab > * { - flex: none; - padding-left: 20px; - position: relative; -} - -.tab > * > a { - display: block; - text-align: center; - padding: 9px 20px; - color: #999; - border-bottom: 2px solid transparent; - border-bottom-color: transparent; - font-size: 12px; - text-transform: uppercase; - transition: color .1s ease-in-out; - line-height: 20px; -} - -.tab > .active > a { - color:#222; - border-color: #1e87f0; -} - -.tab li a { - text-decoration: none; - cursor: pointer; -} - -.tab-content{ - padding: 0; -} - -.tab-content li { - display: none; -} -.tab-content li.active { - display: initial; -} diff --git a/docs/kb/function-tutorial-deploy.md b/docs/kb/function-tutorial-deploy.md index f8dc6abb..3b8b6d25 100644 --- a/docs/kb/function-tutorial-deploy.md +++ b/docs/kb/function-tutorial-deploy.md @@ -85,25 +85,18 @@ docker.io//: ## Introduction to the Kubernetes vmware-functions namespace With the Docker image pushed to the registry, we are now ready to deploy the function to the VEBA appliance. Remember, you will need to copy the Kubernetes config file to your workstation and export the `KUBECONFIG` environment variable so that the `kubectl` command can access the Kubernetes cluster on the VEBA appliance. We will use `kubectl` to deploy the function. Below is a reminder of the steps we used to copy and use the VEBA appliance config file. Getting the Kubernetes config file was covered in the intro [Function Tutorial - Function Intro](function-tutorial-intro). If you have opened a new terminal window, you may need to set the `KUBECONFIG` environment variable once more for the current session. -{% tabs export KUBECONFIG %} - -{% tab export KUBECONFIG#macOS%} +**Hint:** KUBECONFIG export for macOS: ``` export KUBECONFIG=$HOME/veba/config ``` -{% endtab %} - -{% tab export KUBECONFIG#Windows %} +**Hint:** KUBECONFIG export for Windows: ``` Env:KUBECONFIG="$HOME\veba\config" ``` -{% endtab %} - -{% endtabs %} Kubernetes namespaces are resource boundaries within the cluster. Function related resources in the VEBA appliance are segregated into the "vmware-functions" namespace. Use `kubectl` to list out the resources in the vmware-functions namespace: @@ -306,7 +299,7 @@ We have used `kubectl get` and `kubectl apply` in the above examples. The follo ### describe ### -The `kubectl describe` command is very useful and shows details about Kubernetes objects like pods. Remember, we need to explicitely set the namespace if not "default". +The `kubectl describe` command is very useful and shows details about Kubernetes objects like pods. Remember, we need to explicitly set the namespace if not "default". ``` kubectl -n vmware-functions get pods diff --git a/docs/kb/function-tutorial-intro.md b/docs/kb/function-tutorial-intro.md index df416f44..5bdc589f 100644 --- a/docs/kb/function-tutorial-intro.md +++ b/docs/kb/function-tutorial-intro.md @@ -25,6 +25,8 @@ This tutorial will go over: - [The big picture](#the-big-picture) - [The anatomy of a VEBA function](#the-anatomy-of-a-veba-function) - [Installing required tools on your workstation](#installing-required-tools-on-your-workstation) +- [macOS Instructions](#macos-instructions) +- [Windows Instructions](#windows-instructions) ## The big picture VEBA functions provide the "custom" logic to the VEBA appliance to fulfil your business requirements. Functions are packaged as Docker images. The functions can be written in PowerShell, Python, Go, or just about any language, and are packaged and distributed as Docker images. This is because the VEBA appliance runs Kubernetes (on top of the Photon OS) and functions are deployed as containers on the Kubernetes system. Kubernetes provides an abstraction layer to allow containers to run seamlessly on disparate hardware/OSes. @@ -62,12 +64,10 @@ You will need to install and configure/use three tools: Note: even if the function's code is not modified, Docker will still be necessary to run local tests of the function from your workstation. -Install instructions for the required tools follow: +Install instructions for the required tools on macOS are below. Install instructions for Windows follow after the macOS instructions. -{% tabs install %} - -{% tab install#macOS%} +## macOS Instructions ### Install `git` and clone the repo to your workstation @@ -195,9 +195,7 @@ vmware-system Active 14d ``` -{% endtab %} - -{% tab install#Windows %} +## Windows Instructions ### Install `git` and clone the repo to your workstation @@ -326,8 +324,3 @@ vmware-functions Active 14d vmware-system Active 14d ``` - - -{% endtab %} - -{% endtabs %} diff --git a/docs/kb/install-knative.md b/docs/kb/install-knative.md index 0dd094ac..596084a8 100644 --- a/docs/kb/install-knative.md +++ b/docs/kb/install-knative.md @@ -6,7 +6,7 @@ description: Deploying VMware Event Broker Appliance with Knative permalink: /kb/install-knative cta: title: Deploy a Function - description: At this point, you have successfully deployed the VMware Event Broker Appliance and you are ready to start deploying your functions! + description: At this point, you have successfully deployed the VMware Event Broker Appliance and you are ready to start deploying your functions! actions: - text: Check the [Knative Echo Function](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/examples/knative/powershell/kn-ps-echo){:target="_blank"} to quickly get started --- @@ -18,9 +18,9 @@ Customers looking to seamlessly extend their vCenter by either deploying our pre ### Requirements -* 4 vCPU and 8GB of memory for VMware Event Broker Appliance +* 6 vCPU and 8GB of memory for VMware Event Broker Appliance * vCenter Server 6.x or greater - * The VEBA UI requires vCenter Server 7.0 or greater + * **The VEBA UI requires vCenter Server 7.0 or greater** * vCenter TCP/443 accessible from Appliance IP address * Account to login to vCenter Server (readOnly is sufficient) @@ -121,4 +121,3 @@ Webhook: https://[hostname]/stats/webhook ### Step 4 You can verify that everything was deployed correctly by opening a web browser and accessing one of the endpoints along with the associated admin password you had specified as part of the OVA deployment. - From fb85eb1c1190c65d817bbbad3290f6806f21b03b Mon Sep 17 00:00:00 2001 From: Michael Gasch Date: Wed, 30 Mar 2022 11:12:47 +0200 Subject: [PATCH 03/54] chore: Add CodeQL scanning Closes: #828 Signed-off-by: Michael Gasch --- .github/workflows/codeql-analysis.yml | 71 +++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 .github/workflows/codeql-analysis.yml diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 00000000..fc6f17d8 --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,71 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [main] + pull_request: + # The branches below must be a subset of the branches above + branches: [main] + schedule: + - cron: "34 21 * * 5" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: ["go", "python"] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹ️ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 From 36d086dc675dffca69438b4ce45e29a2c3433921 Mon Sep 17 00:00:00 2001 From: Michael Gasch Date: Wed, 30 Mar 2022 11:09:39 +0200 Subject: [PATCH 04/54] fix: Bump gogo proto version Closes: #829 Signed-off-by: Michael Gasch --- vmware-event-router/go.mod | 4 ++-- vmware-event-router/go.sum | 13 ++++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/vmware-event-router/go.mod b/vmware-event-router/go.mod index fd6a8fe3..bf233f26 100644 --- a/vmware-event-router/go.mod +++ b/vmware-event-router/go.mod @@ -20,7 +20,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/vmware/govmomi v0.24.1-0.20210210035757-ed60338583b0 go.uber.org/zap v1.16.0 - golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 + golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 gotest.tools v2.2.0+incompatible k8s.io/api v0.18.8 k8s.io/apimachinery v0.18.8 @@ -42,7 +42,7 @@ require ( github.com/fatih/color v1.10.0 // indirect github.com/fsnotify/fsnotify v1.4.9 // indirect github.com/go-logr/logr v0.1.0 // indirect - github.com/gogo/protobuf v1.3.1 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/protobuf v1.4.3 // indirect github.com/google/go-cmp v0.5.2 // indirect diff --git a/vmware-event-router/go.sum b/vmware-event-router/go.sum index cf3349e8..d6cdf4d4 100644 --- a/vmware-event-router/go.sum +++ b/vmware-event-router/go.sum @@ -256,8 +256,9 @@ github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFG github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= -github.com/gogo/protobuf v1.3.1 h1:DqDEcV5aeaTmdFBePNpYsp3FlcVH/2ISVVM9Qf8PSls= github.com/gogo/protobuf v1.3.1/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= +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-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -413,6 +414,7 @@ github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dv github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= +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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= @@ -752,6 +754,7 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R 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-20200904194848-62affa334b73/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-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -768,8 +771,9 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ 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 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 h1:SQFwaSi55rU7vdNs9Yr0Z324VNlrF+0wMqRXT4St8ck= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -822,6 +826,7 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c= @@ -892,12 +897,14 @@ golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roY 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-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3 h1:DywqrEscRX7O2phNjkT0L6lhHKGBoMLCNX+XcAe7t6s= golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a h1:CB3a9Nez8M13wwlr/E2YtwoU+qYHKfC+JrDa45RXXoQ= +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= From 26e084ff5a1e5b2c1cc72d0a43bec572dfa7c0a5 Mon Sep 17 00:00:00 2001 From: Michael Gasch Date: Wed, 30 Mar 2022 16:39:47 +0200 Subject: [PATCH 05/54] fix: NSX tag sync DEBUG statements Closes: #833 Signed-off-by: Michael Gasch --- .../knative/powercli/kn-pcli-nsx-tag-sync/function.yaml | 2 +- examples/knative/powercli/kn-pcli-nsx-tag-sync/handler.ps1 | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/examples/knative/powercli/kn-pcli-nsx-tag-sync/function.yaml b/examples/knative/powercli/kn-pcli-nsx-tag-sync/function.yaml index 174b8c94..fa9a40e9 100644 --- a/examples/knative/powercli/kn-pcli-nsx-tag-sync/function.yaml +++ b/examples/knative/powercli/kn-pcli-nsx-tag-sync/function.yaml @@ -12,7 +12,7 @@ spec: autoscaling.knative.dev/minScale: "1" spec: containers: - - image: us.gcr.io/daisy-284300/veba/kn-pcli-nsx-tag-sync:1.0 + - image: us.gcr.io/daisy-284300/veba/kn-pcli-nsx-tag-sync:1.1 envFrom: - secretRef: name: tag-secret diff --git a/examples/knative/powercli/kn-pcli-nsx-tag-sync/handler.ps1 b/examples/knative/powercli/kn-pcli-nsx-tag-sync/handler.ps1 index e0c1d513..fb0373e4 100644 --- a/examples/knative/powercli/kn-pcli-nsx-tag-sync/handler.ps1 +++ b/examples/knative/powercli/kn-pcli-nsx-tag-sync/handler.ps1 @@ -98,8 +98,10 @@ Function Process-Handler { } $arguments = $cloudEventData.Arguments | Out-String - Write-Host "$(Get-Date) - DEBUG: CloudEventDataArguments:`n $arguments" - Write-Host "$(Get-Date) - DEBUG: VM name: $vmname" + if (${env:FUNCTION_DEBUG} -eq "true") { + Write-Host "$(Get-Date) - DEBUG: CloudEventDataArguments:`n $arguments" + Write-Host "$(Get-Date) - DEBUG: VM name: $vmname" + } # Get VM object from vCenter try { From 39bfd6a924badfe8b7de9df33ae5a68f64d8f5a4 Mon Sep 17 00:00:00 2001 From: William Lam Date: Wed, 30 Mar 2022 06:25:52 -0700 Subject: [PATCH 06/54] docs: Instructions for using private container registry Closes: #827 Signed-off-by: William Lam --- docs/_data/default.yml | 6 +- docs/kb/private-registry.md | 136 ++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 docs/kb/private-registry.md diff --git a/docs/_data/default.yml b/docs/_data/default.yml index 7ba0d12a..40e818cc 100644 --- a/docs/_data/default.yml +++ b/docs/_data/default.yml @@ -67,9 +67,9 @@ toc: - page: Replace TLS Certificates on VEBA id: advanced-certificates url: /kb/advanced-certificates - - page: Using Harbor with VEBA - id: site-resources - external_url: https://rguske.github.io/post/using-harbor-with-the-vcenter-event-broker-appliance/ + - page: Using Private Container Registry with VEBA + id: private-registry + external_url: /kb/private-registry - page: Monitoring VEBA with vROps id: site-resources external_url: https://rguske.github.io/post/monitoring-the-vmware-event-broker-appliance-with-vrealize-operations-manager/ diff --git a/docs/kb/private-registry.md b/docs/kb/private-registry.md new file mode 100644 index 00000000..36cd4335 --- /dev/null +++ b/docs/kb/private-registry.md @@ -0,0 +1,136 @@ +--- +layout: docs +toc_id: private-container-registry +title: VMware Event Broker Appliance - Private Registry +description: Private Registry +permalink: /kb/private-registry +cta: + description: Using private container registry with the VMware Event Broker Appliance. +--- + +## Using private container registry with VEBA + +By default, the VMware Event Broker Appliance can integrate with any Open Container Initiative (OCI) compliant container registry for hosting and deploying container images that uses a TLS certificate from a trusted authority such as [Docker Hub](https://hub.docker.com/) or [Amazon Elastic Container Registry (ECR)](https://aws.amazon.com/ecr/) as an example. + +For organizations that require the use of a private container registry and uses a self-signed TLS certificate, an additional post-deployment configuration is required within the VMware Event Broker Appliance. Please follow the steps outlined below. + +### Assumptions + +* VMware Event Broker Appliance v0.7.2 or later +* Root CA Certificates from a trusted authority has been pre-downloaded onto your local desktop +* Un-deploy any functions that has been attempted using private registry prior to instructions below + +> **Note:** For those using the Harbor registry, the root CA certificate is located in : **/etc/docker/certs.d/[FQDN]/ca.crt** + +### Steps + +In this example, the root CA certificate key file is named `ca.crt` and is located in `/root` + +1. Copy the root CA certificate from your private registry to VMware Event Broker Appliance Appliance. If SSH has not been enabled, go ahead and start it up by logging into the VM Console and running the following command: + +```console +systemctl start sshd +``` + +1. SSH to the VMware Event Broker Appliance and make a backup of the original containerd configuration file + +```console +cp /etc/containerd/config.toml /etc/containerd/config.toml.bak +``` + +1. Edit the '/etc/containerd/config.toml' file using VI and locate the following section `[plugins."io.containerd.grpc.v1.cri".registry.mirrors]` within the configuration file. + +Append the following two lines below this section and replace the **REPLACE_ME_FQDN** value with FQDN of the private registry and **REPLACE_ME_PATH_TO_ROOT_CA_CERT** value the full path to the root CA certificate located on the VMware Event Broker Appliance + +```yaml +[plugins."io.containerd.grpc.v1.cri".registry.mirrors] + [plugins."io.containerd.grpc.v1.cri".registry.configs."REPLACE_ME_FQDN".tls] + ca_file = "REPLACE_ME_PATH_TO_ROOT_CA_CERT" +``` + +1. Restart the containerd service for the change to go into effect/. + +```console +systemctl restart containderd +``` + +1. Verify containerd is successfully running before proceeding to the next step + +```console +systemctl status containerd + +● containerd.service - containerd container runtime + Loaded: loaded (/usr/lib/systemd/system/containerd.service; enabled; vendor preset: disabled) + Active: active (running) since Mon 2022-03-28 19:35:06 UTC; 1 day 3h ago + Docs: https://containerd.io + Process: 30072 ExecStartPre=/sbin/modprobe overlay (code=exited, status=0/SUCCESS) + Main PID: 30073 (containerd) + Tasks: 545 + Memory: 1.2G + CGroup: /system.slice/containerd.service +``` + +1. Create a kubernetes secret in the `knative-serving` namespace that points to full path of the root CA certificate of private registry which should reside within the VMware Event Broker Appliance + +```console +kubectl -n knative-serving create secret generic customca --from-file=ca.crt=/root/ca.crt +``` + +1. Retrieve the current Knative Serving controller deployment and save it to a file named `knative-serving-controller.yaml` + +```console +kubectl -n knative-serving get deploy/controller -o yaml > knative-serving-controller.yaml +``` + +1. Create the following YTT overlay which will be used to patch the Knative Serving controller to reference the root CA certificate from the private registry + +```console +cat > overlay.yaml < new-knative-serving-controller.yaml +``` + +1. Apply the new Knative Serving controller configuration + +```console +kubectl apply -f new-knative-serving-controller.yaml +``` + +It can take a couple of minutes for the previous Knative Serving controller to terminate and spawn the new configuration. You can monitor the progress using the following commmand and ensure the `READY` state shows 1/1 + +```console +kubectl -n knative-serving get deployment/controller -w + +NAME READY UP-TO-DATE AVAILABLE AGE +controller 1/1 1 1 29h +``` + +> **Note:** If for some reason the deployment is not re-deploying, you can run `kubectl -n knative-serving delete deployment/controller` and then perform the `apply` operation with the new Knative Serving YAML. \ No newline at end of file From d05d1e26946bb41ac5adef5325940e6c632f50a0 Mon Sep 17 00:00:00 2001 From: William Lam Date: Wed, 30 Mar 2022 09:04:11 -0700 Subject: [PATCH 07/54] docs: Open private container registry link in same window Closes: #837 Signed-off-by: William Lam --- docs/_data/default.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/_data/default.yml b/docs/_data/default.yml index 40e818cc..66c5c98a 100644 --- a/docs/_data/default.yml +++ b/docs/_data/default.yml @@ -69,7 +69,7 @@ toc: url: /kb/advanced-certificates - page: Using Private Container Registry with VEBA id: private-registry - external_url: /kb/private-registry + url: /kb/private-registry - page: Monitoring VEBA with vROps id: site-resources external_url: https://rguske.github.io/post/monitoring-the-vmware-event-broker-appliance-with-vrealize-operations-manager/ From 074e45d9ae9c1a965be0730f8183f99f22b39750 Mon Sep 17 00:00:00 2001 From: Patrick Kremer Date: Mon, 4 Apr 2022 11:47:25 -0500 Subject: [PATCH 08/54] docs: Update bad links in functions pages Closes #820 Signed-off-by: Patrick Kremer --- docs/kb/intro-about.md | 2 +- docs/kb/intro-functions.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/kb/intro-about.md b/docs/kb/intro-about.md index 247b0b57..79c4da64 100644 --- a/docs/kb/intro-about.md +++ b/docs/kb/intro-about.md @@ -52,4 +52,4 @@ VMware Event Broker Appliance enables customers to quickly get started with pre- ### Community Use Cases -Please see [this list here](https://github.com/vmware-samples/vcenter-event-broker-appliance/USECASES.md) for a collection of use cases from the VMware Event Broker Appliance community. \ No newline at end of file +Please see [this list here](https://github.com/vmware-samples/vcenter-event-broker-appliance/blob/master/USECASES.md) for a collection of use cases from the VMware Event Broker Appliance community. \ No newline at end of file diff --git a/docs/kb/intro-functions.md b/docs/kb/intro-functions.md index a807daf6..211ed829 100644 --- a/docs/kb/intro-functions.md +++ b/docs/kb/intro-functions.md @@ -28,7 +28,7 @@ The VMware Event Broker Appliance can be deployed using the Knative event proces ## Knative -Users who directly want to jump into VMware vSphere-related function code might want to check out the examples we provide [here](/examples/knative). +Users who directly want to jump into VMware vSphere-related function code might want to check out the examples we provide [here](/examples). ### Knative Naming and Version Control From 8d6eb01481537e3ef6c27608bebde9c2f226d34f Mon Sep 17 00:00:00 2001 From: Patrick Kremer Date: Mon, 4 Apr 2022 11:29:26 -0500 Subject: [PATCH 09/54] docs: Direct new function writers to PowerCLI template Closes #817 Signed-off-by: Patrick Kremer --- docs/kb/contribute-functions.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/kb/contribute-functions.md b/docs/kb/contribute-functions.md index 14e89047..15301f02 100644 --- a/docs/kb/contribute-functions.md +++ b/docs/kb/contribute-functions.md @@ -5,7 +5,7 @@ title: VMware Event Broker Appliance - Building Functions description: Building Functions permalink: /kb/contribute-functions cta: - title: Have a question? + title: Have a question? description: Please check our [Frequently Asked Questions](/faq) first. --- @@ -16,7 +16,7 @@ The VMware Event Broker Appliance (VEBA) uses Knative as a Function-as-a-Service [here](functions). You can also get started quickly with these quickstart [templates](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/examples/knative){:target="_blank"}. -## Instructions +## Intro This guide describes how to create a function with PowerCLI (PowerShell) to apply a vSphere tag when a Virtual Machine is powered on. @@ -26,6 +26,11 @@ apply a vSphere tag when a Virtual Machine is powered on. > correctly. Access to the Kubernetes environment in VEBA via `kubectl` is also > assumed to be working. +A template for Knative PowerCLI functions is available in [kn-pcli-template](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/examples/knative/powercli/kn-pcli-template). If you do not want to build all of the required files from scratch, you can copy all of the files from this template directory. Follow the instructions in the [README](https://github.com/vmware-samples/vcenter-event-broker-appliance/blob/master/examples/knative/powercli/kn-pcli-template/README.md) instead of the instructions on this page. + +To create a function from scratch, continue with the instructions on this page. + +## Instructions First, create a directory for your function code, credentials (implemented via Kubernetes [secrets](https://kubernetes.io/docs/concepts/configuration/secret/)) @@ -35,8 +40,6 @@ and test files. mkdir tag-fn && cd tag-fn ``` -A template for Knative PowerCLI functions is available in [kn-pcli-template](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/development/examples/knative/powercli/kn-pcli-template). If you do not want to build all of the required files from scratch, you can copy all of the files from this template directory, then follow the instructions in the README. - Before we start looking at the actual function business logic (inside `handler.ps1`), let's discuss how `secrets`, such as credentials, are injected and used inside a function. From 012707bd05c9640cb71d1b103097a95f22b6e7f0 Mon Sep 17 00:00:00 2001 From: Patrick Kremer Date: Tue, 5 Apr 2022 17:56:31 -0500 Subject: [PATCH 10/54] fix: Handle unicode characters in tag names Closes #834 Signed-off-by: Patrick Kremer --- .../powercli/kn-pcli-nsx-tag-sync/README.md | 6 ++--- .../kn-pcli-nsx-tag-sync/function.yaml | 2 +- .../powercli/kn-pcli-nsx-tag-sync/handler.ps1 | 24 +++++++++++++------ 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/examples/knative/powercli/kn-pcli-nsx-tag-sync/README.md b/examples/knative/powercli/kn-pcli-nsx-tag-sync/README.md index 575cf2c7..91df477d 100644 --- a/examples/knative/powercli/kn-pcli-nsx-tag-sync/README.md +++ b/examples/knative/powercli/kn-pcli-nsx-tag-sync/README.md @@ -11,7 +11,7 @@ Create the container image locally to test your function logic. ``` # change the IMAGE name accordingly -export IMAGE=us.gcr.io/daisy-284300/veba/kn-pcli-nsx-tag-sync:1.0 +export IMAGE=us.gcr.io/daisy-284300/veba/kn-pcli-nsx-tag-sync:1.2 docker build -t ${IMAGE} . ``` @@ -42,7 +42,7 @@ Update the following variable names within the `docker-test-env-variable` file Start the container image by running the following command: ```console -export IMAGE=us.gcr.io/daisy-284300/veba/kn-pcli-nsx-tag-sync:1.0 +export IMAGE=us.gcr.io/daisy-284300/veba/kn-pcli-nsx-tag-sync:1.2 docker run -e FUNCTION_DEBUG=true -e PORT=8080 --env-file docker-test-env-variable -it --rm -p 8080:8080 ${IMAGE} ``` @@ -160,7 +160,7 @@ done developing and testing your function logic. > or `Dockerfile`. ```console -export IMAGE=us.gcr.io/daisy-284300/veba/kn-pcli-nsx-tag-sync:1.0 +export IMAGE=us.gcr.io/daisy-284300/veba/kn-pcli-nsx-tag-sync:1.2 docker push ${IMAGE} ``` diff --git a/examples/knative/powercli/kn-pcli-nsx-tag-sync/function.yaml b/examples/knative/powercli/kn-pcli-nsx-tag-sync/function.yaml index fa9a40e9..bfe0e466 100644 --- a/examples/knative/powercli/kn-pcli-nsx-tag-sync/function.yaml +++ b/examples/knative/powercli/kn-pcli-nsx-tag-sync/function.yaml @@ -12,7 +12,7 @@ spec: autoscaling.knative.dev/minScale: "1" spec: containers: - - image: us.gcr.io/daisy-284300/veba/kn-pcli-nsx-tag-sync:1.1 + - image: us.gcr.io/daisy-284300/veba/kn-pcli-nsx-tag-sync:1.2 envFrom: - secretRef: name: tag-secret diff --git a/examples/knative/powercli/kn-pcli-nsx-tag-sync/handler.ps1 b/examples/knative/powercli/kn-pcli-nsx-tag-sync/handler.ps1 index fb0373e4..f5141d3b 100644 --- a/examples/knative/powercli/kn-pcli-nsx-tag-sync/handler.ps1 +++ b/examples/knative/powercli/kn-pcli-nsx-tag-sync/handler.ps1 @@ -108,7 +108,12 @@ Function Process-Handler { $vm = Get-VM -name $vmname | Select-Object Name, PersistentId } catch { - Write-Host "$(Get-Date) - ERROR: unable to retrieve VM object" + Write-Host "$(Get-Date) - WARNING: unable to retrieve VM object. Error category: $($error.exception[0].errorcategory)" + # This error will occur if an object other than a VM was tagged. We only care about VM tags, so gracefully return + if ($error.exception[0].errorcategory -eq "ObjectNotFound") { + Write-Host "$(Get-Date) - WARNING: $($vmname) was not found via Get-VM" + return + } throw $_ } @@ -181,18 +186,23 @@ Function Process-Handler { # POST to NSX try { - $response = "" + if (${env:FUNCTION_DEBUG} -eq "true") { + Write-Host "$(Get-Date) - DEBUG: NSX Body=$($nsxBody)" + } + + $encodednsxbody = [System.Text.Encoding]::UTF8.GetBytes($nsxbody) + if ($NSX_SKIP_CERT_CHECK -eq "true") { - $response = Invoke-Webrequest -Uri $nsxUrl -Method POST -Headers $headers -SkipHeaderValidation -Body $nsxbody -SkipCertificateCheck + $response = Invoke-Webrequest -Uri $nsxUrl -Method POST -Headers $headers -SkipHeaderValidation -Body $encodednsxbody -ContentType "application/json; charset=utf-8" -SkipCertificateCheck } else { - $response = Invoke-Webrequest -Uri $nsxUrl -Method POST -Headers $headers -SkipHeaderValidation -Body $nsxbody + $response = Invoke-Webrequest -Uri $nsxUrl -Method POST -Headers $headers -SkipHeaderValidation -Body $encodednsxbody -ContentType "application/json; charset=utf-8" } - + if (${env:FUNCTION_DEBUG} -eq "true") { - Write-Host "$(Get-Date) - DEBUG: Invoke-WebRequest response=$($response)" + Write-Host "$(Get-Date) - DEBUG: Invoke-WebRequest response code=$($response.StatusCode)" } - + Write-Host "$(Get-Date) - vSphere Tag to NSX Operation complete" Write-Host "$(Get-Date) - Handler Processing complete" } From 78c6a3f7282da2a44b9686235a65ad1087bacac3 Mon Sep 17 00:00:00 2001 From: Michael Gasch Date: Sun, 3 Apr 2022 22:11:18 +0200 Subject: [PATCH 11/54] feat: Add kn-go-nsx-tag-sync Function Closes: #836 Signed-off-by: Michael Gasch --- docs/site/examples.md | 3 + .../knative/go/kn-go-nsx-tag-sync/.ko.yaml | 14 + .../knative/go/kn-go-nsx-tag-sync/README.md | 277 +++++++ .../go/kn-go-nsx-tag-sync/function.yaml | 88 +++ examples/knative/go/kn-go-nsx-tag-sync/go.mod | 39 + examples/knative/go/kn-go-nsx-tag-sync/go.sum | 733 ++++++++++++++++++ .../knative/go/kn-go-nsx-tag-sync/main.go | 75 ++ .../knative/go/kn-go-nsx-tag-sync/tags/nsx.go | 112 +++ .../go/kn-go-nsx-tag-sync/tags/tags.go | 281 +++++++ .../go/kn-go-nsx-tag-sync/tags/tags_test.go | 278 +++++++ .../go/kn-go-nsx-tag-sync/tags/vsphere.go | 32 + .../kn-go-nsx-tag-sync/tags/vsphere_test.go | 97 +++ 12 files changed, 2029 insertions(+) create mode 100644 examples/knative/go/kn-go-nsx-tag-sync/.ko.yaml create mode 100644 examples/knative/go/kn-go-nsx-tag-sync/README.md create mode 100644 examples/knative/go/kn-go-nsx-tag-sync/function.yaml create mode 100644 examples/knative/go/kn-go-nsx-tag-sync/go.mod create mode 100644 examples/knative/go/kn-go-nsx-tag-sync/go.sum create mode 100644 examples/knative/go/kn-go-nsx-tag-sync/main.go create mode 100644 examples/knative/go/kn-go-nsx-tag-sync/tags/nsx.go create mode 100644 examples/knative/go/kn-go-nsx-tag-sync/tags/tags.go create mode 100644 examples/knative/go/kn-go-nsx-tag-sync/tags/tags_test.go create mode 100644 examples/knative/go/kn-go-nsx-tag-sync/tags/vsphere.go create mode 100644 examples/knative/go/kn-go-nsx-tag-sync/tags/vsphere_test.go diff --git a/docs/site/examples.md b/docs/site/examples.md index 06848694..530689ba 100644 --- a/docs/site/examples.md +++ b/docs/site/examples.md @@ -150,6 +150,9 @@ examples: links: - language: powercli url: "/tree/master/examples/knative/powercli/kn-pcli-nsx-tag-sync" + - language: go + url: "/tree/master/examples/knative/go/kn-go-nsx-tag-sync" + - title: vSphere Tagging usecases: diff --git a/examples/knative/go/kn-go-nsx-tag-sync/.ko.yaml b/examples/knative/go/kn-go-nsx-tag-sync/.ko.yaml new file mode 100644 index 00000000..d6710a53 --- /dev/null +++ b/examples/knative/go/kn-go-nsx-tag-sync/.ko.yaml @@ -0,0 +1,14 @@ +builds: + - id: function + # dir: . + # main: . + env: + - GOPRIVATE=*.vmware.com + flags: + - -tags + - netgo + ldflags: + - -s -w + - -extldflags "-static" + - -X main.buildCommit={{.Env.KO_COMMIT}} + - -X main.buildTag={{.Env.KO_TAG}} diff --git a/examples/knative/go/kn-go-nsx-tag-sync/README.md b/examples/knative/go/kn-go-nsx-tag-sync/README.md new file mode 100644 index 00000000..71f1d573 --- /dev/null +++ b/examples/knative/go/kn-go-nsx-tag-sync/README.md @@ -0,0 +1,277 @@ +# kn-go-nsx-tag-sync + +Example Knative Go function for synchronizing vSphere virtual machine tags to +NSX-T based on vSphere tagging events. + +⚠️ This guide assumes that you have stood up a working Knative environment using +the vCenter Event Broker Appliance (VEBA). + +# How the Synchronization works + +When a vSphere tag is `attached` or `detached` to/from a virtual machine, a +corresponding vSphere event is generated. This is also true when batched tagging +operations are performed, i.e. every tag generates one event. + +The `kn-go-nsx-tag-sync` function reacts to these events and performs the +following steps: + +1) Validate that the received event is a valid `CloudEvent` +1) Validate that the received `CloudEvent` payload (`data`) contains a valid + tagging event +1) Retrieve the virtual machine `ManagedObjectReference` for the `Value` in the `Arguments[].Key == "Object"` field in the payload +1) If the `Object` does not resolve to a virtual machine, e.g. when a tag is attached to a folder or cluster, a warning is logged and the event is discarded +1) Retrieve the instance ID of the virtual machine +1) Retrieve all attached `category:tag` associations for the virtual machine +1) Update the tags in NSX-T for the virtual machine via +`api/v1/fabric/virtual-machines?action=update_tags` +[API](https://vdc-download.vmware.com/vmwb-repository/dcr-public/ce4128ae-8334-4f91-871b-ecce254cf69e/488f1280-204c-441d-8520-8279ac33d54b/api_includes/method_TagVirtualMachine.html) + +In case of temporary errors, e.g. HTTP timeouts or any HTTP 5xx code from NSX-T, +the VEBA `default` broker retries to deliver the event to the function. The +function will also log any errors during execution. + +## Performance and Scalability + +The function is configured to process incoming tagging events serially in a FIFO +(first-in, first-out) order (one event in-flight, maximum one function instance +running). + +This guarantees that concurrent or batched tagging operations on the same object +are **not interleaved, producing determinstic synchronization** results (safety +guarantee), eventually converging to the desired state. + +However, in larger environments with lots of objects and tags, the function can +fall behind processing from the VEBA `default` broker, causing delays and stale +tag states (views) in NSX-T. + +⚠️ FIFO execution can impact your network security, e.g. when a virtual machine is +supposed to be removed from a security group or firewall rule. + +To increase throughput and reduce latency (staleness) several tuning knobs exist +to **parallize** the processing of tagging events (see the [advanced +settings](#advanced-settings) section). In this case, when a function instance +(`pod`) receives multiple events **for the same object** (virtual machine), +events are deduplicated and only one operation to synchronize the tag(s) is +performed. + +⚠️ With parallel processing, **FIFO order is not guaranteed**. Depending on the +environment (size, tagging activities, etc.), there is a chance of interleaving +tagging operations **for the same object** which can lead to inconsistent +synchronisation results. The function will log cases were deduplication was +performed so an administrator can manually inspect the outcome. + +⚠️ When function autoscaling is also enabled (`maxScale > 1`), concurrent +operations without FIFO order can only be detected by inspecting the logs of all +active instances of the `kn-go-nsx-tag-sync` function. + +# Step 1 - Build + +⚠️ This step is only required if you made code changes to any of the \*.go +files. To directly deploy the function jump to [Step 3](#step-3---deploy). + +Requirement: If you make changes to the Go code, the +[ko](https://github.com/google/ko) tool is required to create the artifacts. + +Set the destination to push the function container image with an environment +variable. + +```bash +export KO_DOCKER_REPO=docker.io/my-user +export KO_COMMIT=$(git rev-parse --short=8 HEAD) +export KO_TAG=1.0 +``` + +The following command will build and push the image to the specified +`KO_DOCKER_REPO` repository. + +```bash +# for docker.io +ko publish --bare -t $KO_TAG . + +# for GCR +ko publish -B -t $KO_TAG . +``` + +⚠️ Using the above example, the resulting image would be +`docker.io/myuser/kn-go-nsx-tag-sync:1.0`. + + +# Step 2 - Test + +Run unit tests using the following command: + +```bash +go test -v -race -count 1 ./... +``` + +# Step 3 - Deploy + +⚠️ The following steps assume a working Knative environment using the `default` + Rabbit `broker`. The Knative `service` and `triggers` will be installed in the + `vmware-functions` Kubernetes namespace, assuming that the `broker` is also + available there. + +## Create vSphere and NSX Credentials Secrets + +Create a secret holding the username (role) and password needed to access +vCenter Server. The role must have at least **read-only** access to (the desired +subset of) virtual machines, e.g. cluster/datacenter, and tags in the inventory. + +```bash +kubectl create secret generic vsphere-credentials \ +--type=kubernetes.io/basic-auth \ +--from-literal=username='ro-user@vsphere.local' \ +--from-literal=password='ReplaceMe' \ +--namespace vmware-functions + +# update label for secret to show up in VEBA UI +kubectl -n vmware-functions label secret vsphere-credentials app=veba-ui +``` + +Create a secret holding the username (role) and password needed to access NSX +Manager. The role must have **write** permissions to manage virtual machine tags +(`Inventory > VM > Create & Assign Tags`). + +```bash +kubectl create secret generic nsx-credentials \ +--type=kubernetes.io/basic-auth \ +--from-literal=username='tag-admin@nsx.local' \ +--from-literal=password='ReplaceMe' \ +--namespace vmware-functions + +# update label for secret to show up in VEBA UI +kubectl -n vmware-functions label secret nsx-credentials app=veba-ui +``` + +## Update Environment Settings + +Update environment specific settings under `env:` in the `function.yaml` file. + +Please see the table below for a description of the available (and **required**) +settings. + + +| Configuration | Description | Example Values | Required | +|-----------------------|---------------------------------------------------------------------------------------------------|-----------------------------------|----------| +| `VCENTER_URL` | URL of the vCenter Server | `"https://vcenter-01.corp.local"` | **Yes** | +| `VCENTER_INSECURE` | When set to `false` require strict TLS (certificate) validation when connecting to vCenter Server | `"true"` | No | +| `VCENTER_SECRET_PATH` | The path where the vSphere credentials secret will be mounted | `"/var/bindings/vsphere"` | No | +| `NSX_URL` | URL of the NSX Manager Server | `"https://nsx-01.corp.local"` | **Yes** | +| `NSX_INSECURE` | When set to `false` require strict TLS (certificate) validation when connecting to NSX Manager | `"true"` | No | +| `NSX_SECRET_PATH` | The path where the NSX credentials secret will be mounted | `"/var/bindings/nsx"` | No | +| `DEBUG` | Enable debug logging | `"true"` | No | + +## Deploy the Function + +⚠️ If you made changes to the Go code/container image in [Step +1](#step-1---build) edit the `function.yaml` file with the custom name of the +container image used to build and push. + +Deploy the function to the VMware Event Broker Appliance (VEBA): + +```bash +kubectl apply -f function.yaml -n vmware-functions +``` + +For testing purposes, the [Knative manifest](function.yaml) contains the +following annotations, which will ensure the Knative Service Pod will always run +**exactly** one instance for debugging purposes. Functions deployed through +through the VMware Event Broker Appliance UI defaults to scale to 0, which means +the pods will only run when it is triggered by an vCenter Event. + +```yaml +annotations: + autoscaling.knative.dev/maxScale: "1" + autoscaling.knative.dev/minScale: "1" +``` + +## Advanced Settings + +The following sections describe advanced settings for the `function.yaml` file. + +⚠️ Only change the default values once you understand the implications outlined +in the [performance and scalability](#performance-and-scalability) section. + +### `containerConcurrency` (Function) + +The field `containerConcurrency` influences how many events the function will +receive concurrently (i.e. "in-flight") from the attached trigger. + +In the default `containerConcurrency: 1` setting, the function will process +exactly one event at the same time (no concurrency). + +When changing this field to a higher value, +`rabbitmq.eventing.knative.dev/prefetchCount` must also be changed accordingly. + +### `rabbitmq.eventing.knative.dev/prefetchCount` (Trigger) + +The field `rabbitmq.eventing.knative.dev/prefetchCount` influences how many +events the corresponding trigger will pull (prefetch) from the event queue +(broker) and process them in parallel, i.e. send to the function. + +In order for this setting to be effective, the function must be configured with +`containerConcurrency >= prefetchCount` (recommended) or +`autoscaling.knative.dev/maxScale >= prefetchCount`. + +### `autoscaling.knative.dev/[min|max]Scale` (Function) + +The fields `autoscaling.knative.dev/minScale` and +`autoscaling.knative.dev/maxScale` influence how many instances of the function +are allowed to run. + +In the default setting (below) the function is a singleton and will not scale to +zero: + +```yaml +autoscaling.knative.dev/maxScale: "1" +autoscaling.knative.dev/minScale: "1" +``` + +To enable [scale to +zero](https://knative.dev/docs/serving/autoscaling/scale-to-zero/), set +`autoscaling.knative.dev/minScale: "0"`. + +In busy vSphere environments with lots of tagging operations and many objects, +it might be required to allow multiple instances of the function to share the +event queue ("worker pattern"). This should be the last resort if changing +`containerConcurrency` is not sufficient. + +Depending on the number of events per second and settings in +`containerConcurrency` and `rabbitmq.eventing.knative.dev/prefetchCount`, the +autoscaler will then create multiple instances of the function. + +### Example + +To handle up to **100 concurrent** tagging events if the default values (FIFO +semantics) are not appropriate: + +*Trigger settings:* + +```yaml +# pull up to 200 events (batch) from broker +rabbitmq.eventing.knative.dev/prefetchCount: "200" +``` + +*Function settings:* + +```yaml +# each function instance will process up to 20 events concurrently +containerConcurrency: 20 +``` + +```yaml +# scale to zero and max 10 function instances +autoscaling.knative.dev/maxScale: "10" +autoscaling.knative.dev/minScale: "0" +``` + +# Step 4 - Undeploy + +```bash +# undeploy function +kubectl delete -f function.yaml -n vmware-functions + +# delete secret +kubectl delete secret vsphere-credentials -n vmware-functions +kubectl delete secret nsx-credentials -n vmware-functions +``` diff --git a/examples/knative/go/kn-go-nsx-tag-sync/function.yaml b/examples/knative/go/kn-go-nsx-tag-sync/function.yaml new file mode 100644 index 00000000..b8c93f56 --- /dev/null +++ b/examples/knative/go/kn-go-nsx-tag-sync/function.yaml @@ -0,0 +1,88 @@ +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: kn-go-nsx-tag-sync + labels: + app: veba-ui +spec: + template: + metadata: + annotations: + autoscaling.knative.dev/maxScale: "1" + autoscaling.knative.dev/minScale: "1" + spec: + # max events in-flight - if this setting and prefetchCount in trigger is + # greater than 1 FIFO order is not respected + containerConcurrency: 1 + containers: + - image: us.gcr.io/daisy-284300/veba/kn-go-nsx-tag-sync:1.0 + imagePullPolicy: IfNotPresent + env: + - name: VCENTER_URL + value: "https://replace.me" + - name: VCENTER_INSECURE + value: "false" + - name: VCENTER_SECRET_PATH + value: "/var/bindings/vsphere" # default + - name: NSX_URL + value: "https://replace.me" + - name: NSX_INSECURE + value: "false" + - name: NSX_SECRET_PATH + value: "/var/bindings/nsx" # default + - name: DEBUG + value: "true" + volumeMounts: + - name: vsphere-credentials + mountPath: /var/bindings/vsphere + readOnly: true + - name: nsx-credentials + mountPath: /var/bindings/nsx + readOnly: true + volumes: + - name: vsphere-credentials + secret: + secretName: vsphere-credentials + - name: nsx-credentials + secret: + secretName: nsx-credentials +--- +apiVersion: eventing.knative.dev/v1 +kind: Trigger +metadata: + name: veba-go-nsx-tag-sync-trigger-attach + annotations: + # Value must be between 1 and 1000 + # A value of 1 RabbitMQ Trigger behaves as a FIFO queue when function maxScale=1 + # Values above 1 break message ordering guarantees but can be seen as more performance oriented. + rabbitmq.eventing.knative.dev/prefetchCount: "1" +spec: + broker: default + filter: + attributes: + subject: com.vmware.cis.tagging.attach + subscriber: + ref: + apiVersion: v1 + kind: Service + name: kn-go-nsx-tag-sync +--- +apiVersion: eventing.knative.dev/v1 +kind: Trigger +metadata: + name: veba-go-nsx-tag-sync-trigger-detach + annotations: + # Value must be between 1 and 1000 + # A value of 1 RabbitMQ Trigger behaves as a FIFO queue when function maxScale=1 + # Values above 1 break message ordering guarantees but can be seen as more performance oriented. + rabbitmq.eventing.knative.dev/prefetchCount: "1" +spec: + broker: default + filter: + attributes: + subject: com.vmware.cis.tagging.detach + subscriber: + ref: + apiVersion: v1 + kind: Service + name: kn-go-nsx-tag-sync diff --git a/examples/knative/go/kn-go-nsx-tag-sync/go.mod b/examples/knative/go/kn-go-nsx-tag-sync/go.mod new file mode 100644 index 00000000..91a3defd --- /dev/null +++ b/examples/knative/go/kn-go-nsx-tag-sync/go.mod @@ -0,0 +1,39 @@ +module github.com/vmware-samples/vcenter-event-broker-appliance/examples/knative/go/kn-go-nsx-tag-sync + +go 1.17 + +require ( + github.com/cloudevents/sdk-go/v2 v2.8.0 + github.com/embano1/vsphere v0.2.1 + github.com/google/uuid v1.3.0 + github.com/kelseyhightower/envconfig v1.4.0 + github.com/vmware/go-vmware-nsxt v0.0.0-20220328155605-f49a14c1ef5f + github.com/vmware/govmomi v0.27.2 + go.uber.org/zap v1.20.0 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c + gotest.tools/v3 v3.0.3 + +) + +require ( + github.com/antihax/optional v1.0.0 // indirect + github.com/benbjohnson/clock v1.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/golang/protobuf v1.5.2 // indirect + github.com/google/go-cmp v0.5.6 // indirect + github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/hashicorp/go-multierror v1.0.0 // indirect + github.com/json-iterator/go v1.1.11 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/testify v1.7.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 // indirect + golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d // indirect + google.golang.org/appengine v1.6.7 // indirect + google.golang.org/protobuf v1.26.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect +) diff --git a/examples/knative/go/kn-go-nsx-tag-sync/go.sum b/examples/knative/go/kn-go-nsx-tag-sync/go.sum new file mode 100644 index 00000000..44b1ce80 --- /dev/null +++ b/examples/knative/go/kn-go-nsx-tag-sync/go.sum @@ -0,0 +1,733 @@ +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/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/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/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +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/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= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= +github.com/Azure/go-autorest/autorest v0.11.12/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw= +github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A= +github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= +github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= +github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= +github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +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/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= +github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/a8m/tree v0.0.0-20210115125333-10a5fd5b637d/go.mod h1:FSdwKX97koS5efgm8WevNf7XS3PqtyFkKDDXrz778cg= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= +github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +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/cloudevents/sdk-go/v2 v2.8.0 h1:kmRaLbsafZmidZ0rZ6h7WOMqCkRMcVTLV5lxV/HKQ9Y= +github.com/cloudevents/sdk-go/v2 v2.8.0/go.mod h1:GpCBmUj7DIRiDhVvsK5d6WCbgTWs8DxAWTRtAwQmIXs= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.11/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/davecgh/go-xdr v0.0.0-20161123171359-e6a2ba005892/go.mod h1:CTDl0pzVzE5DEzZhPfvhY/9sPFMQIxaJ9VAMs9AagrE= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= +github.com/embano1/vsphere v0.2.1 h1:2iXK1QzX45aVprbYB5qaR/lG5/ic10iaKTtivoR51og= +github.com/embano1/vsphere v0.2.1/go.mod h1:1G9PR75ds28DW/s4SOndi1htxKScpPCtii/Zq4nhvT0= +github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful v2.9.5+incompatible/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ= +github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +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-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= +github.com/go-logr/logr v0.2.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= +github.com/go-logr/zapr v0.4.0/go.mod h1:tabnROwaDl0UNxkVeFRbY8bwB37GwRv0P8lg6aAiEnk= +github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= +github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= +github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= +github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= +github.com/go-openapi/spec v0.19.5/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= +github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +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-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +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/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.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.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.2/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/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.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/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.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/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/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= +github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +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/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +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/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +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/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +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/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= +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/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= +github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= +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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.2/go.mod h1:CObGmKUOKaSC0RjmoAK7tKyn4Azo5P2IWuoMnvwxz1E= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.13.0/go.mod h1:lRk9szgn8TxENtWd0Tp4c3wjlRfMTMH27I+3Je41yGY= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= +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.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +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.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +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/vladimirvivien/gexe v0.1.1/go.mod h1:LHQL00w/7gDUKIak24n801ABp8C+ni6eBht9vGVst8w= +github.com/vmware/go-vmware-nsxt v0.0.0-20220328155605-f49a14c1ef5f h1:NbC9yOr5At92seXK+kOr2TzU3mIWzcJOVzZasGSuwoU= +github.com/vmware/go-vmware-nsxt v0.0.0-20220328155605-f49a14c1ef5f/go.mod h1:VEqcmf4Sp7gPB7z05QGyKVmn6xWppr7Nz8cVNvyC80o= +github.com/vmware/govmomi v0.27.2 h1:Ecooqg069gUbl5EuWYwcrvzRqMkah9J8BXaf9HCEGVM= +github.com/vmware/govmomi v0.27.2/go.mod h1:daTuJEcQosNMXYJOeku0qdBJP9SOLLWB3Mqz8THtv6o= +github.com/vmware/vmw-guestinfo v0.0.0-20170707015358-25eff159a728/go.mod h1:x9oS4Wk2s2u4tS29nEaDLdzvuHdB19CvSGJjPgkZJNk= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= +go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= +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.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +go.uber.org/zap v1.20.0 h1:N4oPlghZwYG55MlU6LXk/Zp00FVNE9X9wrYO8CEs4lc= +go.uber.org/zap v1.20.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +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-20190611184440-5c40567a22f8/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.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +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/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.2/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-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/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-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/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-20200520004742-59133d7f0dd7/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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +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 h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= +golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +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-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190422165155-953cdadca894/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-20190616124812-15dcb6c0061f/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-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/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-20200106162015-b016eb3dc98e/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-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +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.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +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-20210220033141-f8bda1e9f3ba/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +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-20190328211700-ab21143f2384/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-20190614205625-5aca471b1d59/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-20190624222133-a101b041ded4/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-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/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-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/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-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= +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.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= +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.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-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/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.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/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 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +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-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= +gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/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.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= +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= +k8s.io/api v0.21.1/go.mod h1:FstGROTmsSHBarKc8bylzXih8BLNYTiS3TZcsoEDg2s= +k8s.io/apiextensions-apiserver v0.21.1/go.mod h1:KESQFCGjqVcVsZ9g0xX5bacMjyX5emuWcS2arzdEouA= +k8s.io/apimachinery v0.21.1/go.mod h1:jbreFvJo3ov9rj7eWT7+sYiRx+qZuCYXwWT1bcDswPY= +k8s.io/apiserver v0.21.1/go.mod h1:nLLYZvMWn35glJ4/FZRhzLG/3MPxAaZTgV4FJZdr+tY= +k8s.io/client-go v0.21.1/go.mod h1:/kEw4RgW+3xnBGzvp9IWxKSNA+lXn3A7AuH3gdOAzLs= +k8s.io/code-generator v0.21.1/go.mod h1:hUlps5+9QaTrKx+jiM4rmq7YmH8wPOIko64uZCHDh6Q= +k8s.io/component-base v0.21.1/go.mod h1:NgzFZ2qu4m1juby4TnrmpR8adRk6ka62YdH5DkIIyKA= +k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= +k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= +k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= +k8s.io/klog/v2 v2.8.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= +k8s.io/kube-openapi v0.0.0-20210305001622-591a79e4bda7/go.mod h1:wXW5VT87nVfh/iLV8FpR2uDvrFyomxbtb1KivDbvPTE= +k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/utils v0.0.0-20210527160623-6fdb442a123b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +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/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= +sigs.k8s.io/controller-runtime v0.9.0/go.mod h1:TgkfvrhhEw3PlI0BRL/5xM+89y3/yc0ZDfdbTl84si8= +sigs.k8s.io/e2e-framework v0.0.5/go.mod h1:ckH1mQj5eeRTxndiTLuVF+oq5fDIJsfvIVjtNM4npcI= +sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/structured-merge-diff/v4 v4.1.0/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/examples/knative/go/kn-go-nsx-tag-sync/main.go b/examples/knative/go/kn-go-nsx-tag-sync/main.go new file mode 100644 index 00000000..d71bb4e2 --- /dev/null +++ b/examples/knative/go/kn-go-nsx-tag-sync/main.go @@ -0,0 +1,75 @@ +package main + +import ( + "context" + "errors" + "os" + "os/signal" + "syscall" + + "github.com/embano1/vsphere/logger" + "github.com/kelseyhightower/envconfig" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + + "github.com/vmware-samples/vcenter-event-broker-appliance/examples/knative/go/kn-go-nsx-tag-sync/tags" +) + +var ( + buildCommit = "unknown" + buildTag = "unknown" +) + +func main() { + var cfg tags.Config + if err := envconfig.Process("", &cfg); err != nil { + panic("process environment variables: " + err.Error()) + } + + log, err := getLogger(cfg.Debug) + if err != nil { + panic("create logger: " + err.Error()) + } + log = log.Named("nsx-tag-sync") + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + ctx = logger.Set(ctx, log) + + syncer, err := tags.NewSyncer(ctx) + if err != nil { + log.Fatal("could not create vsphere to nsx tag synchronizer", zap.Error(err)) + } + + log.Info("starting vsphere to nsx tag synchronizer", + zap.Int("listenPort", cfg.Port), + zap.Bool("debug", cfg.Debug), + ) + + if err = syncer.Run(ctx); err != nil && !errors.Is(err, context.Canceled) { + log.Fatal("could not run vsphere to nsx tag synchronizer", zap.Error(err)) + } + log.Info("shutdown complete") +} + +func getLogger(debug bool) (*zap.Logger, error) { + fields := []zap.Field{ + zap.String("commit", buildCommit), + zap.String("tag", buildTag), + } + + var config zap.Config + if debug { + config = zap.NewDevelopmentConfig() + } else { + config = zap.NewProductionConfig() + config.EncoderConfig.EncodeTime = zapcore.RFC3339NanoTimeEncoder + } + + log, err := config.Build(zap.Fields(fields...)) + if err != nil { + return nil, err + } + + return log, nil +} diff --git a/examples/knative/go/kn-go-nsx-tag-sync/tags/nsx.go b/examples/knative/go/kn-go-nsx-tag-sync/tags/nsx.go new file mode 100644 index 00000000..b8cdcab8 --- /dev/null +++ b/examples/knative/go/kn-go-nsx-tag-sync/tags/nsx.go @@ -0,0 +1,112 @@ +package tags + +import ( + "context" + "fmt" + "io/ioutil" + "net/url" + "path/filepath" + "time" + + "github.com/embano1/vsphere/logger" + "github.com/kelseyhightower/envconfig" + nsxt "github.com/vmware/go-vmware-nsxt" + "go.uber.org/zap" +) + +const ( + nsxApiBase = "/api/v1" + nsxAPITimeout = time.Second * 5 // http client transport timeout per request + nsxSessionRefresh = time.Minute +) + +var defaultRetryOnStatusCodes = []int{429, 500, 503} // nsx client + +func newNSXClient(ctx context.Context) (*nsxt.APIClient, error) { + var cfg Config + if err := envconfig.Process("", &cfg); err != nil { + return nil, fmt.Errorf("process environment variables: %w", err) + } + + log := logger.Get(ctx) + + nsxMgr, err := url.Parse(cfg.NSXAddress) + if err != nil { + return nil, fmt.Errorf("parse NSX_URL server value: %w", err) + } + + user, err := readKey("username") + if err != nil { + return nil, fmt.Errorf("read nsx username secret value: %w", err) + } + if user == "" { + return nil, fmt.Errorf("nsx username secret value must not be empty") + } + + pass, err := readKey("password") + if err != nil { + return nil, fmt.Errorf("read nsx password secret value: %w", err) + } + if pass == "" { + return nil, fmt.Errorf("nsx password secret value must not be empty") + } + + nsxCfg := nsxt.Configuration{ + // TODO (mgasch): CA/certs + BasePath: nsxApiBase, + Host: nsxMgr.Host, + Scheme: nsxMgr.Scheme, + UserName: user, + Password: pass, + SkipSessionAuth: true, // https://github.com/vmware/go-vmware-nsxt/issues/51 + UserAgent: "veba-nsx-tag-sync/1.0", + Insecure: cfg.NSXInsecure, + DefaultHeader: map[string]string{}, + RetriesConfiguration: nsxt.ClientRetriesConfiguration{ + MaxRetries: 3, + RetryMinDelay: 500, + RetryMaxDelay: 30000, + RetryOnStatuses: defaultRetryOnStatusCodes, + }, + } + + // we need to do this trick to use init logic for http client but customize + // request timeouts to not block on requests forever + if err = nsxt.InitHttpClient(&nsxCfg); err != nil { + return nil, fmt.Errorf("initialize nsx http client: %w", err) + } + nsxCfg.HTTPClient.Timeout = nsxAPITimeout + + // creates unauthenticated client + nsxClient, err := nsxt.NewAPIClient(&nsxCfg) + if err != nil { + return nil, fmt.Errorf("create nsx client: %w", err) + } + + log.Info("connecting to nsx manager", zap.String("host", cfg.NSXAddress)) + if cfg.NSXInsecure { + log.Warn("using potentially insecure connection to nsx manager", zap.Bool("insecure", cfg.NSXInsecure)) + } + + // create session + err = nsxt.GetDefaultHeaders(nsxClient) + if err != nil { + return nil, fmt.Errorf("connect to nsx manager: %w", err) + } + + return nsxClient, nil +} + +// readKey reads the file from the secret path +func readKey(key string) (string, error) { + var env Config + if err := envconfig.Process("", &env); err != nil { + return "", err + } + + data, err := ioutil.ReadFile(filepath.Join(env.NSXSecretPath, key)) + if err != nil { + return "", err + } + return string(data), nil +} diff --git a/examples/knative/go/kn-go-nsx-tag-sync/tags/tags.go b/examples/knative/go/kn-go-nsx-tag-sync/tags/tags.go new file mode 100644 index 00000000..5e68cc5f --- /dev/null +++ b/examples/knative/go/kn-go-nsx-tag-sync/tags/tags.go @@ -0,0 +1,281 @@ +package tags + +import ( + "context" + "errors" + "fmt" + nethttp "net/http" + "reflect" + "sync" + "time" + + ce "github.com/cloudevents/sdk-go/v2" + "github.com/cloudevents/sdk-go/v2/protocol/http" + vsphere "github.com/embano1/vsphere/client" + "github.com/embano1/vsphere/logger" + "github.com/kelseyhightower/envconfig" + nsxt "github.com/vmware/go-vmware-nsxt" + "github.com/vmware/go-vmware-nsxt/common" + "github.com/vmware/go-vmware-nsxt/manager" + "github.com/vmware/govmomi/find" + "github.com/vmware/govmomi/vim25/types" + "go.uber.org/zap" + "golang.org/x/sync/errgroup" + "golang.org/x/sync/singleflight" +) + +// Config configures the syncer +type Config struct { + // cloudevents listener + Port int `envconfig:"PORT" default:"8080"` // knative injected + Debug bool `envconfig:"DEBUG" default:"false"` + + // NSX + NSXAddress string `envconfig:"NSX_URL" required:"true"` + NSXInsecure bool `envconfig:"NSX_INSECURE" default:"false"` + NSXSecretPath string `envconfig:"NSX_SECRET_PATH" default:"/var/bindings/nsx"` + + // vCenter + vsphere.Config +} + +// Syncer synchronizes vSphere tags to nsx +type Syncer struct { + vsphere *vsphere.Client + ce ce.Client + + sessionLock sync.RWMutex // periodically recreate nsx session + nsx *nsxt.APIClient + + serializer *singleflight.Group // dedupe concurrent operations for same vm object +} + +// NewSyncer returns an initialized syncer configured via environment variables +func NewSyncer(ctx context.Context) (*Syncer, error) { + var cfg Config + if err := envconfig.Process("", &cfg); err != nil { + return nil, fmt.Errorf("process environment variables: %w", err) + } + + log := logger.Get(ctx) + log.Info("connecting to vcenter", zap.String("host", cfg.Address)) + if cfg.Insecure { + log.Warn("using potentially insecure connection to vcenter", zap.Bool("insecure", cfg.Insecure)) + } + + var s Syncer + vc, err := vsphere.New(ctx) + if err != nil { + return nil, fmt.Errorf("create vsphere client: %w", err) + } + s.vsphere = vc + + nsx, err := newNSXClient(ctx) + if err != nil { + return nil, fmt.Errorf("create nsx client: %w", err) + } + + s.nsx = nsx + + s.serializer = &singleflight.Group{} + ceClient, err := ce.NewClientHTTP(http.WithPort(cfg.Port)) + if err != nil { + return nil, fmt.Errorf("create cloudevents http client: %w", err) + } + s.ce = ceClient + + return &s, nil +} + +// Run runs the syncer +func (s *Syncer) Run(ctx context.Context) error { + eg, egCtx := errgroup.WithContext(ctx) + + eg.Go(func() error { + return s.ce.StartReceiver(egCtx, s.handler) + }) + + // nsx session handling + eg.Go(func() error { + ticker := time.NewTicker(nsxSessionRefresh) + defer ticker.Stop() + + for { + select { + case <-egCtx.Done(): + return egCtx.Err() + case <-ticker.C: + reauth := func() error { + logger.Get(egCtx).Debug("attempting to reauthenticate nsx session") + s.sessionLock.Lock() + defer s.sessionLock.Unlock() + + // note: could block up to nsxAPITimeout + if err := nsxt.GetDefaultHeaders(s.nsx); err != nil { + return fmt.Errorf("reauthenticate nsx session: %w", err) + } + logger.Get(egCtx).Debug("successfully reauthenticated nsx session") + + return nil + }() + + if err := reauth; err != nil { + return err + } + } + } + }) + + eg.Go(func() error { + <-egCtx.Done() + logger.Get(egCtx).Info("received shutdown signal", zap.String("signal", egCtx.Err().Error())) + return nil + }) + + return eg.Wait() +} + +func (s *Syncer) handler(ctx context.Context, event ce.Event) error { + log := logger.Get(ctx).With(zap.String("eventID", event.ID())) + log.Debug("received event", zap.Any("event", event)) + + var vevent types.EventEx + if err := event.DataAs(&vevent); err != nil { + log.Error("could not marshal event to eventex", zap.Error(err)) + return http.NewResult(nethttp.StatusBadRequest, + "could not marshal cloudevent event data to vsphere event (eventID: %d)", + event.ID(), + ) + } + + var object string + for _, arg := range vevent.Arguments { + if arg.Key == "Object" { + if o, ok := arg.Value.(string); ok { + object = o + break + } else { + valueType := reflect.TypeOf(arg.Value).String() + log.Error("could not convert eventex argument value to string", + zap.Any("argumentObject", arg), + zap.String("valueType", valueType), + ) + return http.NewResult(nethttp.StatusBadRequest, + "could not read object value from event arguments (eventID: %d)", + event.ID(), + ) + } + } + } + + if object == "" { + log.Error("event did not contain object key", zap.Any("event", event)) + return http.NewResult(nethttp.StatusBadRequest, + "could not read object value from event arguments (eventID: %d)", + event.ID(), + ) + } + + ctx = logger.Set(ctx, log) + nsxTimeout := time.Second * 3 + _, err, shared := s.serializer.Do(object, s.syncVmTags(ctx, object, nsxTimeout)) + if shared { + log.Info("serialized and deduplicated concurrent calls to object ", zap.String("object", object)) + } + + if err != nil { + // either tag not on a vm object or vm already removed from inventory + var nfe *find.NotFoundError + if errors.As(err, &nfe) { + log.Warn("ignoring object", zap.String("object", object), zap.Error(err)) + + // return 400 instead of 404 because some brokers retry on 404 + return http.NewResult(nethttp.StatusBadRequest, + "could not synchronize tags for object %q (eventID: %d)", + object, + event.ID(), + ) + } + + log.Error("could not synchronize vm tags", zap.String("object", object), zap.Error(err)) + return http.NewResult(nethttp.StatusInternalServerError, + "could not synchronize tags for vm %q (eventID: %d)", + object, + event.ID(), + ) + } + + return nil +} + +func (s *Syncer) syncVmTags(ctx context.Context, vm string, nsxTimeout time.Duration) func() (interface{}, error) { + return func() (interface{}, error) { + log := logger.Get(ctx) + log.Debug("retrieving vm managed object reference", zap.String("object", vm)) + + ref, err := getVmRef(ctx, s.vsphere.SOAP.Client, vm) + if err != nil { + return nil, fmt.Errorf("find vm reference for object %q: %w", vm, err) + } + + log.Debug("retrieving vm instance id", zap.Any("ref", ref)) + id, err := getInstancedID(ctx, s.vsphere.SOAP.Client, ref) + if err != nil { + return nil, fmt.Errorf("retrieve vm instance id for ref %q: %w", ref, err) + } + + log.Debug("retrieving vm tags", zap.Any("ref", ref)) + attachedTags, err := s.vsphere.Tags.ListAttachedTags(ctx, ref) + if err != nil { + return nil, fmt.Errorf("list attached tags: %w", err) + } + + req := manager.VirtualMachineTagUpdate{ + ExternalId: id, + Tags: make([]common.Tag, len(attachedTags)), + } + + for idx, t := range attachedTags { + details, err := s.vsphere.Tags.GetTag(ctx, t) + if err != nil { + return nil, fmt.Errorf("get tag %q: %w", t, err) + } + + categoryDetails, err := s.vsphere.Tags.GetCategory(ctx, details.CategoryID) + if err != nil { + return nil, fmt.Errorf("get category %q: %w", details.CategoryID, err) + } + + req.Tags[idx] = common.Tag{ + Scope: categoryDetails.Name, + Tag: details.Name, + } + } + + // use explicitly provided timeout here to return fast from this singleflight + // routine and reduce likelihood of stale tag information during concurrent + // (blocked) operations on the same object (key) + // + // note: in case timeout fires before nsx ack, singleflight caller returns HTTP + // 500 due to ctx.Error() in the event handler, causing event sender (broker) + // typically to retry + ctx, cancel := context.WithTimeout(ctx, nsxTimeout) + defer cancel() + log.Info("updating virtual machine tags in nsx", zap.Any("request", req)) + + updateTags := func() error { + s.sessionLock.RLock() + defer s.sessionLock.RUnlock() + if _, err = s.nsx.FabricApi.UpdateVirtualMachineTagsUpdateTags(ctx, req); err != nil { + return err + } + return nil + } + + if err = updateTags(); err != nil { + return nil, fmt.Errorf("update virtual machine tags in nsx: %w", err) + } + + return nil, nil + } +} diff --git a/examples/knative/go/kn-go-nsx-tag-sync/tags/tags_test.go b/examples/knative/go/kn-go-nsx-tag-sync/tags/tags_test.go new file mode 100644 index 00000000..dcab7296 --- /dev/null +++ b/examples/knative/go/kn-go-nsx-tag-sync/tags/tags_test.go @@ -0,0 +1,278 @@ +package tags + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + nethttp "net/http" + "net/url" + "sync" + "testing" + "time" + + ce "github.com/cloudevents/sdk-go/v2" + "github.com/cloudevents/sdk-go/v2/client/test" + "github.com/cloudevents/sdk-go/v2/protocol/http" + vsphere "github.com/embano1/vsphere/client" + "github.com/embano1/vsphere/logger" + "github.com/google/uuid" + nsxt "github.com/vmware/go-vmware-nsxt" + "github.com/vmware/go-vmware-nsxt/manager" + "github.com/vmware/govmomi" + "github.com/vmware/govmomi/session" + "github.com/vmware/govmomi/simulator" + "github.com/vmware/govmomi/vapi/rest" + _ "github.com/vmware/govmomi/vapi/simulator" + "github.com/vmware/govmomi/vapi/tags" + "github.com/vmware/govmomi/vim25" + "github.com/vmware/govmomi/vim25/types" + "go.uber.org/zap/zaptest" + "golang.org/x/sync/singleflight" + "gotest.tools/v3/assert" +) + +func newVmPoweredOnEvent() types.BaseEvent { + return &types.VmPoweredOnEvent{ + VmEvent: types.VmEvent{ + Event: types.Event{ + Key: 1, + ChainId: 1, + CreatedTime: time.Now(), + UserName: "administrator", + Vm: &types.VmEventArgument{ + Vm: types.ManagedObjectReference{ + Type: "VirtualMachine", + Value: "vm-1", + }, + }, + }, + }, + } +} + +func newCloudEvent(t *testing.T, data interface{}) ce.Event { + t.Helper() + e := ce.NewEvent() + e.SetID(uuid.New().String()) + e.SetType("test.event.v0") + e.SetSource("test.source") + + err := e.SetData(ce.ApplicationJSON, data) + assert.NilError(t, err) + + return e +} + +func TestSyncer_Run(t *testing.T) { + t.Run("returns when context cancelled", func(t *testing.T) { + ceClient, _ := test.NewMockReceiverClient(t, 1) + s := &Syncer{ + ce: ceClient, + serializer: &singleflight.Group{}, + } + + ctx := logger.Set(context.Background(), zaptest.NewLogger(t)) + ctx, cancel := context.WithTimeout(ctx, time.Millisecond*500) + defer cancel() + + err := s.Run(ctx) + assert.ErrorType(t, err, context.DeadlineExceeded) + }) +} + +func TestSyncer_handler(t *testing.T) { + type wantError struct { + message string + code int + } + + testCases := []struct { + name string + event ce.Event + wantError wantError + }{ + { + name: "cloudevent data is not vsphere event", + event: newCloudEvent(t, `{"hello":"world"}`), + wantError: wantError{message: "could not marshal", code: 400}, + }, { + name: "cloudevent data is not tagging event", + event: newCloudEvent(t, newVmPoweredOnEvent()), + wantError: wantError{message: "could not read object", code: 400}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + ctx := logger.Set(context.Background(), zaptest.NewLogger(t)) + + s := &Syncer{} + err := s.handler(ctx, tc.event) + assert.ErrorType(t, err, &http.Result{}) + assert.Equal(t, err.(*http.Result).StatusCode, tc.wantError.code) + }) + } +} + +func TestSyncer_syncTags(t *testing.T) { + testCases := []struct { + name string + object string // vm + tagMap map[string]string // category/tag mappings + nsxMock *nsxAPIMock // mock NSX API responses + wantErr string + }{ + { + name: "fails when vm object cannot be found", + object: "notexist", + nsxMock: &nsxAPIMock{}, + wantErr: "not found", + }, + { + name: "fails on nsx http 500 error", + object: "DC0_H0_VM0", + nsxMock: &nsxAPIMock{codes: []int{500}}, + wantErr: "500", + }, + { + name: "successfully synchronizes tags", + object: "DC0_H0_VM0", + tagMap: map[string]string{ + "category1": "tag1", + "category2": "tag2", + }, + nsxMock: &nsxAPIMock{codes: []int{200}}, + wantErr: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + simulator.Run(func(ctx context.Context, client *vim25.Client) error { + ctx = logger.Set(ctx, zaptest.NewLogger(t)) + + tc.nsxMock.t = t + mockHttp := nethttp.DefaultClient + mockHttp.Transport = tc.nsxMock + + nsxCfg := nsxt.Configuration{ + SkipSessionAuth: true, + RetriesConfiguration: nsxt.ClientRetriesConfiguration{}, // disable retries + HTTPClient: mockHttp, + } + + nsxClient, err := nsxt.NewAPIClient(&nsxCfg) + assert.NilError(t, err) + + govm := govmomi.Client{ + Client: client, + SessionManager: session.NewManager(client), + } + + rc := rest.NewClient(client) + err = rc.Login(ctx, url.UserPassword("user", "pass")) + assert.NilError(t, err) + tm := tags.NewManager(rc) + + s := &Syncer{ + vsphere: &vsphere.Client{ + SOAP: &govm, + REST: rc, + Tags: tm, + }, + nsx: nsxClient, + } + + var wantID string + + // only required when we don't expect error + if tc.wantErr == "" { + ref, err := getVmRef(ctx, s.vsphere.SOAP.Client, tc.object) + assert.NilError(t, err) + wantID, err = getInstancedID(ctx, s.vsphere.SOAP.Client, ref) + assert.NilError(t, err) + attachTags(t, ctx, s.vsphere.Tags, ref, tc.tagMap) + } + + _, err = s.syncVmTags(ctx, tc.object, time.Second)() + + if tc.wantErr != "" { + assert.ErrorContains(t, err, tc.wantErr) + } else { + assert.NilError(t, err) + assert.Equal(t, tc.nsxMock.tagReq.ExternalId, wantID) + assert.Equal(t, len(tc.tagMap), len(tc.nsxMock.tagReq.Tags)) + } + + return nil + }) + }) + } +} + +type nsxAPIMock struct { + t *testing.T + codes []int // response codes/count + + sync.Mutex + counter int + + tagReq manager.VirtualMachineTagUpdate // stores last syncer nsx tag request +} + +func (nsx *nsxAPIMock) RoundTrip(req *nethttp.Request) (*nethttp.Response, error) { + var buf bytes.Buffer + + _, err := io.Copy(&buf, req.Body) + assert.NilError(nsx.t, err) + + var tagReq manager.VirtualMachineTagUpdate + err = json.Unmarshal(buf.Bytes(), &tagReq) + assert.NilError(nsx.t, err) + + nsx.Lock() + defer func() { + nsx.counter++ + _ = req.Body.Close() + nsx.Unlock() + }() + + nsx.tagReq = tagReq + + code := nsx.codes[nsx.counter] + return &nethttp.Response{ + StatusCode: code, + Status: fmt.Sprintf("%d", code), + Request: req.Clone(context.TODO()), + }, nil +} + +func attachTags(t *testing.T, ctx context.Context, tm *tags.Manager, ref types.ManagedObjectReference, mappings map[string]string) { + t.Helper() + + var tagIDs []string + for cat, tag := range mappings { + newCat := tags.Category{ + Name: cat, + Description: cat, + // Cardinality: "", + } + catID, err := tm.CreateCategory(ctx, &newCat) + assert.NilError(t, err) + + newTag := tags.Tag{ + Description: tag, + Name: tag, + CategoryID: catID, + } + tagID, err := tm.CreateTag(ctx, &newTag) + assert.NilError(t, err) + + tagIDs = append(tagIDs, tagID) + + } + err := tm.AttachMultipleTagsToObject(ctx, tagIDs, ref) + assert.NilError(t, err) +} diff --git a/examples/knative/go/kn-go-nsx-tag-sync/tags/vsphere.go b/examples/knative/go/kn-go-nsx-tag-sync/tags/vsphere.go new file mode 100644 index 00000000..1b283b40 --- /dev/null +++ b/examples/knative/go/kn-go-nsx-tag-sync/tags/vsphere.go @@ -0,0 +1,32 @@ +package tags + +import ( + "context" + "fmt" + + "github.com/vmware/govmomi/find" + "github.com/vmware/govmomi/property" + "github.com/vmware/govmomi/vim25" + "github.com/vmware/govmomi/vim25/mo" + "github.com/vmware/govmomi/vim25/types" +) + +func getInstancedID(ctx context.Context, client *vim25.Client, ref types.ManagedObjectReference) (string, error) { + var info mo.VirtualMachine + pc := property.DefaultCollector(client) + if err := pc.RetrieveOne(ctx, ref, []string{"config.instanceUuid"}, &info); err != nil { + return "", fmt.Errorf("retrieve instanceUuid property: %w", err) + } + + return info.Config.InstanceUuid, nil +} + +func getVmRef(ctx context.Context, client *vim25.Client, name string) (types.ManagedObjectReference, error) { + f := find.NewFinder(client) + vm, err := f.VirtualMachine(ctx, name) + if err != nil { + return types.ManagedObjectReference{}, err + } + + return vm.Reference(), nil +} diff --git a/examples/knative/go/kn-go-nsx-tag-sync/tags/vsphere_test.go b/examples/knative/go/kn-go-nsx-tag-sync/tags/vsphere_test.go new file mode 100644 index 00000000..cd0f46e5 --- /dev/null +++ b/examples/knative/go/kn-go-nsx-tag-sync/tags/vsphere_test.go @@ -0,0 +1,97 @@ +package tags + +import ( + "context" + "testing" + + "github.com/vmware/govmomi/simulator" + "github.com/vmware/govmomi/vim25" + "github.com/vmware/govmomi/vim25/types" + "gotest.tools/v3/assert" +) + +func Test_getVmRef(t *testing.T) { + tests := []struct { + name string + vm string + want types.ManagedObjectReference + wantErr string + }{ + { + name: "retrieves ref for vm", + vm: "DC0_H0_VM0", + want: types.ManagedObjectReference{ + Type: "VirtualMachine", + Value: "vm-54", + }, + wantErr: "", + }, + { + name: "vm does not exist", + vm: "vm-not-exist", + want: types.ManagedObjectReference{ + Type: "", + Value: "", + }, + wantErr: "not found", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + simulator.Run(func(ctx context.Context, client *vim25.Client) error { + ref, err := getVmRef(ctx, client, tt.vm) + assert.Equal(t, tt.want, ref) + + if tt.wantErr != "" { + assert.ErrorContains(t, err, tt.wantErr) + } else { + assert.NilError(t, err) + } + + return nil + }) + }) + } +} + +func Test_getInstancedID(t *testing.T) { + tests := []struct { + name string + vm types.ManagedObjectReference + wantErr string + }{ + { + name: "retrieves id for vm", + vm: types.ManagedObjectReference{ + Type: "VirtualMachine", + Value: "vm-54", + }, + wantErr: "", + }, + { + name: "vm does not exist", + vm: types.ManagedObjectReference{ + Type: "VirtualMachine", + Value: "invalid", + }, + wantErr: "has already been deleted", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + simulator.Run(func(ctx context.Context, client *vim25.Client) error { + id, err := getInstancedID(ctx, client, tt.vm) + + if tt.wantErr != "" { + assert.Equal(t, id, "") + assert.ErrorContains(t, err, tt.wantErr) + } else { + assert.NilError(t, err) + assert.Assert(t, len(id) > 0) + } + + return nil + }) + }) + } +} From 0da7740eb7d51fe24bca4bb2a607b04f5b16be2f Mon Sep 17 00:00:00 2001 From: Adrian Begg Date: Wed, 13 Apr 2022 12:41:18 +0200 Subject: [PATCH 12/54] feat: Add iam_auth_role for EventBridge Adjusted the config and aws_event_bridge processor to support the aws_iam_role AuthMethod. The purpose of this is to enable short-term credentials generated from assumed role on the executing EKS pod or EC2 machine to be used to access Amazon Event Bridge. Closes: #823 Signed-off-by: Adrian Begg --- ...uter-config-vcenter-aws-iam-role-auth.yaml | 38 +++++++++++++ .../internal/config/v1alpha1/auth.go | 6 ++- .../internal/config/v1alpha1/processor.go | 4 +- .../processor/aws/aws_event_bridge.go | 53 +++++++++++++------ 4 files changed, 81 insertions(+), 20 deletions(-) create mode 100644 vmware-event-router/deploy/event-router-config-vcenter-aws-iam-role-auth.yaml diff --git a/vmware-event-router/deploy/event-router-config-vcenter-aws-iam-role-auth.yaml b/vmware-event-router/deploy/event-router-config-vcenter-aws-iam-role-auth.yaml new file mode 100644 index 00000000..ca995201 --- /dev/null +++ b/vmware-event-router/deploy/event-router-config-vcenter-aws-iam-role-auth.yaml @@ -0,0 +1,38 @@ +apiVersion: event-router.vmware.com/v1alpha1 +kind: RouterConfig +metadata: + name: router-config-vcenter-aws + labels: + key: value +eventProvider: + type: vcenter + name: veba-demo-vc-01 + vcenter: + address: https://my-vcenter01.domain.local/sdk + insecureSSL: false + checkpoint: false + auth: + type: basic_auth + basicAuth: + username: administrator@vsphere.local + password: ReplaceMe +eventProcessor: + type: aws_event_bridge + name: veba-demo-aws + awsEventBridge: + eventBus: default + region: us-west-1 + ruleARN: arn:aws:events:us-west-1:1234567890:rule/vmware-event-router + auth: + type: aws_iam_role +metricsProvider: + type: default + name: veba-demo-metrics + default: + bindAddress: "0.0.0.0:8082" +# disabling auth for the metrics endpoint +# auth: +# type: basic_auth +# basicAuth: +# username: admin +# password: ReplaceMe diff --git a/vmware-event-router/internal/config/v1alpha1/auth.go b/vmware-event-router/internal/config/v1alpha1/auth.go index a2978eeb..62c322c2 100644 --- a/vmware-event-router/internal/config/v1alpha1/auth.go +++ b/vmware-event-router/internal/config/v1alpha1/auth.go @@ -8,6 +8,10 @@ const ( BasicAuth AuthMethodType = "basic_auth" // AWSAccessKeyAuth represents the AWS IAM authentication method using access key and secret key AWSAccessKeyAuth AuthMethodType = "aws_access_key" + // AWSIAMRoleAuth represents the AWS IAM authentication method using temporary credentials provided + // by Security Token Service (STS). Intended for use as IAM role with a Kubernetes service account + // for use case of running under the Amazon EKS. + AWSIAMRoleAuth AuthMethodType = "aws_iam_role" // ActiveDirectory represents the MS Active Directory domain/user/password scheme ActiveDirectory AuthMethodType = "active_directory" ) @@ -15,7 +19,7 @@ const ( // AuthMethod configures authentication data type AuthMethod struct { // Type sets the authentication method - Type AuthMethodType `yaml:"type" json:"type" jsonschema:"enum=basic_auth,enum=aws_access_key,enum=active_directory,default=basic_auth,description=The authentication method to use"` + Type AuthMethodType `yaml:"type" json:"type" jsonschema:"enum=basic_auth,enum=aws_access_key,enum=aws_iam_role,enum=active_directory,default=basic_auth,description=The authentication method to use"` // +optional BasicAuth *BasicAuthMethod `yaml:"basicAuth,omitempty" json:"basicAuth,omitempty" jsonschema:"oneof_required=basicAuth,description=Basic authentication with username and password"` // +optional diff --git a/vmware-event-router/internal/config/v1alpha1/processor.go b/vmware-event-router/internal/config/v1alpha1/processor.go index 10ff6ee0..9682d45b 100644 --- a/vmware-event-router/internal/config/v1alpha1/processor.go +++ b/vmware-event-router/internal/config/v1alpha1/processor.go @@ -56,8 +56,8 @@ type ProcessorConfigEventBridge struct { // RuleARN is the ARN of the rule to use for configuring pattern matching and event forwarding // TODO (@mgasch): deprecate and support 1..n rules per given eventbus RuleARN string `yaml:"ruleARN" json:"ruleARN" jsonschema:"required,default=arn:aws:events:us-west-1:1234567890:rule/vmware-event-router"` - // Auth sets the AWS authentication credentials. Only aws_access_key is - // supported. + // Auth sets the AWS authentication credentials. Only aws_access_key or + // aws_iam_role is supported. Auth *AuthMethod `yaml:"auth,omitempty" json:"auth,omitempty" jsonschema:"oneof_required=auth,description=Authentication configuration for this section"` } diff --git a/vmware-event-router/internal/processor/aws/aws_event_bridge.go b/vmware-event-router/internal/processor/aws/aws_event_bridge.go index 69eaaf96..4abb1151 100644 --- a/vmware-event-router/internal/processor/aws/aws_event_bridge.go +++ b/vmware-event-router/internal/processor/aws/aws_event_bridge.go @@ -85,6 +85,9 @@ type eventPattern struct { // NewEventBridgeProcessor returns an AWS EventBridge processor for the given // configuration func NewEventBridgeProcessor(ctx context.Context, cfg *config.ProcessorConfigEventBridge, ms metrics.Receiver, log logger.Logger, opts ...Option) (*EventBridgeProcessor, error) { + // Initialize awsSession for the AWS SDK client + var awsSession *session.Session + awsLog := log if zapSugared, ok := log.(*zap.SugaredLogger); ok { proc := strings.ToUpper(string(config.ProcessorEventBridge)) @@ -107,13 +110,6 @@ func NewEventBridgeProcessor(ctx context.Context, cfg *config.ProcessorConfigEve return nil, errors.New("no AWS EventBridge configuration found") } - if cfg.Auth == nil || cfg.Auth.AWSAccessKeyAuth == nil { - return nil, fmt.Errorf("invalid %s credentials: accessKey and secretKey must be set", config.AWSAccessKeyAuth) - } - - accessKey := cfg.Auth.AWSAccessKeyAuth.AccessKey - secretKey := cfg.Auth.AWSAccessKeyAuth.SecretKey - if cfg.Region == "" { return nil, errors.New("region must be specified") } @@ -126,16 +122,39 @@ func NewEventBridgeProcessor(ctx context.Context, cfg *config.ProcessorConfigEve return nil, errors.New("event bus must be specified") } - awsSession, err := session.NewSession(&aws.Config{ - Region: aws.String(cfg.Region), - Credentials: credentials.NewStaticCredentials( - accessKey, - secretKey, - "", // a token will be created when the session is used. - ), - }) - if err != nil { - return nil, errors.Wrap(err, "create AWS session") + // Check the Auth Method to determine how the Session should be established + if cfg.Auth.Type == config.AWSAccessKeyAuth { + if cfg.Auth == nil || cfg.Auth.AWSAccessKeyAuth == nil { + return nil, fmt.Errorf("invalid %s credentials: accessKey and secretKey must be set", config.AWSAccessKeyAuth) + } + accessKey := cfg.Auth.AWSAccessKeyAuth.AccessKey + secretKey := cfg.Auth.AWSAccessKeyAuth.SecretKey + + awsSessionAccessKey, err := session.NewSession(&aws.Config{ + Region: aws.String(cfg.Region), + Credentials: credentials.NewStaticCredentials( + accessKey, + secretKey, + "", // a token will be created when the session is used. + ), + }) + if err != nil { + return nil, errors.Wrap(err, "create AWS session") + } + // Set the AWS Session to the IAM Role authenticated session + awsSession = awsSessionAccessKey + } + if cfg.Auth.Type == config.AWSIAMRoleAuth { + // Create Session without additional options will load credentials region, + // and profile loaded from the environment and shared config automatically + awsSessionIam, err := session.NewSession(&aws.Config{ + Region: aws.String(cfg.Region), + }) + if err != nil { + return nil, errors.Wrap(err, "create AWS session") + } + // Set the AWS Session to the IAM Role authenticated session + awsSession = awsSessionIam } eventBridge.session = *awsSession From 722a3762cef709b1d9f1fd6ff31e6f6620a580d0 Mon Sep 17 00:00:00 2001 From: Adrian Begg Date: Wed, 13 Apr 2022 16:48:33 +0200 Subject: [PATCH 13/54] fix: Remove whitespace from comments Signed-off-by: Adrian Begg --- vmware-event-router/internal/config/v1alpha1/auth.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vmware-event-router/internal/config/v1alpha1/auth.go b/vmware-event-router/internal/config/v1alpha1/auth.go index 62c322c2..ea713bca 100644 --- a/vmware-event-router/internal/config/v1alpha1/auth.go +++ b/vmware-event-router/internal/config/v1alpha1/auth.go @@ -9,7 +9,7 @@ const ( // AWSAccessKeyAuth represents the AWS IAM authentication method using access key and secret key AWSAccessKeyAuth AuthMethodType = "aws_access_key" // AWSIAMRoleAuth represents the AWS IAM authentication method using temporary credentials provided - // by Security Token Service (STS). Intended for use as IAM role with a Kubernetes service account + // by Security Token Service (STS). Intended for use as IAM role with a Kubernetes service account // for use case of running under the Amazon EKS. AWSIAMRoleAuth AuthMethodType = "aws_iam_role" // ActiveDirectory represents the MS Active Directory domain/user/password scheme From e53daa5a01ea8fcb90f26571e9e73f319787e243 Mon Sep 17 00:00:00 2001 From: Michael Gasch Date: Wed, 13 Apr 2022 21:01:37 +0200 Subject: [PATCH 14/54] chore: Update JSON schema Closes: #852 Signed-off-by: Michael Gasch --- vmware-event-router/routerconfig.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vmware-event-router/routerconfig.schema.json b/vmware-event-router/routerconfig.schema.json index 4c0db482..40ebffb5 100644 --- a/vmware-event-router/routerconfig.schema.json +++ b/vmware-event-router/routerconfig.schema.json @@ -1 +1 @@ -{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/RouterConfig","definitions":{"AWSAccessKeyAuthMethod":{"required":["accessKey","secretKey"],"properties":{"accessKey":{"type":"string"},"secretKey":{"type":"string"}},"additionalProperties":false,"type":"object"},"ActiveDirectoryAuthMethod":{"required":["domain","username","password"],"properties":{"domain":{"type":"string"},"username":{"type":"string"},"password":{"type":"string"}},"additionalProperties":false,"type":"object"},"AuthMethod":{"required":["type"],"properties":{"type":{"enum":["basic_auth","aws_access_key","active_directory"],"type":"string","description":"The authentication method to use","default":"basic_auth"},"basicAuth":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/BasicAuthMethod","description":"Basic authentication with username and password"},"awsAccessKeyAuth":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/AWSAccessKeyAuthMethod","description":"AWS authentication with access and secret key"},"activeDirectoryAuth":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ActiveDirectoryAuthMethod","description":"Active Directory authentication with domain"}},"additionalProperties":false,"type":"object","oneOf":[{"required":["basicAuth"],"title":"basicAuth"},{"required":["awsAccessKeyAuth"],"title":"awsAccessKeyAuth"},{"required":["activeDirectoryAuth"],"title":"activeDirectoryAuth"}]},"BasicAuthMethod":{"required":["username","password"],"properties":{"username":{"type":"string"},"password":{"type":"string"}},"additionalProperties":false,"type":"object"},"Certificates":{"properties":{"rootCAs":{"items":{"type":"string"},"type":"array"}},"additionalProperties":false,"type":"object"},"Destination":{"properties":{"ref":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/KReference"},"uri":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/URL"}},"additionalProperties":false,"type":"object"},"KReference":{"required":["kind","name","apiVersion"],"properties":{"kind":{"type":"string"},"namespace":{"type":"string"},"name":{"type":"string"},"apiVersion":{"type":"string"}},"additionalProperties":false,"type":"object"},"MetricsProvider":{"required":["type","name"],"properties":{"type":{"enum":["default"],"type":"string"},"name":{"type":"string"},"default":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/MetricsProviderConfigDefault"}},"additionalProperties":false,"type":"object","oneOf":[{"required":["default"],"title":"default"}]},"MetricsProviderConfigDefault":{"required":["bindAddress"],"properties":{"bindAddress":{"type":"string","default":"0.0.0.0:8082"},"auth":{"$ref":"#/definitions/AuthMethod","description":"Authentication configuration for this section"}},"additionalProperties":false,"type":"object"},"ObjectMeta":{"required":["name"],"properties":{"name":{"type":"string"},"labels":{"patternProperties":{".*":{"type":"string"}},"type":"object"}},"additionalProperties":false,"type":"object"},"Processor":{"required":["type","name"],"properties":{"type":{"enum":["openfaas","aws_event_bridge","knative"],"type":"string"},"name":{"type":"string"},"openfaas":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ProcessorConfigOpenFaaS"},"awsEventBridge":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ProcessorConfigEventBridge"},"knative":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ProcessorConfigKnative"}},"additionalProperties":false,"type":"object","oneOf":[{"required":["openfaas"],"title":"openfaas"},{"required":["awsEventBridge"],"title":"awsEventBridge"},{"required":["knative"],"title":"knative"}]},"ProcessorConfigEventBridge":{"required":["region","eventBus","ruleARN"],"properties":{"region":{"type":"string","default":"us-west-1"},"eventBus":{"type":"string","default":"default"},"ruleARN":{"type":"string","default":"arn:aws:events:us-west-1:1234567890:rule/vmware-event-router"},"auth":{"$ref":"#/definitions/AuthMethod","description":"Authentication configuration for this section"}},"additionalProperties":false,"type":"object","oneOf":[{"required":["auth"],"title":"auth"}]},"ProcessorConfigKnative":{"required":["insecureSSL","encoding"],"properties":{"destination":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/Destination","description":"Destination sink where to send events"},"insecureSSL":{"type":"boolean"},"encoding":{"enum":["binary","structured"],"type":"string","default":"structured"},"auth":{"$ref":"#/definitions/AuthMethod","description":"Authentication configuration for this section"}},"additionalProperties":false,"type":"object","oneOf":[{"required":["destination"],"title":"destination"}]},"ProcessorConfigOpenFaaS":{"required":["address","async"],"properties":{"address":{"type":"string","description":"OpenFaaS gateway address","default":"http://gateway.openfaas:8080"},"async":{"type":"boolean","description":"Use async function invocation mode"},"auth":{"$ref":"#/definitions/AuthMethod","description":"Authentication configuration for this section"}},"additionalProperties":false,"type":"object"},"Provider":{"required":["type","name"],"properties":{"type":{"enum":["vcenter","webhook","vcsim","horizon"],"type":"string"},"name":{"type":"string"},"vcenter":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ProviderConfigVCenter"},"vcsim":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ProviderConfigVCSIM"},"webhook":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ProviderConfigWebhook"},"horizon":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ProviderConfigHorizon"}},"additionalProperties":false,"type":"object","oneOf":[{"required":["vcenter"],"title":"vcenter"},{"required":["vcsim"],"title":"vcsim"},{"required":["webhook"],"title":"webhook"},{"required":["horizon"],"title":"horizon"}]},"ProviderConfigHorizon":{"required":["address","insecureSSL"],"properties":{"address":{"type":"string","default":"https://api.myhorizon.domain.local"},"insecureSSL":{"type":"boolean"},"auth":{"$ref":"#/definitions/AuthMethod","description":"Authentication configuration for this section"}},"additionalProperties":false,"type":"object","oneOf":[{"required":["auth"],"title":"auth"}]},"ProviderConfigVCSIM":{"required":["address","insecureSSL"],"properties":{"address":{"type":"string","default":"https://my-vcenter01.domain.local/sdk"},"insecureSSL":{"type":"boolean"},"auth":{"$ref":"#/definitions/AuthMethod","description":"Authentication configuration for this section"}},"additionalProperties":false,"type":"object","oneOf":[{"required":["auth"],"title":"auth"}]},"ProviderConfigVCenter":{"required":["address","insecureSSL","checkpoint"],"properties":{"address":{"type":"string","default":"https://my-vcenter01.domain.local/sdk"},"insecureSSL":{"type":"boolean"},"checkpoint":{"type":"boolean","description":"Enable checkpointing via checkpoint file for event recovery and replay purposes"},"checkpointDir":{"type":"string","description":"Directory where to persist checkpoints if enabled","default":"./checkpoints"},"auth":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/AuthMethod","description":"Authentication configuration for this section"}},"additionalProperties":false,"type":"object","oneOf":[{"required":["auth"],"title":"auth"}]},"ProviderConfigWebhook":{"required":["bindAddress","path"],"properties":{"bindAddress":{"type":"string","default":"0.0.0.0:8080"},"path":{"type":"string","default":"/webhook"},"auth":{"$ref":"#/definitions/AuthMethod","description":"Authentication configuration for this section"}},"additionalProperties":false,"type":"object"},"RouterConfig":{"required":["apiVersion","kind","metadata","eventProvider","eventProcessor","metricsProvider"],"properties":{"apiVersion":{"enum":["event-router.vmware.com/v1alpha1"],"type":"string"},"kind":{"enum":["RouterConfig"],"type":"string"},"metadata":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ObjectMeta"},"eventProvider":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/Provider"},"eventProcessor":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/Processor"},"metricsProvider":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/MetricsProvider"},"certificates":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/Certificates"}},"additionalProperties":false,"type":"object"},"URL":{"required":["Scheme","Opaque","User","Host","Path","RawPath","ForceQuery","RawQuery","Fragment","RawFragment"],"properties":{"Scheme":{"type":"string"},"Opaque":{"type":"string"},"User":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/Userinfo"},"Host":{"type":"string"},"Path":{"type":"string"},"RawPath":{"type":"string"},"ForceQuery":{"type":"boolean"},"RawQuery":{"type":"string"},"Fragment":{"type":"string"},"RawFragment":{"type":"string"}},"additionalProperties":false,"type":"object"},"Userinfo":{"properties":{},"additionalProperties":false,"type":"object"}}} \ No newline at end of file +{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/RouterConfig","definitions":{"AWSAccessKeyAuthMethod":{"required":["accessKey","secretKey"],"properties":{"accessKey":{"type":"string"},"secretKey":{"type":"string"}},"additionalProperties":false,"type":"object"},"ActiveDirectoryAuthMethod":{"required":["domain","username","password"],"properties":{"domain":{"type":"string"},"username":{"type":"string"},"password":{"type":"string"}},"additionalProperties":false,"type":"object"},"AuthMethod":{"required":["type"],"properties":{"type":{"enum":["basic_auth","aws_access_key","aws_iam_role","active_directory"],"type":"string","description":"The authentication method to use","default":"basic_auth"},"basicAuth":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/BasicAuthMethod","description":"Basic authentication with username and password"},"awsAccessKeyAuth":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/AWSAccessKeyAuthMethod","description":"AWS authentication with access and secret key"},"activeDirectoryAuth":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ActiveDirectoryAuthMethod","description":"Active Directory authentication with domain"}},"additionalProperties":false,"type":"object","oneOf":[{"required":["basicAuth"],"title":"basicAuth"},{"required":["awsAccessKeyAuth"],"title":"awsAccessKeyAuth"},{"required":["activeDirectoryAuth"],"title":"activeDirectoryAuth"}]},"BasicAuthMethod":{"required":["username","password"],"properties":{"username":{"type":"string"},"password":{"type":"string"}},"additionalProperties":false,"type":"object"},"Certificates":{"properties":{"rootCAs":{"items":{"type":"string"},"type":"array"}},"additionalProperties":false,"type":"object"},"Destination":{"properties":{"ref":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/KReference"},"uri":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/URL"}},"additionalProperties":false,"type":"object"},"KReference":{"required":["kind","name","apiVersion"],"properties":{"kind":{"type":"string"},"namespace":{"type":"string"},"name":{"type":"string"},"apiVersion":{"type":"string"}},"additionalProperties":false,"type":"object"},"MetricsProvider":{"required":["type","name"],"properties":{"type":{"enum":["default"],"type":"string"},"name":{"type":"string"},"default":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/MetricsProviderConfigDefault"}},"additionalProperties":false,"type":"object","oneOf":[{"required":["default"],"title":"default"}]},"MetricsProviderConfigDefault":{"required":["bindAddress"],"properties":{"bindAddress":{"type":"string","default":"0.0.0.0:8082"},"auth":{"$ref":"#/definitions/AuthMethod","description":"Authentication configuration for this section"}},"additionalProperties":false,"type":"object"},"ObjectMeta":{"required":["name"],"properties":{"name":{"type":"string"},"labels":{"patternProperties":{".*":{"type":"string"}},"type":"object"}},"additionalProperties":false,"type":"object"},"Processor":{"required":["type","name"],"properties":{"type":{"enum":["openfaas","aws_event_bridge","knative"],"type":"string"},"name":{"type":"string"},"openfaas":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ProcessorConfigOpenFaaS"},"awsEventBridge":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ProcessorConfigEventBridge"},"knative":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ProcessorConfigKnative"}},"additionalProperties":false,"type":"object","oneOf":[{"required":["openfaas"],"title":"openfaas"},{"required":["awsEventBridge"],"title":"awsEventBridge"},{"required":["knative"],"title":"knative"}]},"ProcessorConfigEventBridge":{"required":["region","eventBus","ruleARN"],"properties":{"region":{"type":"string","default":"us-west-1"},"eventBus":{"type":"string","default":"default"},"ruleARN":{"type":"string","default":"arn:aws:events:us-west-1:1234567890:rule/vmware-event-router"},"auth":{"$ref":"#/definitions/AuthMethod","description":"Authentication configuration for this section"}},"additionalProperties":false,"type":"object","oneOf":[{"required":["auth"],"title":"auth"}]},"ProcessorConfigKnative":{"required":["insecureSSL","encoding"],"properties":{"destination":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/Destination","description":"Destination sink where to send events"},"insecureSSL":{"type":"boolean"},"encoding":{"enum":["binary","structured"],"type":"string","default":"structured"},"auth":{"$ref":"#/definitions/AuthMethod","description":"Authentication configuration for this section"}},"additionalProperties":false,"type":"object","oneOf":[{"required":["destination"],"title":"destination"}]},"ProcessorConfigOpenFaaS":{"required":["address","async"],"properties":{"address":{"type":"string","description":"OpenFaaS gateway address","default":"http://gateway.openfaas:8080"},"async":{"type":"boolean","description":"Use async function invocation mode"},"auth":{"$ref":"#/definitions/AuthMethod","description":"Authentication configuration for this section"}},"additionalProperties":false,"type":"object"},"Provider":{"required":["type","name"],"properties":{"type":{"enum":["vcenter","webhook","vcsim","horizon"],"type":"string"},"name":{"type":"string"},"vcenter":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ProviderConfigVCenter"},"vcsim":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ProviderConfigVCSIM"},"webhook":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ProviderConfigWebhook"},"horizon":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ProviderConfigHorizon"}},"additionalProperties":false,"type":"object","oneOf":[{"required":["vcenter"],"title":"vcenter"},{"required":["vcsim"],"title":"vcsim"},{"required":["webhook"],"title":"webhook"},{"required":["horizon"],"title":"horizon"}]},"ProviderConfigHorizon":{"required":["address","insecureSSL"],"properties":{"address":{"type":"string","default":"https://api.myhorizon.domain.local"},"insecureSSL":{"type":"boolean"},"auth":{"$ref":"#/definitions/AuthMethod","description":"Authentication configuration for this section"}},"additionalProperties":false,"type":"object","oneOf":[{"required":["auth"],"title":"auth"}]},"ProviderConfigVCSIM":{"required":["address","insecureSSL"],"properties":{"address":{"type":"string","default":"https://my-vcenter01.domain.local/sdk"},"insecureSSL":{"type":"boolean"},"auth":{"$ref":"#/definitions/AuthMethod","description":"Authentication configuration for this section"}},"additionalProperties":false,"type":"object","oneOf":[{"required":["auth"],"title":"auth"}]},"ProviderConfigVCenter":{"required":["address","insecureSSL","checkpoint"],"properties":{"address":{"type":"string","default":"https://my-vcenter01.domain.local/sdk"},"insecureSSL":{"type":"boolean"},"checkpoint":{"type":"boolean","description":"Enable checkpointing via checkpoint file for event recovery and replay purposes"},"checkpointDir":{"type":"string","description":"Directory where to persist checkpoints if enabled","default":"./checkpoints"},"auth":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/AuthMethod","description":"Authentication configuration for this section"}},"additionalProperties":false,"type":"object","oneOf":[{"required":["auth"],"title":"auth"}]},"ProviderConfigWebhook":{"required":["bindAddress","path"],"properties":{"bindAddress":{"type":"string","default":"0.0.0.0:8080"},"path":{"type":"string","default":"/webhook"},"auth":{"$ref":"#/definitions/AuthMethod","description":"Authentication configuration for this section"}},"additionalProperties":false,"type":"object"},"RouterConfig":{"required":["apiVersion","kind","metadata","eventProvider","eventProcessor","metricsProvider"],"properties":{"apiVersion":{"enum":["event-router.vmware.com/v1alpha1"],"type":"string"},"kind":{"enum":["RouterConfig"],"type":"string"},"metadata":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/ObjectMeta"},"eventProvider":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/Provider"},"eventProcessor":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/Processor"},"metricsProvider":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/MetricsProvider"},"certificates":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/Certificates"}},"additionalProperties":false,"type":"object"},"URL":{"required":["Scheme","Opaque","User","Host","Path","RawPath","ForceQuery","RawQuery","Fragment","RawFragment"],"properties":{"Scheme":{"type":"string"},"Opaque":{"type":"string"},"User":{"$schema":"http://json-schema.org/draft-04/schema#","$ref":"#/definitions/Userinfo"},"Host":{"type":"string"},"Path":{"type":"string"},"RawPath":{"type":"string"},"ForceQuery":{"type":"boolean"},"RawQuery":{"type":"string"},"Fragment":{"type":"string"},"RawFragment":{"type":"string"}},"additionalProperties":false,"type":"object"},"Userinfo":{"properties":{},"additionalProperties":false,"type":"object"}}} \ No newline at end of file From 012512ed0060dbec487a85e6b63cee2cc9651f42 Mon Sep 17 00:00:00 2001 From: Patrick Kremer Date: Thu, 14 Apr 2022 12:48:46 -0500 Subject: [PATCH 15/54] docs: Expand troubleshooting guide Closes #854 Signed-off-by: Patrick Kremer --- docs/kb/troubleshoot-appliance.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/kb/troubleshoot-appliance.md b/docs/kb/troubleshoot-appliance.md index 9eea24b3..17194da9 100644 --- a/docs/kb/troubleshoot-appliance.md +++ b/docs/kb/troubleshoot-appliance.md @@ -5,7 +5,7 @@ title: VMware Event Broker Appliance Troubleshooting description: Troubleshooting guide for general appliance issues permalink: /kb/troubleshoot-appliance cta: - title: Still having trouble? + title: Still having trouble? description: Please submit bug reports and feature requests by using our GitHub [Issues](https://github.com/vmware-samples/vcenter-event-broker-appliance/issues){:target="_blank"} page or Join us on slack [#vcenter-event-broker-appliance](https://vmwarecode.slack.com/archives/CQLT9B5AA){:target="_blank"} on vmwarecode.slack.com. --- # VMware Event Broker Appliance - Troubleshooting @@ -74,7 +74,10 @@ vmware-system vmware-event-router-5dd9c8f858-5c9mh 0/1 > **Note:** The status ```Completed``` of the container ```contour-certgen-v1.10.0-btmlp``` is expected after successful appliance deployment. -One of the first things to look for is whether a pod is in a crash state. In this case, the vmware-event-router pod is crashing. We need to look at the logs with this command: +One of the first things to look for is whether a pod is in a crash state. + +### Recovering from a crashing event router pod +In the above case, the vmware-event-router pod is crashing. We need to look at the logs with this command: ```bash kubectl -n vmware-system logs vmware-event-router-5dd9c8f858-5c9mh @@ -167,6 +170,17 @@ Here is the command output: We now see that the Event Router came online, connected to vCenter, and successfully received an event. +### Check for completed installation + +If the pods appear to be up without a crash status, check to make sure the installation completed. The file `/root/ran_customzation` gets created when installation completes successfully. If this file is missing, you can turn to the installation logs to find out why. +```bash +root@veba [ ~ ]# ls -al /root/ran_customization +-rw-r--r-- 1 root root 0 Oct 1 2021 /root/ran_customization +``` + +### Examine log files + +The appliance installation log file is found in `/var/log/bootstrap.log`. If enabled at install time, a debug log is available in `/var/log/bootstrap-debug.log`. The logs should point you toward the source of the issue. Don't hesitate to [reach out](#bottom) to the team if you need help. ## Changing the vCenter service account If you need to change the account the appliance uses to connect to vCenter, the procedure above can be used. @@ -292,4 +306,5 @@ Here is the command output: } } } -``` \ No newline at end of file +``` + \ No newline at end of file From e6ebd986c75f6e686e9dd2cbf21a4a62a42f3173 Mon Sep 17 00:00:00 2001 From: Patrick Kremer Date: Wed, 20 Apr 2022 14:29:04 -0500 Subject: [PATCH 16/54] feat: Port hostmaint alarms function to Knative Closes #856 Signed-off-by: Patrick Kremer --- .../kn-pcli-hostmaint-alarms/Dockerfile | 5 + .../kn-pcli-hostmaint-alarms/README.md | 175 ++++++++++++++++++ .../kn-pcli-hostmaint-alarms/function.yaml | 61 ++++++ .../kn-pcli-hostmaint-alarms/handler.ps1 | 129 +++++++++++++ .../hostmaint_secret.json | 6 + .../test/docker-test-env-variable | 1 + .../test/enter-maint-payload.json | 33 ++++ .../test/exit-maint-payload.json | 33 ++++ .../test/send-cloudevent-test.ps1 | 32 ++++ .../test/send-cloudevent-test.sh | 29 +++ 10 files changed, 504 insertions(+) create mode 100644 examples/knative/powercli/kn-pcli-hostmaint-alarms/Dockerfile create mode 100644 examples/knative/powercli/kn-pcli-hostmaint-alarms/README.md create mode 100644 examples/knative/powercli/kn-pcli-hostmaint-alarms/function.yaml create mode 100644 examples/knative/powercli/kn-pcli-hostmaint-alarms/handler.ps1 create mode 100644 examples/knative/powercli/kn-pcli-hostmaint-alarms/hostmaint_secret.json create mode 100644 examples/knative/powercli/kn-pcli-hostmaint-alarms/test/docker-test-env-variable create mode 100644 examples/knative/powercli/kn-pcli-hostmaint-alarms/test/enter-maint-payload.json create mode 100644 examples/knative/powercli/kn-pcli-hostmaint-alarms/test/exit-maint-payload.json create mode 100644 examples/knative/powercli/kn-pcli-hostmaint-alarms/test/send-cloudevent-test.ps1 create mode 100644 examples/knative/powercli/kn-pcli-hostmaint-alarms/test/send-cloudevent-test.sh diff --git a/examples/knative/powercli/kn-pcli-hostmaint-alarms/Dockerfile b/examples/knative/powercli/kn-pcli-hostmaint-alarms/Dockerfile new file mode 100644 index 00000000..49b2c980 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-hostmaint-alarms/Dockerfile @@ -0,0 +1,5 @@ +FROM us.gcr.io/daisy-284300/veba/ce-pcli-base:1.4 + +COPY handler.ps1 handler.ps1 + +CMD ["pwsh","./server.ps1"] diff --git a/examples/knative/powercli/kn-pcli-hostmaint-alarms/README.md b/examples/knative/powercli/kn-pcli-hostmaint-alarms/README.md new file mode 100644 index 00000000..7f696071 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-hostmaint-alarms/README.md @@ -0,0 +1,175 @@ +# kn-pcli-hostmaint-alarms +Example Knative PowerCLI function kn-pcli-hostmaint-alarms. This function will disable host alarm actions when a host is placed in maintenance mode, and enable host alarm actions when a host is removed from maintenance mode. + +# Step 1 - Build + +> **Note:** This step is only required if you made code changes to `handler.ps1` +> or `Dockerfile`. + +Create the container image locally to test your function logic. + +Mac/Linux +``` +# change the IMAGE name accordingly, example below for Docker +export IMAGE=/kn-pcli-hostmaint-alarms:1.0 +docker build -t ${IMAGE} . +``` + +Windows +``` +# change the IMAGE name accordingly, example below for Docker +$IMAGE="/kn-pcli-hostmaint-alarms:1.0" +docker build -t ${IMAGE} . +``` +# Step 2 - Test + +Verify the container image works by executing it locally. + +Change into the `test` directory +```console +cd test +``` + +Update the following variable names within the `docker-test-env-variable` file. +> Note - The sample variables are built for a vCenter function. You will need to replace them if you are authoring a function for a different purpose. +> Any changes to variables must also be updated in `hostmaint_secret.yaml` and `test/docker-test-env-variable` + +* `VCENTER_SERVER` - IP Address or FQDN of the vCenter Server to connect to +* `VCENTER_USERNAME` - vCenter account with permission to reconfigure distributed virtual switches +* `VCENTER_PASSWORD` - vCenter password associated with the username +* `VCENTER_CERTIFCATE_ACTION` - Set-PowerCLIConfiguration Action to configure when connection fails due to certificate error, default is Fail. (Possible values: Fail, Ignore or Warn) + +If you built a custom image in Step 1, comment out the default `IMAGE` command below - the `docker run` command will then use use the value previously stored in the `IMAGE` variable. Otherwise, use the default image as shown below. Start the container image by running the following commands: + +Mac/Linux +```console +export IMAGE=us.gcr.io/daisy-284300/veba/kn-pcli-hostmaint-alarms:1.0 +docker run -e FUNCTION_DEBUG=true -e PORT=8080 --env-file docker-test-env-variable -it --rm -p 8080:8080 ${IMAGE} +``` +Windows +```console +$IMAGE="us.gcr.io/daisy-284300/veba/kn-pcli-hostmaint-alarms:1.0" +docker run -e FUNCTION_DEBUG=true -e PORT=8080 --env-file docker-test-env-variable -it --rm -p 8080:8080 ${IMAGE} +``` + +--- +This function has two sample payload files: `enter-maint-payload.json` and `exit-maint-payload.json`. Locate the `Host` section of each file: +```json + "Host": { + "Name": "esx02.cl.vmweventbroker.io", + "Host": { + "Type": "HostSystem", + "Value": "REPLACE-ME" + } + }, +``` +Change `Value:` from `REPLACE-ME` to the ID of a host currently in your vCenter inventory. If you do not make this change, the function will still be invoked, but the configuration operation will fail because the host will not be found. The function output will also make more sense if you change `Name:` to match the name of your host. The function will still work without changing the name. + +One way to figure out a host's ID is with this PowerCLI command: +```powershell +(Get-VMHost "esx02.cl.vmweventbroker.io").id +HostSystem-host-58 +``` + +Based on the previous example host ID, the JSON should look like this: + +```json + "Host": { + "Name": "esx02.cl.vmweventbroker.io", + "Host": { + "Type": "HostSystem", + "Value": "host-58" + } + }, +``` + +**WARNING** - This function will reconfigure alarm actions on the host you specify. +--- + +In a separate terminal, run either `send-cloudevent-test.ps1` (PowerShell Script) or `send-cloudevent-test.sh` (Bash Script) to simulate a CloudEvent payload being sent to the local container image. When run with no arguments, the scripts will send the contents of `enter-maint-payload.json` as the payload and an event subject of `EnteredMaintenanceModeEvent`. + +This is an example of running a test for entering maintenance mode. +```console +> .\send-cloudevent-test.ps1 +Testing Function ... +See docker container console for output +``` +```console +# Output from docker container console +04/21/2022 13:52:51 - DEBUG: Event - EnteredMaintenanceModeEvent +04/21/2022 13:52:52 - Disabling alarm actions on host: esx02.cl.vmweventbroker.io +04/21/2022 13:52:52 - kn-pcli-hostmaint-alarms operation complete ... +``` + +To simulate an exit maintenance mode event, pass the JSON file name along with the exit event name . Example: `./send-cloudevent-test.ps1 ./exit-maint-payload.json ExitMaintenanceModeEvent`. + +```console +> ./send-cloudevent-test.ps1 ./exit-maint-payload.json ExitMaintenanceModeEvent +Testing Function ... +See docker container console for output +``` + +```console +# Output from docker container console +04/21/2022 14:01:31 - DEBUG: Event - ExitMaintenanceModeEvent +04/21/2022 14:01:31 - Enabling alarm actions on host: esx02.cl.vmweventbroker.io +04/21/2022 14:01:31 - kn-pcli-hostmaint-alarms operation complete ... +``` + +--- + +> Pro Tip - If you are rapidly iterating on the code and want to easily rebuild and launch the container, +> you can chain all of the commands together with ampersands. This will allow you to re-run +> the commands by simply pressing the `up` arrow and `Enter`. + +```console +cd .. && docker build -t ${IMAGE} . && cd test && docker run -e FUNCTION_DEBUG=true -e PORT=8080 --env-file docker-test-env-variable -it --rm -p 8080:8080 ${IMAGE} +``` +# Step 3 - Deploy + +> **Note:** The following steps assume a working Knative environment using the +`default` Rabbit `broker`. The Knative `service` and `trigger` will be installed in the +`vmware-functions` Kubernetes namespace, assuming that the `broker` is also available there. + +If you built a custom image, push it to an accessible registry such as Docker once you're done developing and testing your function logic. + +```console +docker push ${IMAGE} +``` + +Update the `hostmaint_secret.json` file with your vCenter Server credentials and configurations and then create the kubernetes secret which can then be accessed from within the function by using the environment variable named called `HOSTMAINT_SECRET`. + +```console +# create secret +kubectl -n vmware-functions create secret generic hostmaint-secret --from-file=HOSTMAINT_SECRET=hostmaint_secret.json + +# update label for secret to show up in VEBA UI +kubectl -n vmware-functions label secret hostmaint-secret app=veba-ui +``` + +Edit the `function.yaml` file with the name of the container image from Step 1 if you made any changes. If not, the default VMware container image will suffice. By default, the function deployment will filter on the `EnteredMaintenanceModeEvent` and `ExitMaintenanceModeEvent` vCenter Server events. If you wish to change this, update the `subject` field within `function.yaml` to the desired event type. + + +Deploy the function to the VMware Event Broker Appliance (VEBA). +```console +# deploy function +kubectl -n vmware-functions apply -f function.yaml +``` + +For testing purposes, the `function.yaml` contains the following annotations, which will ensure the Knative Service Pod will always run **exactly** one instance for debugging purposes. Functions deployed through through the VMware Event Broker Appliance UI defaults to scale to 0, which means the pods will only run when it is triggered by an vCenter Event. + +```yaml +annotations: + autoscaling.knative.dev/maxScale: "1" + autoscaling.knative.dev/minScale: "1" +``` + +# Step 4 - Undeploy + +```console +# undeploy function +kubectl -n vmware-functions delete -f function.yaml + +# delete secret +kubectl -n vmware-functions delete secret hostmaint-secret +``` diff --git a/examples/knative/powercli/kn-pcli-hostmaint-alarms/function.yaml b/examples/knative/powercli/kn-pcli-hostmaint-alarms/function.yaml new file mode 100644 index 00000000..5b946227 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-hostmaint-alarms/function.yaml @@ -0,0 +1,61 @@ +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: kn-pcli-hostmaint-alarms + labels: + app: veba-ui +spec: + template: + metadata: + annotations: + autoscaling.knative.dev/maxScale: "1" + autoscaling.knative.dev/minScale: "1" + spec: + containers: + - image: us.gcr.io/daisy-284300/veba/kn-pcli-hostmaint-alarms:1.0 + envFrom: + - secretRef: + name: hostmaint-secret + env: + - name: FUNCTION_DEBUG + value: "true" +--- +apiVersion: eventing.knative.dev/v1 +kind: Trigger +metadata: + name: veba-pcli-hostmaint-alarms-trigger-exit + labels: + app: veba-ui +spec: + broker: default + filter: + attributes: + type: com.vmware.event.router/event + # Replace this subject with the event you need to trigger on + # Then, edit send-cloudevent-test.ps1 and send-cloudevent-test.sh in the /test folder + subject: ExitMaintenanceModeEvent + subscriber: + ref: + apiVersion: serving.knative.dev/v1 + kind: Service + name: kn-pcli-hostmaint-alarms +--- +apiVersion: eventing.knative.dev/v1 +kind: Trigger +metadata: + name: veba-pcli-hostmaint-alarms-trigger-enter + labels: + app: veba-ui +spec: + broker: default + filter: + attributes: + type: com.vmware.event.router/event + # Replace this subject with the event you need to trigger on + # Then, edit send-cloudevent-test.ps1 and send-cloudevent-test.sh in the /test folder + subject: EnteredMaintenanceModeEvent + subscriber: + ref: + apiVersion: serving.knative.dev/v1 + kind: Service + name: kn-pcli-hostmaint-alarms \ No newline at end of file diff --git a/examples/knative/powercli/kn-pcli-hostmaint-alarms/handler.ps1 b/examples/knative/powercli/kn-pcli-hostmaint-alarms/handler.ps1 new file mode 100644 index 00000000..96b6a43e --- /dev/null +++ b/examples/knative/powercli/kn-pcli-hostmaint-alarms/handler.ps1 @@ -0,0 +1,129 @@ +Function Process-Init { + [CmdletBinding()] + param() + Write-Host "$(Get-Date) - Processing Init`n" + + try { + $jsonSecrets = ${env:HOSTMAINT_SECRET} | ConvertFrom-Json + } + catch { + throw "`nK8s secret `$env:HOSTMAINT_SECRET does not look to be defined" + } + + # Extract all tag secrets for ease of use in function + $VCENTER_SERVER = ${jsonSecrets}.VCENTER_SERVER + $VCENTER_USERNAME = ${jsonSecrets}.VCENTER_USERNAME + $VCENTER_PASSWORD = ${jsonSecrets}.VCENTER_PASSWORD + $VCENTER_CERTIFICATE_ACTION = ${jsonSecrets}.VCENTER_CERTIFICATE_ACTION + + # Configure TLS 1.2/1.3 support as this is required for latest vSphere release + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 -bor [System.Net.SecurityProtocolType]::Tls13 + + Write-Host "$(Get-Date) - Configuring PowerCLI Configuration Settings`n" + Set-PowerCLIConfiguration -InvalidCertificateAction:${VCENTER_CERTIFICATE_ACTION} -ParticipateInCeip:$true -Confirm:$false + + Write-Host "$(Get-Date) - Connecting to vCenter Server $VCENTER_SERVER`n" + + try { + Connect-VIServer -Server $VCENTER_SERVER -User $VCENTER_USERNAME -Password $VCENTER_PASSWORD + } + catch { + Write-Error "$(Get-Date) - ERROR: Failed to connect to vCenter Server" + throw $_ + } + + Write-Host "$(Get-Date) - Successfully connected to $VCENTER_SERVER`n" + + Write-Host "$(Get-Date) - Init Processing Completed`n" +} + +Function Process-Shutdown { + [CmdletBinding()] + param() + Write-Host "$(Get-Date) - Processing Shutdown`n" + + Write-Host "$(Get-Date) - Disconnecting from vCenter Server`n" + + try { + Disconnect-VIServer * -Confirm:$false + } + catch { + Write-Error "$(Get-Date) - Error: Failed to Disconnect from vCenter Server" + } + + Write-Host "$(Get-Date) - Shutdown Processing Completed`n" +} + +Function Process-Handler { + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=$true)][CloudNative.CloudEvents.CloudEvent]$CloudEvent + ) + + # Decode CloudEvent + try { + $cloudEventData = $cloudEvent | Read-CloudEventJsonData -Depth 10 + } + catch { + throw "`nPayload must be JSON encoded" + } + + try { + $jsonSecrets = ${env:HOSTMAINT_SECRET} | ConvertFrom-Json + } + catch { + throw "`nK8s secret `$env:HOSTMAINT_SECRET does not look to be defined" + } + + if (${env:FUNCTION_DEBUG} -eq "true") { + Write-Host "$(Get-Date) - DEBUG: Event - $($cloudEvent.subject)" + } + $hostName = $cloudEventData.Host.Name + $moRef = New-Object VMware.Vim.ManagedObjectReference + $moRef.Type = $cloudEventData.Host.Host.Type + $moRef.Value = $cloudEventData.Host.Host.Value + + try { + $hostMoRef = Get-View $moRef + } + catch { + Write-Host "$(Get-Date) - ERROR: Could not retrieve host view by moRef" + throw $_ + } + + try { + $alarmManager = Get-View AlarmManager + } + catch { + Write-Host "$(Get-Date) - ERROR: Could not retrieve AlarmManager object" + throw $_ + } + + if ($cloudEvent.subject -eq "EnteredMaintenanceModeEvent") { + # Disable alarm actions on the host + Write-Host "$(Get-Date) - Disabling alarm actions on host: $hostName" + try { + $alarmManager.EnableAlarmActions($hostMoRef.MoRef, $false) + } + catch { + Write-Host "$(Get-Date) - ERROR: Could not disable alarm actions" + throw $_ + } + } + + if ($cloudEvent.subject -eq "ExitMaintenanceModeEvent") { + # Enable alarm actions on the host + Write-Host "$(Get-Date) - Enabling alarm actions on host: $hostName" + try { + $alarmManager.EnableAlarmActions($hostMoRef.MoRef, $true) + } + catch { + Write-Host "$(Get-Date) - ERROR: Could not enable alarm actions" + throw $_ + } + } + + Write-Host "$(Get-Date) - kn-pcli-hostmaint-alarms operation complete ...`n" + + Write-Host "$(Get-Date) - Handler Processing Completed ...`n" +} diff --git a/examples/knative/powercli/kn-pcli-hostmaint-alarms/hostmaint_secret.json b/examples/knative/powercli/kn-pcli-hostmaint-alarms/hostmaint_secret.json new file mode 100644 index 00000000..b45d68aa --- /dev/null +++ b/examples/knative/powercli/kn-pcli-hostmaint-alarms/hostmaint_secret.json @@ -0,0 +1,6 @@ +{ + "VCENTER_SERVER": "FILL-ME-IN", + "VCENTER_USERNAME" : "FILL-ME-IN", + "VCENTER_PASSWORD" : "FILL-ME-IN", + "VCENTER_CERTIFICATE_ACTION" : "Ignore" +} diff --git a/examples/knative/powercli/kn-pcli-hostmaint-alarms/test/docker-test-env-variable b/examples/knative/powercli/kn-pcli-hostmaint-alarms/test/docker-test-env-variable new file mode 100644 index 00000000..d181a22f --- /dev/null +++ b/examples/knative/powercli/kn-pcli-hostmaint-alarms/test/docker-test-env-variable @@ -0,0 +1 @@ +HOSTMAINT_SECRET={"VCENTER_SERVER":"FILL-ME-IN","VCENTER_USERNAME":"FILL-ME-IN","VCENTER_PASSWORD":"FILL-ME-IN","VCENTER_CERTIFICATE_ACTION":"Ignore"} \ No newline at end of file diff --git a/examples/knative/powercli/kn-pcli-hostmaint-alarms/test/enter-maint-payload.json b/examples/knative/powercli/kn-pcli-hostmaint-alarms/test/enter-maint-payload.json new file mode 100644 index 00000000..1c9152a7 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-hostmaint-alarms/test/enter-maint-payload.json @@ -0,0 +1,33 @@ +{ + "Key": 910093, + "ChainId": 910066, + "CreatedTime": "2022-04-20T15:47:32.365Z", + "UserName": "VSPHERE.LOCAL\\Administrator", + "Datacenter": { + "Name": "HomeLab", + "Datacenter": { + "Type": "Datacenter", + "Value": "datacenter-47" + } + }, + "ComputeResource": { + "Name": "CL1", + "ComputeResource": { + "Type": "ClusterComputeResource", + "Value": "domain-c52" + } + }, + "Host": { + "Name": "esx02.cl.vmweventbroker.io", + "Host": { + "Type": "HostSystem", + "Value": "REPLACE-ME" + } + }, + "Vm": null, + "Ds": null, + "Net": null, + "Dvs": null, + "FullFormattedMessage": "Host esx02.cl.vmweventbroker.io in HomeLab has entered maintenance mode", + "ChangeTag": "" + } \ No newline at end of file diff --git a/examples/knative/powercli/kn-pcli-hostmaint-alarms/test/exit-maint-payload.json b/examples/knative/powercli/kn-pcli-hostmaint-alarms/test/exit-maint-payload.json new file mode 100644 index 00000000..6801c44e --- /dev/null +++ b/examples/knative/powercli/kn-pcli-hostmaint-alarms/test/exit-maint-payload.json @@ -0,0 +1,33 @@ +{ + "Key": 910100, + "ChainId": 910098, + "CreatedTime": "2022-04-20T15:48:39.140999Z", + "UserName": "VSPHERE.LOCAL\\Administrator", + "Datacenter": { + "Name": "HomeLab", + "Datacenter": { + "Type": "Datacenter", + "Value": "datacenter-47" + } + }, + "ComputeResource": { + "Name": "CL1", + "ComputeResource": { + "Type": "ClusterComputeResource", + "Value": "domain-c52" + } + }, + "Host": { + "Name": "esx02.cl.vmweventbroker.io", + "Host": { + "Type": "HostSystem", + "Value": "REPLACE-ME" + } + }, + "Vm": null, + "Ds": null, + "Net": null, + "Dvs": null, + "FullFormattedMessage": "Host esx02.cl.vmweventbroker.io in HomeLab has exited maintenance mode", + "ChangeTag": "" + } \ No newline at end of file diff --git a/examples/knative/powercli/kn-pcli-hostmaint-alarms/test/send-cloudevent-test.ps1 b/examples/knative/powercli/kn-pcli-hostmaint-alarms/test/send-cloudevent-test.ps1 new file mode 100644 index 00000000..2b594ef5 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-hostmaint-alarms/test/send-cloudevent-test.ps1 @@ -0,0 +1,32 @@ +# The ce-subject value should match the event router subject in function.yaml +$subject = "EnteredMaintenanceModeEvent" +$payloadPath = "./enter-maint-payload.json" + +if ( $args.Count -gt 0 ) { + if ( Test-Path $args[0] ) { + $payloadPath = $args[0] + } + else { + Write-Host "$(Get-Date) - ERROR: Invalid path"$args[0]"`n" + exit + } + + if ( $args.Count -gt 1 ) { + $subject = $args[1] + } +} + +$headers = @{ + "Content-Type" = "application/json"; + "ce-specversion" = "1.0"; + "ce-id" = "id-123"; + "ce-source" = "source-123"; + "ce-type" = "com.vmware.event.router/event"; + "ce-subject" = $($subject); +} +$body = Get-Content -Raw -Path $payloadPath + +Write-Host "Testing Function ..." +Invoke-WebRequest -Uri http://localhost:8080 -Method POST -Headers $headers -Body $body + +Write-host "See docker container console for output" \ No newline at end of file diff --git a/examples/knative/powercli/kn-pcli-hostmaint-alarms/test/send-cloudevent-test.sh b/examples/knative/powercli/kn-pcli-hostmaint-alarms/test/send-cloudevent-test.sh new file mode 100644 index 00000000..d0944fe8 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-hostmaint-alarms/test/send-cloudevent-test.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# The ce-subject value should match the event router subject in function.yaml +echo "Testing Function ..." +PAYLOAD_PATH="enter-maint-payload.json" +SUBJECT="EnteredMaintenanceModeEvent" + +if [ $# -gt 0 ]; then + if test -f "$1"; then + PAYLOAD_PATH=$1 + else + echo "$1 not found" + exit 1 + fi + + if [ $# -gt 1 ]; then + SUBJECT=$2 + fi +fi +curl -d@$PAYLOAD_PATH \ + -H "Content-Type: application/json" \ + -H 'ce-specversion: 1.0' \ + -H 'ce-id: d70079f9-fddd-4b7f-aa76-1193f28b0611' \ + -H 'ce-source: https://vcenter.local/sdk' \ + -H 'ce-type: com.vmware.event.router/event' \ + -H 'ce-subject: '$SUBJECT \ + -X POST localhost:8080 + +echo "See docker container console for output" From 98020a94adf3694603dcf901d999fde4b6b5a1cd Mon Sep 17 00:00:00 2001 From: Patrick Kremer Date: Thu, 21 Apr 2022 09:42:19 -0500 Subject: [PATCH 17/54] feat: Add optional event subject arg to testing scripts Closes #858 Signed-off-by: Patrick Kremer --- .../powercli/kn-pcli-template/README.md | 6 ++++- .../test/send-cloudevent-test.ps1 | 24 ++++++++++++------- .../test/send-cloudevent-test.sh | 8 ++++++- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/examples/knative/powercli/kn-pcli-template/README.md b/examples/knative/powercli/kn-pcli-template/README.md index cefb6ed0..85e4a4dd 100644 --- a/examples/knative/powercli/kn-pcli-template/README.md +++ b/examples/knative/powercli/kn-pcli-template/README.md @@ -90,7 +90,11 @@ Next, locate the `ConfigSpec` section of the JSON file. Change the `MaxMTU` prop **WARNING** - This function will reconfigure your distributed virtual switch - if your MTU is not set to 1500, it will be reset by this function. --- -In a separate terminal, run either `send-cloudevent-test.ps1` (PowerShell Script) or `send-cloudevent-test.sh` (Bash Script) to simulate a CloudEvent payload being sent to the local container image. When run with no arguments, the scripts will send the contents of `test-payload.json` as the payload. If you pass the scripts a different filename as an argument, they will send the contents of the specified file instead. Example: `send-cloudevent-test.ps1 test-payload2.json`. This testing technique is useful when writing complex functions with varying payloads. +In a separate terminal, run either `send-cloudevent-test.ps1` (PowerShell Script) or `send-cloudevent-test.sh` (Bash Script) to simulate a CloudEvent payload being sent to the local container image. When run with no arguments, the scripts will send the contents of `test-payload.json` as the payload. If you pass the scripts a different filename as an argument, they will send the contents of the specified file instead. Example: `send-cloudevent-test.ps1 test-payload2.json`. + +You can also send a custom event subject as an argument. The default event is `DvsReconfiguredEvent`. Example: `send-cloudevent-test.ps1 test-payload2.json EnteredMaintenanceModeEvent` will send `test-payload2.json` with a subject of `EnteredMaintenanceModeEvent`. + +These testing technique are useful when writing complex functions with varying payloads, or when writing functions with separate triggers on different events. ```console Testing Function ... diff --git a/examples/knative/powercli/kn-pcli-template/test/send-cloudevent-test.ps1 b/examples/knative/powercli/kn-pcli-template/test/send-cloudevent-test.ps1 index 4970dbee..9d2ea0f4 100644 --- a/examples/knative/powercli/kn-pcli-template/test/send-cloudevent-test.ps1 +++ b/examples/knative/powercli/kn-pcli-template/test/send-cloudevent-test.ps1 @@ -1,14 +1,7 @@ # The ce-subject value should match the event router subject in function.yaml -$headers = @{ - "Content-Type" = "application/json"; - "ce-specversion" = "1.0"; - "ce-id" = "id-123"; - "ce-source" = "source-123"; - "ce-type" = "com.vmware.event.router/event"; - "ce-subject" = "DvsReconfiguredEvent"; -} - +$subject = "DvsReconfiguredEvent" $payloadPath = "./test-payload.json" + if ( $args.Count -gt 0 ) { if ( Test-Path $args[0] ) { $payloadPath = $args[0] @@ -17,6 +10,19 @@ if ( $args.Count -gt 0 ) { Write-Host "$(Get-Date) - ERROR: Invalid path"$args[0]"`n" exit } + + if ( $args.Count -gt 1 ) { + $subject = $args[1] + } +} + +$headers = @{ + "Content-Type" = "application/json"; + "ce-specversion" = "1.0"; + "ce-id" = "id-123"; + "ce-source" = "source-123"; + "ce-type" = "com.vmware.event.router/event"; + "ce-subject" = $($subject); } $body = Get-Content -Raw -Path $payloadPath diff --git a/examples/knative/powercli/kn-pcli-template/test/send-cloudevent-test.sh b/examples/knative/powercli/kn-pcli-template/test/send-cloudevent-test.sh index 3c7f114f..b2652b4b 100644 --- a/examples/knative/powercli/kn-pcli-template/test/send-cloudevent-test.sh +++ b/examples/knative/powercli/kn-pcli-template/test/send-cloudevent-test.sh @@ -3,6 +3,8 @@ # The ce-subject value should match the event router subject in function.yaml echo "Testing Function ..." PAYLOAD_PATH="test-payload.json" +SUBJECT="DvsReconfiguredEvent" + if [ $# -gt 0 ]; then if test -f "$1"; then PAYLOAD_PATH=$1 @@ -10,6 +12,10 @@ if [ $# -gt 0 ]; then echo "$1 not found" exit 1 fi + + if [ $# -gt 1 ]; then + SUBJECT=$2 + fi fi curl -d@$PAYLOAD_PATH \ -H "Content-Type: application/json" \ @@ -17,7 +23,7 @@ curl -d@$PAYLOAD_PATH \ -H 'ce-id: d70079f9-fddd-4b7f-aa76-1193f28b0611' \ -H 'ce-source: https://vcenter.local/sdk' \ -H 'ce-type: com.vmware.event.router/event' \ - -H 'ce-subject: DvsReconfiguredEvent' \ + -H 'ce-subject: '$SUBJECT \ -X POST localhost:8080 echo "See docker container console for output" From 8b29721843b6db8e002808a66b2b964fec36e853 Mon Sep 17 00:00:00 2001 From: Patrick Kremer Date: Thu, 21 Apr 2022 14:30:13 -0500 Subject: [PATCH 18/54] docs: Add kn-pcli-hostmaint-alarms to website Closes #862 Signed-off-by: Patrick Kremer --- docs/site/examples.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/site/examples.md b/docs/site/examples.md index 530689ba..f12fa39c 100644 --- a/docs/site/examples.md +++ b/docs/site/examples.md @@ -33,6 +33,15 @@ examples: - language: powershell url: "/tree/master/examples/knative/powershell/kn-ps-email" + - title: Host Maintenance Alarm Actions + usecases: + - item: notification + id: kn-pcli-hostmaint-alarms + description: Automatically disables alarm actions when a host enters maintenance mode, enables alarm action when a host exits maintenance mode. + links: + - language: powercli + url: "/tree/master/examples/knative/powercli/kn-pcli-hostmaint-alarms" + - title: Slack Notification usecases: - item: integration From 3359684f2a876267ec22d06a551682d04ab55a5f Mon Sep 17 00:00:00 2001 From: William Lam Date: Sat, 23 Apr 2022 07:00:20 -0700 Subject: [PATCH 19/54] feat: Add Zapier webhook example Closes: #864 Signed-off-by: William Lam --- docs/site/examples.md | 9 ++ .../powershell/kn-ps-zapier/Dockerfile | 5 + .../knative/powershell/kn-ps-zapier/README.md | 101 ++++++++++++++++++ .../powershell/kn-ps-zapier/function.yaml | 39 +++++++ .../powershell/kn-ps-zapier/handler.ps1 | 75 +++++++++++++ .../test/docker-test-env-variable | 1 + .../test/send-cloudevent-test.ps1 | 16 +++ .../kn-ps-zapier/test/send-cloudevent-test.sh | 13 +++ .../kn-ps-zapier/test/test-payload.json | 44 ++++++++ .../kn-ps-zapier/zapier_secret.json | 3 + 10 files changed, 306 insertions(+) create mode 100644 examples/knative/powershell/kn-ps-zapier/Dockerfile create mode 100644 examples/knative/powershell/kn-ps-zapier/README.md create mode 100644 examples/knative/powershell/kn-ps-zapier/function.yaml create mode 100644 examples/knative/powershell/kn-ps-zapier/handler.ps1 create mode 100644 examples/knative/powershell/kn-ps-zapier/test/docker-test-env-variable create mode 100644 examples/knative/powershell/kn-ps-zapier/test/send-cloudevent-test.ps1 create mode 100755 examples/knative/powershell/kn-ps-zapier/test/send-cloudevent-test.sh create mode 100644 examples/knative/powershell/kn-ps-zapier/test/test-payload.json create mode 100644 examples/knative/powershell/kn-ps-zapier/zapier_secret.json diff --git a/docs/site/examples.md b/docs/site/examples.md index f12fa39c..1c92d155 100644 --- a/docs/site/examples.md +++ b/docs/site/examples.md @@ -229,6 +229,15 @@ examples: links: - language: python url: "/tree/master/examples/knative/python/kn-py-vro" + + - title: Zapier workflow integration + usecases: + - item: integration + id: kn-ps-zapier-function + description: Trigger a Zapier workflow, passing select CloudEvent data to a Zapier webhook + links: + - language: powershell + url: "/tree/master/examples/knative/powershell/kn-ps-zapier" --- A complete and updated list of ready to use functions curated by the VMware Event Broker community is listed below. diff --git a/examples/knative/powershell/kn-ps-zapier/Dockerfile b/examples/knative/powershell/kn-ps-zapier/Dockerfile new file mode 100644 index 00000000..1b16e1ef --- /dev/null +++ b/examples/knative/powershell/kn-ps-zapier/Dockerfile @@ -0,0 +1,5 @@ +FROM us.gcr.io/daisy-284300/veba/ce-ps-base:1.4 + +COPY handler.ps1 handler.ps1 + +CMD ["pwsh","./server.ps1"] diff --git a/examples/knative/powershell/kn-ps-zapier/README.md b/examples/knative/powershell/kn-ps-zapier/README.md new file mode 100644 index 00000000..2ca1916d --- /dev/null +++ b/examples/knative/powershell/kn-ps-zapier/README.md @@ -0,0 +1,101 @@ +# kn-ps-zapier +Example Knative PowerShell function for sending to a [Zapier webhook](https://zapier.com/page/webhooks/) when a failed vCenter Server login has been detected. + +# Step 1 - Build + +Create the container image locally to test your function logic. + +``` +export TAG= +docker build -t /kn-ps-zapier:${TAG} . +``` + +# Step 2 - Test + +Verify the container image works by executing it locally. + +Change into the `test` directory +```console +cd test +``` + +Update the following variable names within the `docker-test-env-variable` file + +* ZAPIER_WEBHOOK_URL - Zapier webhook URL + +Start the container image by running the following command: + +```console +docker run -e FUNCTION_DEBUG=true -e PORT=8080 --env-file docker-test-env-variable -it --rm -p 8080:8080 /kn-ps-zapier:${TAG} +``` + +In a separate terminal, run either `send-cloudevent-test.ps1` (PowerShell Script) or `send-cloudevent-test.sh` (Bash Script) to simulate a CloudEvent payload being sent to the local container image + +```console +Testing Function ... +See docker container console for output + +# Output from docker container console +Detected change to subject-123 ... +04/22/2022 22:52:00 - PowerShell HTTP server start listening on 'http://*:8080/' +04/22/2022 22:52:00 - Processing Init + +04/22/2022 22:52:00 - Init Processing Completed + +04/22/2022 22:52:00 - Starting HTTP CloudEvent listener +04/22/2022 22:52:08 - Sending Webhook payload to Zapier ... +04/22/2022 22:52:09 - Successfully sent Webhook ... +``` + +# Step 3 - Deploy + +> **Note:** The following steps assume a working Knative environment using the +`default` Rabbit `broker`. The Knative `service` and `trigger` will be installed in the +`vmware-functions` Kubernetes namespace, assuming that the `broker` is also available there. + +Push your container image to an accessible registry such as Docker once you're done developing and testing your function logic. + +```console +docker push /kn-ps-zapier:${TAG} +``` + +Update the `zapier_secret.json` file with your Zapier webhook configurations and then create the kubernetes secret which can then be accessed from within the function by using the environment variable named called `ZAPIER_SECRET`. + +```console +# create secret + +kubectl -n vmware-functions create secret generic zapier-secret --from-file=ZAPIER_SECRET=zapier_secret.json + +# update label for secret to show up in VEBA UI +kubectl -n vmware-functions label secret zapier-secret app=veba-ui +``` + +Edit the `function.yaml` file with the name of the container image from Step 1 if you made any changes. If not, the default VMware container image will suffice. By default, the function deployment will filter on the `com.vmware.sso.LoginFailure` vCenter Server Event. If you wish to change this, update the `subject` field within `function.yaml` to the desired event type. + + +Deploy the function to the VMware Event Broker Appliance (VEBA). + +```console +# deploy function + +kubectl -n vmware-functions apply -f function.yaml +``` + +For testing purposes, the `function.yaml` contains the following annotations, which will ensure the Knative Service Pod will always run **exactly** one instance for debugging purposes. Functions deployed through through the VMware Event Broker Appliance UI defaults to scale to 0, which means the pods will only run when it is triggered by an vCenter Event. + +```yaml +annotations: + autoscaling.knative.dev/maxScale: "1" + autoscaling.knative.dev/minScale: "1" +``` + +# Step 4 - Undeploy + +```console +# undeploy function + +kubectl -n vmware-functions delete -f function.yaml + +# delete secret +kubectl -n vmware-functions delete secret zapier-secret +``` \ No newline at end of file diff --git a/examples/knative/powershell/kn-ps-zapier/function.yaml b/examples/knative/powershell/kn-ps-zapier/function.yaml new file mode 100644 index 00000000..fb162832 --- /dev/null +++ b/examples/knative/powershell/kn-ps-zapier/function.yaml @@ -0,0 +1,39 @@ +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: kn-ps-zapier + labels: + app: veba-ui +spec: + template: + metadata: + annotations: + autoscaling.knative.dev/maxScale: "1" + autoscaling.knative.dev/minScale: "1" + spec: + containers: + - image: us.gcr.io/daisy-284300/veba/kn-ps-zapier:1.0 + envFrom: + - secretRef: + name: zapier-secret + env: + - name: FUNCTION_DEBUG + value: "false" +--- +apiVersion: eventing.knative.dev/v1 +kind: Trigger +metadata: + name: veba-ps-zapier-trigger + labels: + app: veba-ui +spec: + broker: default + filter: + attributes: + type: com.vmware.event.router/eventex + subject: com.vmware.sso.LoginFailure + subscriber: + ref: + apiVersion: serving.knative.dev/v1 + kind: Service + name: kn-ps-zapier diff --git a/examples/knative/powershell/kn-ps-zapier/handler.ps1 b/examples/knative/powershell/kn-ps-zapier/handler.ps1 new file mode 100644 index 00000000..0441ea9f --- /dev/null +++ b/examples/knative/powershell/kn-ps-zapier/handler.ps1 @@ -0,0 +1,75 @@ +Function Process-Init { + [CmdletBinding()] + param() + Write-Host "$(Get-Date) - Processing Init`n" + + Write-Host "$(Get-Date) - Init Processing Completed`n" +} + +Function Process-Shutdown { + [CmdletBinding()] + param() + Write-Host "$(Get-Date) - Processing Shutdown`n" + + Write-Host "$(Get-Date) - Shutdown Processing Completed`n" +} + +Function Process-Handler { + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=$true)][CloudNative.CloudEvents.CloudEvent]$CloudEvent + ) + + # Decode CloudEvent + try { + $cloudEventData = $cloudEvent | Read-CloudEventJsonData -Depth 10 + } catch { + throw "`nPayload must be JSON encoded" + } + + try { + $jsonSecrets = ${env:ZAPIER_SECRET} | ConvertFrom-Json + } catch { + throw "`nK8s secrets `$env:ZAPIER_SECRET does not look to be defined" + } + + if(${env:FUNCTION_DEBUG} -eq "true") { + Write-Host "$(Get-Date) - DEBUG: K8s Secrets:`n${env:ZAPIER_SECRET}`n" + + Write-Host "$(Get-Date) - DEBUG: CloudEventData`n $(${cloudEventData} | Out-String)`n" + } + + # Arguments is returned as an array of Key/Value objects + $arguments = ${cloudEventData}.Arguments + foreach ($argument in $arguments) { + if($argument.Key -eq "userIp") { + $userIp = $argument.Value + break + } + } + + # Construct Zapier payload + $payload = @{ + Username = $cloudEventData.UserName + UserIP = $userIp + TimeStamp = $cloudEventData.CreatedTime + } + + # Convert Zapier payload into JSON + $body = $payload | ConvertTo-Json -Depth 5 + + if(${env:FUNCTION_DEBUG} -eq "true") { + Write-Host "$(Get-Date) - DEBUG: `"$body`"" + } + + Write-Host "$(Get-Date) - Sending Webhook payload to Zapier ..." + $ProgressPreference = "SilentlyContinue" + + try { + Invoke-WebRequest -Uri $(${jsonSecrets}.ZAPIER_WEBHOOK_URL) -Method POST -ContentType "application/json" -Body $body + } catch { + throw "$(Get-Date) - Failed to send Zapier Message: $($_)" + } + + Write-Host "$(Get-Date) - Successfully sent Webhook ..." +} diff --git a/examples/knative/powershell/kn-ps-zapier/test/docker-test-env-variable b/examples/knative/powershell/kn-ps-zapier/test/docker-test-env-variable new file mode 100644 index 00000000..22adee18 --- /dev/null +++ b/examples/knative/powershell/kn-ps-zapier/test/docker-test-env-variable @@ -0,0 +1 @@ +ZAPIER_SECRET={"ZAPIER_WEBHOOK_URL":"YOUR_WEBHOOK_URL"} \ No newline at end of file diff --git a/examples/knative/powershell/kn-ps-zapier/test/send-cloudevent-test.ps1 b/examples/knative/powershell/kn-ps-zapier/test/send-cloudevent-test.ps1 new file mode 100644 index 00000000..5e717cb6 --- /dev/null +++ b/examples/knative/powershell/kn-ps-zapier/test/send-cloudevent-test.ps1 @@ -0,0 +1,16 @@ + +$headers = @{ + "Content-Type" = "application/json"; + "ce-specversion" = "1.0"; + "ce-id" = "d70079f9-fddd-4b7f-aa76-1193f28b0611"; + "ce-source" = "https://vcenter.local/sdk"; + "ce-type" = "com.vmware.event.router/eventex"; + "ce-subject" = "com.vmware.sso.LoginFailure"; +} + +$body = Get-Content -Raw -Path "./test-payload.json" + +Write-Host "Testing Function ..." +Invoke-WebRequest -Uri http://localhost:8080 -Method POST -Headers $headers -Body $body + +Write-host "See docker container console for output" \ No newline at end of file diff --git a/examples/knative/powershell/kn-ps-zapier/test/send-cloudevent-test.sh b/examples/knative/powershell/kn-ps-zapier/test/send-cloudevent-test.sh new file mode 100755 index 00000000..22436474 --- /dev/null +++ b/examples/knative/powershell/kn-ps-zapier/test/send-cloudevent-test.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +echo "Testing Function ..." +curl -d@test-payload.json \ + -H "Content-Type: application/json" \ + -H 'ce-specversion: 1.0' \ + -H 'ce-id: d70079f9-fddd-4b7f-aa76-1193f28b0611' \ + -H 'ce-source: https://vcenter.local/sdk' \ + -H 'ce-type: com.vmware.event.router/eventex' \ + -H 'ce-subject: com.vmware.sso.LoginFailure' \ + -X POST localhost:8080 + +echo "See docker container console for output" diff --git a/examples/knative/powershell/kn-ps-zapier/test/test-payload.json b/examples/knative/powershell/kn-ps-zapier/test/test-payload.json new file mode 100644 index 00000000..4a460bb0 --- /dev/null +++ b/examples/knative/powershell/kn-ps-zapier/test/test-payload.json @@ -0,0 +1,44 @@ +{ + "Key": 9766460, + "ChainId": 9766460, + "CreatedTime": "2022-04-22T21:54:38.628999Z", + "UserName": "hacker@vsphere.local", + "Datacenter": null, + "ComputeResource": null, + "Host": null, + "Vm": null, + "Ds": null, + "Net": null, + "Dvs": null, + "FullFormattedMessage": "Failed login hacker@vsphere.local from 10.0.1.337 at 04/22/2022 21:53:06 GMT in SSO", + "ChangeTag": "", + "EventTypeId": "com.vmware.sso.LoginFailure", + "Severity": "info", + "Message": "", + "Arguments": [ + { + "Key": "userName", + "Value": "hacker@vsphere.local" + }, + { + "Key": "description", + "Value": "User hacker@vsphere.local@10.0.1.337 failed to log in with response code 401" + }, + { + "Key": "userIp", + "Value": "10.0.1.337" + }, + { + "Key": "timestamp", + "Value": "04/22/2022 21:53:06 GMT" + }, + { + "Key": "_sourcehost_", + "Value": "vcsa.primp-industries.local" + } + ], + "ObjectId": "", + "ObjectType": "", + "ObjectName": "", + "Fault": null + } \ No newline at end of file diff --git a/examples/knative/powershell/kn-ps-zapier/zapier_secret.json b/examples/knative/powershell/kn-ps-zapier/zapier_secret.json new file mode 100644 index 00000000..c301262f --- /dev/null +++ b/examples/knative/powershell/kn-ps-zapier/zapier_secret.json @@ -0,0 +1,3 @@ +{ + "ZAPIER_WEBHOOK_URL": "YOUR_WEBHOOK_URL" +} \ No newline at end of file From 4f7dc040dddfd06c6f4165ce1e3d715feb927c2b Mon Sep 17 00:00:00 2001 From: Patrick Kremer Date: Tue, 26 Apr 2022 09:11:05 -0500 Subject: [PATCH 20/54] docs: Update kn-pcli-template Closes #871 Signed-off-by: Patrick Kremer --- examples/knative/powercli/kn-pcli-template/README.md | 1 + examples/knative/powercli/kn-pcli-template/function.yaml | 2 +- examples/knative/powercli/kn-pcli-template/function_secret.json | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/knative/powercli/kn-pcli-template/README.md b/examples/knative/powercli/kn-pcli-template/README.md index 85e4a4dd..808b5a93 100644 --- a/examples/knative/powercli/kn-pcli-template/README.md +++ b/examples/knative/powercli/kn-pcli-template/README.md @@ -7,6 +7,7 @@ Delete this section before publishing your new function - handler.ps1 - README.md - function_secret.json (rename the file) + - function.yaml - Obtain a new payload file - The example `test/test-payload.json` is a sample event payload file for event type `DvsReconfiguredEvent`. This needs to be replaced with the payload for your specific event. This is easily obtained using the built-in Sockeye service. Browse to the `/events` endpoint of your VEBA deployment and cause your event to trigger. You can then can easily copy and paste the payload from the output in Sockeye. # kn-pcli-#REPLACE-FN-NAME# diff --git a/examples/knative/powercli/kn-pcli-template/function.yaml b/examples/knative/powercli/kn-pcli-template/function.yaml index cb940697..1291791c 100644 --- a/examples/knative/powercli/kn-pcli-template/function.yaml +++ b/examples/knative/powercli/kn-pcli-template/function.yaml @@ -38,4 +38,4 @@ spec: ref: apiVersion: serving.knative.dev/v1 kind: Service - name: kn-pcli-#REPLACE-ME# + name: kn-pcli-#REPLACE-FN-NAME# diff --git a/examples/knative/powercli/kn-pcli-template/function_secret.json b/examples/knative/powercli/kn-pcli-template/function_secret.json index b45d68aa..00eac43b 100644 --- a/examples/knative/powercli/kn-pcli-template/function_secret.json +++ b/examples/knative/powercli/kn-pcli-template/function_secret.json @@ -2,5 +2,5 @@ "VCENTER_SERVER": "FILL-ME-IN", "VCENTER_USERNAME" : "FILL-ME-IN", "VCENTER_PASSWORD" : "FILL-ME-IN", - "VCENTER_CERTIFICATE_ACTION" : "Ignore" + "VCENTER_CERTIFICATE_ACTION" : "Fail" } From fbd4fffd957a438b82e182deae0ee80d80b1f27d Mon Sep 17 00:00:00 2001 From: Michael Gasch Date: Tue, 26 Apr 2022 21:34:44 +0200 Subject: [PATCH 21/54] docs: Update AWS EventBridge auth Closes: #873 Signed-off-by: Michael Gasch --- docs/kb/intro-event-router.md | 50 ++++++++++++++++++++++++++++++++--- vmware-event-router/README.MD | 49 +++++++++++++++++++++++++++++++--- 2 files changed, 93 insertions(+), 6 deletions(-) diff --git a/docs/kb/intro-event-router.md b/docs/kb/intro-event-router.md index 73eee07a..eb92443d 100644 --- a/docs/kb/intro-event-router.md +++ b/docs/kb/intro-event-router.md @@ -97,6 +97,7 @@ not retried and discarded. - [The `auth` section](#the-auth-section) - [Type `basic_auth`](#type-basic_auth) - [Type `aws_access_key`](#type-aws_access_key) + - [Type `aws_iam_role`](#type-aws_iam_role) - [Type `active_directory`](#type-active_directory) - [The `metricsProvider` section](#the-metricsprovider-section) - [Provider Type `default`](#provider-type-default) @@ -463,7 +464,7 @@ as an event `processor`. | `region` | String | AWS region to use, see [regions doc](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html). | true | `us-west-1` | | `eventBus` | String | Name of the event bus to use | true | `default` or `arn:aws:events:us-west-1:1234567890:event-bus/customBus` | | `ruleARN` | String | Rule ARN to use for event pattern matching | true | `arn:aws:events:us-west-1:1234567890:rule/vmware-event-router` | -| `` | Object | AWS IAM role credentials | true | (see `aws_access_key` example below) | +| `` | Object | AWS IAM role credentials | true | (see `aws_access_key` and `aws_iam_role` examples below) | ## The `auth` section @@ -490,6 +491,9 @@ Supported providers/processors: ### Type `aws_access_key` +Use an AWS IAM role with the provided access key ID and secret access key for +authentication. + Supported providers/processors: - `aws_event_bridge` @@ -501,8 +505,48 @@ Supported providers/processors: | `awsAccessKeyAuth.accessKey` | String | Access Key ID for the IAM role used | true | `ABCDEFGHIJK` | | `awsAccessKeyAuth.secretKey` | String | Secret Access Key for the IAM role used | true | `ZYXWVUTSRQPO` | -> **Note:** Currently only IAM user accounts with access key/secret are -> supported to authenticate against AWS EventBridge. Please follow the [user +> **Note:** Please follow the EventBridge IAM [user +> guide](https://docs.aws.amazon.com/eventbridge/latest/userguide/getting-set-up-eventbridge.html) +> before deploying the event router. Further information can also be found in +> the +> [authentication](https://docs.aws.amazon.com/eventbridge/latest/userguide/auth-and-access-control-eventbridge.html#authentication-eventbridge) +> section. + +In addition to the recommendation in the AWS EventBridge user guide you might +want to lock down the IAM role for the VMware Event Router and scope it to these +permissions ("Action"): + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor0", + "Effect": "Allow", + "Action": [ + "events:PutEvents", + "events:ListRules", + "events:TestEventPattern" + ], + "Resource": "*" + } + ] +} +``` + +### Type `aws_iam_role` + +Use an AWS IAM role configured from the shared credentials file. + +Supported providers/processors: + +- `aws_event_bridge` + +| Field | Type | Description | Required | Example | +|--------|--------|------------------------------|----------|----------------| +| `type` | String | Authentication method to use | true | `aws_iam_role` | + +> **Note:** Please follow the EventBridge IAM [user > guide](https://docs.aws.amazon.com/eventbridge/latest/userguide/getting-set-up-eventbridge.html) > before deploying the event router. Further information can also be found in > the diff --git a/vmware-event-router/README.MD b/vmware-event-router/README.MD index 63dea988..252511f4 100644 --- a/vmware-event-router/README.MD +++ b/vmware-event-router/README.MD @@ -417,7 +417,7 @@ as an event `processor`. | `region` | String | AWS region to use, see [regions doc](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html). | true | `us-west-1` | | `eventBus` | String | Name of the event bus to use | true | `default` or `arn:aws:events:us-west-1:1234567890:event-bus/customBus` | | `ruleARN` | String | Rule ARN to use for event pattern matching | true | `arn:aws:events:us-west-1:1234567890:rule/vmware-event-router` | -| `` | Object | AWS IAM role credentials | true | (see `aws_access_key` example below) | +| `` | Object | AWS IAM role credentials | true | (see `aws_access_key` and `aws_iam_role` examples below) | ## The `auth` section @@ -444,6 +444,9 @@ Supported providers/processors: ### Type `aws_access_key` +Use an AWS IAM role with the provided access key ID and secret access key for +authentication. + Supported providers/processors: - `aws_event_bridge` @@ -455,8 +458,48 @@ Supported providers/processors: | `awsAccessKeyAuth.accessKey` | String | Access Key ID for the IAM role used | true | `ABCDEFGHIJK` | | `awsAccessKeyAuth.secretKey` | String | Secret Access Key for the IAM role used | true | `ZYXWVUTSRQPO` | -> **Note:** Currently only IAM user accounts with access key/secret are -> supported to authenticate against AWS EventBridge. Please follow the [user +> **Note:** Please follow the EventBridge IAM [user +> guide](https://docs.aws.amazon.com/eventbridge/latest/userguide/getting-set-up-eventbridge.html) +> before deploying the event router. Further information can also be found in +> the +> [authentication](https://docs.aws.amazon.com/eventbridge/latest/userguide/auth-and-access-control-eventbridge.html#authentication-eventbridge) +> section. + +In addition to the recommendation in the AWS EventBridge user guide you might +want to lock down the IAM role for the VMware Event Router and scope it to these +permissions ("Action"): + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "VisualEditor0", + "Effect": "Allow", + "Action": [ + "events:PutEvents", + "events:ListRules", + "events:TestEventPattern" + ], + "Resource": "*" + } + ] +} +``` + +### Type `aws_iam_role` + +Use an AWS IAM role configured from the shared credentials file. + +Supported providers/processors: + +- `aws_event_bridge` + +| Field | Type | Description | Required | Example | +|--------|--------|------------------------------|----------|----------------| +| `type` | String | Authentication method to use | true | `aws_iam_role` | + +> **Note:** Please follow the EventBridge IAM [user > guide](https://docs.aws.amazon.com/eventbridge/latest/userguide/getting-set-up-eventbridge.html) > before deploying the event router. Further information can also be found in > the From c7cb2c62cc802e3f3ba9cc1db7fad406ba2ad7ac Mon Sep 17 00:00:00 2001 From: Patrick Kremer Date: Mon, 25 Apr 2022 12:42:01 -0500 Subject: [PATCH 22/54] feat: Port HA restart function to Knative Closes #867 Closes #869 Signed-off-by: Patrick Kremer --- docs/site/examples.md | 9 + .../kn-pcli-ha-restarted-vms/Dockerfile | 5 + .../kn-pcli-ha-restarted-vms/README.md | 149 +++++++++++++ .../kn-pcli-ha-restarted-vms/function.yaml | 41 ++++ .../kn-pcli-ha-restarted-vms/ha_secret.json | 13 ++ .../kn-pcli-ha-restarted-vms/handler.ps1 | 199 ++++++++++++++++++ .../test/docker-test-env-variable | 1 + .../test/send-cloudevent-test.ps1 | 32 +++ .../test/send-cloudevent-test.sh | 29 +++ .../test/test-payload.json | 35 +++ 10 files changed, 513 insertions(+) create mode 100644 examples/knative/powercli/kn-pcli-ha-restarted-vms/Dockerfile create mode 100644 examples/knative/powercli/kn-pcli-ha-restarted-vms/README.md create mode 100644 examples/knative/powercli/kn-pcli-ha-restarted-vms/function.yaml create mode 100644 examples/knative/powercli/kn-pcli-ha-restarted-vms/ha_secret.json create mode 100644 examples/knative/powercli/kn-pcli-ha-restarted-vms/handler.ps1 create mode 100644 examples/knative/powercli/kn-pcli-ha-restarted-vms/test/docker-test-env-variable create mode 100644 examples/knative/powercli/kn-pcli-ha-restarted-vms/test/send-cloudevent-test.ps1 create mode 100644 examples/knative/powercli/kn-pcli-ha-restarted-vms/test/send-cloudevent-test.sh create mode 100644 examples/knative/powercli/kn-pcli-ha-restarted-vms/test/test-payload.json diff --git a/docs/site/examples.md b/docs/site/examples.md index 1c92d155..d60a7a65 100644 --- a/docs/site/examples.md +++ b/docs/site/examples.md @@ -95,6 +95,15 @@ examples: - language: powershell url: "/tree/master/examples/knative/powershell/kn-ps-ngw-slack" + - title: VMware HA Notification + usecases: + - item: notification + id: kn-pcli-ha-restarted-vms + description: Function to send an e-mail notification after an HA event. The email includes a list of VMs and timestamps showing when each VM was restarted by HA. + links: + - language: powercli + url: "/tree/master/examples/knative/powercli/kn-pcli-ha-restarted-vms" + - title: VMware Horizon Notification usecases: - item: integration diff --git a/examples/knative/powercli/kn-pcli-ha-restarted-vms/Dockerfile b/examples/knative/powercli/kn-pcli-ha-restarted-vms/Dockerfile new file mode 100644 index 00000000..49b2c980 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-ha-restarted-vms/Dockerfile @@ -0,0 +1,5 @@ +FROM us.gcr.io/daisy-284300/veba/ce-pcli-base:1.4 + +COPY handler.ps1 handler.ps1 + +CMD ["pwsh","./server.ps1"] diff --git a/examples/knative/powercli/kn-pcli-ha-restarted-vms/README.md b/examples/knative/powercli/kn-pcli-ha-restarted-vms/README.md new file mode 100644 index 00000000..4ed112c8 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-ha-restarted-vms/README.md @@ -0,0 +1,149 @@ +# kn-pcli-ha-restarted-vms +Example Knative PowerCLI function kn-pcli-ha-restarted-vms. This function emails an administrator a list of virtual machines that were restarted by vSphere High Availability (HA) after an HA event. The VM list contains all VMs that were restarted on the date the cluster failover event completes. Example: A failover event happens on Feb. 10th at 8:50AM. The function will email a timestamped list of all VMs that were failed over during the 24 hour period between midnight on Feb. 10th and midnight on Feb. 11th + +# Step 1 - Build + +> **Note:** This step is only required if you made code changes to `handler.ps1` +> or `Dockerfile`. + +Create the container image locally to test your function logic. + +Mac/Linux +``` +# change the IMAGE name accordingly, example below for Docker +export IMAGE=/kn-pcli-ha-restarted-vms:1.0 +docker build -t ${IMAGE} . +``` + +Windows +``` +# change the IMAGE name accordingly, example below for Docker +$IMAGE="/kn-pcli-ha-restarted-vms:1.0" +docker build -t ${IMAGE} . +``` +# Step 2 - Test + +Verify the container image works by executing it locally. + +Change into the `test` directory +```console +cd test +``` + +Update the following variable names within the `docker-test-env-variable` file. +> Any changes to variables must also be updated in `ha_secret.yaml` and `test/docker-test-env-variable` + +* `VCENTER_SERVER` - IP Address or FQDN of the vCenter Server to connect to +* `VCENTER_USERNAME` - vCenter account with permission to reconfigure distributed virtual switches +* `VCENTER_PASSWORD` - vCenter password associated with the username +* `VCENTER_CERTIFCATE_ACTION` - Set-PowerCLIConfiguration Action to configure when connection fails due to certificate error, default is Fail. (Possible values: `Fail`, `Ignore` or `Warn`) +* `SMTP_SERVER` - The SMTP server responsible for relaying the e-mail notification +* `SMTP_PORT` - The port `SMTP_SERVER` is listening on +* `SMTP_USERNAME` - Optional. Username for SMTP authentication +* `SMTP_PASSWORD` - Optional. Username for SMTP authentication +* `EMAIL_TO` - Email address to receive notifications. At least one is required, multiple are allowed +* `EMAIL_FROM` - Email address to send notifications +* `DISPLAY_HOST_FQDN` - `true` or `false` - Set to true if you want the host's fully qualified domain name included in the report + +If you built a custom image in Step 1, comment out the default `IMAGE` command below - the `docker run` command will then use use the value previously stored in the `IMAGE` variable. Otherwise, use the default image as shown below. Start the container image by running the following commands: + +Mac/Linux +```console +export IMAGE=us.gcr.io/daisy-284300/veba/kn-pcli-ha-restarted-vms:1.0 +docker run -e FUNCTION_DEBUG=true -e PORT=8080 --env-file docker-test-env-variable -it --rm -p 8080:8080 ${IMAGE} +``` +Windows +```console +$IMAGE="us.gcr.io/daisy-284300/veba/kn-pcli-ha-restarted-vms:1.0" +docker run -e FUNCTION_DEBUG=true -e PORT=8080 --env-file docker-test-env-variable -it --rm -p 8080:8080 ${IMAGE} +``` + +Unlike many other sample functions, you do not need to edit `test-payload.json` to test your function. However, your vCenter must have logged a recent HA event with VM restart for this function to work. + +One way to simulate an HA event is by forcing a PSOD. You can do this at the command line of a vSphere host. Make sure to have at least one VM running on the host for HA to restart. +> Warning: This command will immediately cause a kernel panic, crashing your host! Do not run this in Production. +```console +vsish -e set /reliability/crashMe/Panic 1 +``` +Wait for HA to restart your VM(s) before continuing. + +In a separate terminal, run either `send-cloudevent-test.ps1` (PowerShell Script) or `send-cloudevent-test.sh` (Bash Script) to simulate a CloudEvent payload being sent to the local container image. The scripts will send the contents of `test-payload.json` as the payload with a subject of `com.vmware.vc.HA.ClusterFailoverActionCompletedEvent` + +```console +Testing Function ... +See docker container console for output + +# Output from docker container console +04/25/2022 21:35:20 - PowerShell HTTP server start listening on 'http://*:8080/' +04/25/2022 21:35:20 - Processing Init + +04/25/2022 21:35:20 - Configuring PowerCLI Configuration Settings + +04/25/2022 21:35:21 - Connecting to vCenter Server vcsa.primp-industries.local + +04/25/2022 21:35:25 - Successfully connected to vcsa.primp-industries.local + +04/25/2022 21:35:25 - Init Processing Completed + +04/25/2022 21:35:25 - Starting HTTP CloudEvent listener + +04/25/2022 21:35:29 - DEBUG: From - noreply@vmweventbroker.io +04/25/2022 21:35:29 - DEBUG: To - notifications@vmweventbroker.io +04/25/2022 21:35:29 - Handler Processing Completed ... +``` + +> Pro Tip - If you are rapidly iterating on the code and want to easily rebuild and launch the container, +> you can chain all of the commands together with ampersands. This will allow you to re-run +> the commands by simply pressing the `up` arrow and `Enter`. + +```console +cd .. && docker build -t ${IMAGE} . && cd test && docker run -e HA_DEBUG=true -e PORT=8080 --env-file docker-test-env-variable -it --rm -p 8080:8080 ${IMAGE} +``` +# Step 3 - Deploy + +> **Note:** The following steps assume a working Knative environment using the +`default` Rabbit `broker`. The Knative `service` and `trigger` will be installed in the +`vmware-functions` Kubernetes namespace, assuming that the `broker` is also available there. + +If you built a custom image, push it to an accessible registry such as Docker once you're done developing and testing your function logic. + +```console +docker push ${IMAGE} +``` + +Update the `ha_secret.json` file with your vCenter Server credentials and configurations and then create the Kubernetes secret which can then be accessed from within the function by using the environment variable named called `HA_SECRET`. + +```console +# create secret +kubectl -n vmware-functions create secret generic ha-secret --from-file=HA_SECRET=ha_secret.json + +# update label for secret to show up in VEBA UI +kubectl -n vmware-functions label secret ha-secret app=veba-ui +``` + +Edit the `function.yaml` file with the name of the container image from Step 1 if you made any changes. If not, the default VMware container image will suffice. By default, the function deployment will filter on the `com.vmware.vc.HA.ClusterFailoverActionCompletedEvent` vCenter Server Event. + +Deploy the function to the VMware Event Broker Appliance (VEBA). + +```console +# deploy function +kubectl -n vmware-functions apply -f function.yaml +``` + +For testing purposes, the `function.yaml` contains the following annotations, which will ensure the Knative Service Pod will always run **exactly** one instance for debugging purposes. Functions deployed through through the VMware Event Broker Appliance UI defaults to scale to 0, which means the pods will only run when it is triggered by an vCenter Event. + +```yaml +annotations: + autoscaling.knative.dev/maxScale: "1" + autoscaling.knative.dev/minScale: "1" +``` + +# Step 4 - Undeploy + +```console +# undeploy function +kubectl -n vmware-functions delete -f function.yaml + +# delete secret +kubectl -n vmware-functions delete secret ha-secret +``` diff --git a/examples/knative/powercli/kn-pcli-ha-restarted-vms/function.yaml b/examples/knative/powercli/kn-pcli-ha-restarted-vms/function.yaml new file mode 100644 index 00000000..3b248bf7 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-ha-restarted-vms/function.yaml @@ -0,0 +1,41 @@ +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: kn-pcli-ha-restarted-vms + labels: + app: veba-ui +spec: + template: + metadata: + annotations: + autoscaling.knative.dev/maxScale: "1" + autoscaling.knative.dev/minScale: "1" + spec: + containers: + - image: us.gcr.io/daisy-284300/veba/kn-pcli-ha-restarted-vms:1.0 + envFrom: + - secretRef: + name: ha-secret + env: + - name: HA_DEBUG + value: "true" +--- +apiVersion: eventing.knative.dev/v1 +kind: Trigger +metadata: + name: veba-pcli-ha-restarted-vms-trigger + labels: + app: veba-ui +spec: + broker: default + filter: + attributes: + type: com.vmware.event.router/eventex + # Replace this subject with the event you need to trigger on + # Then, edit send-cloudevent-test.ps1 and send-cloudevent-test.sh in the /test folder + subject: com.vmware.vc.HA.ClusterFailoverActionCompletedEvent + subscriber: + ref: + apiVersion: serving.knative.dev/v1 + kind: Service + name: kn-pcli-ha-restarted-vms diff --git a/examples/knative/powercli/kn-pcli-ha-restarted-vms/ha_secret.json b/examples/knative/powercli/kn-pcli-ha-restarted-vms/ha_secret.json new file mode 100644 index 00000000..5a59088d --- /dev/null +++ b/examples/knative/powercli/kn-pcli-ha-restarted-vms/ha_secret.json @@ -0,0 +1,13 @@ +{ + "VCENTER_SERVER": "FILL-ME-IN", + "VCENTER_USERNAME" : "FILL-ME-IN", + "VCENTER_PASSWORD" : "FILL-ME-IN", + "VCENTER_CERTIFICATE_ACTION" : "Fail", + "SMTP_SERVER" : "FILL-ME-IN", + "SMTP_PORT" : "FILL-ME-IN", + "SMTP_USERNAME" : "", + "SMTP_PASSWORD" : "", + "EMAIL_TO": ["FILL-ME-IN"], + "EMAIL_FROM" : "FILL-ME-IN", + "DISPLAY_HOST_FQDN":"true" +} diff --git a/examples/knative/powercli/kn-pcli-ha-restarted-vms/handler.ps1 b/examples/knative/powercli/kn-pcli-ha-restarted-vms/handler.ps1 new file mode 100644 index 00000000..87cb4ffb --- /dev/null +++ b/examples/knative/powercli/kn-pcli-ha-restarted-vms/handler.ps1 @@ -0,0 +1,199 @@ +Function Process-Init { + [CmdletBinding()] + param() + Write-Host "$(Get-Date) - Processing Init`n" + + try { + $jsonSecrets = ${env:HA_SECRET} | ConvertFrom-Json + } + catch { + throw "`nK8s secret `$env:HA_SECRET does not look to be defined" + } + + # Extract all tag secrets for ease of use in function + $VCENTER_SERVER = ${jsonSecrets}.VCENTER_SERVER + $VCENTER_USERNAME = ${jsonSecrets}.VCENTER_USERNAME + $VCENTER_PASSWORD = ${jsonSecrets}.VCENTER_PASSWORD + $VCENTER_CERTIFICATE_ACTION = ${jsonSecrets}.VCENTER_CERTIFICATE_ACTION + + # Configure TLS 1.2/1.3 support as this is required for latest vSphere release + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 -bor [System.Net.SecurityProtocolType]::Tls13 + + Write-Host "$(Get-Date) - Configuring PowerCLI Configuration Settings`n" + Set-PowerCLIConfiguration -InvalidCertificateAction:${VCENTER_CERTIFICATE_ACTION} -ParticipateInCeip:$true -Confirm:$false + + Write-Host "$(Get-Date) - Connecting to vCenter Server $VCENTER_SERVER`n" + + try { + Connect-VIServer -Server $VCENTER_SERVER -User $VCENTER_USERNAME -Password $VCENTER_PASSWORD + } + catch { + Write-Error "$(Get-Date) - ERROR: Failed to connect to vCenter Server" + throw $_ + } + + Write-Host "$(Get-Date) - Successfully connected to $VCENTER_SERVER`n" + + Write-Host "$(Get-Date) - Init Processing Completed`n" +} + +Function Process-Shutdown { + [CmdletBinding()] + param() + Write-Host "$(Get-Date) - Processing Shutdown`n" + + Write-Host "$(Get-Date) - Disconnecting from vCenter Server`n" + + try { + Disconnect-VIServer * -Confirm:$false + } + catch { + Write-Error "$(Get-Date) - Error: Failed to Disconnect from vCenter Server" + } + + Write-Host "$(Get-Date) - Shutdown Processing Completed`n" +} + +Function Process-Handler { + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=$true)][CloudNative.CloudEvents.CloudEvent]$CloudEvent + ) + + # Decode CloudEvent + try { + $cloudEventData = $cloudEvent | Read-CloudEventJsonData -Depth 10 + } + catch { + throw "`nPayload must be JSON encoded" + } + + try { + $jsonSecrets = ${env:HA_SECRET} | ConvertFrom-Json + } + catch { + throw "`nK8s secret `$env:HA_SECRET does not look to be defined" + } + + # Main processing of gathering specific events + $report = @() + $eventnumber = 1000 + # get vCenter EventManager + try { + $si = get-view ServiceInstance + } + catch { + Write-Host "$(Get-Date) - ERROR: Could not retrieve Service Instance" + throw $_ + } + + try { + $em = get-view $si.Content.EventManager + } + catch { + Write-Host "$(Get-Date) - ERROR: Could not retrieve Event Manager" + throw $_ + } + + # Create Event Filter Spec + $EventFilterSpec = New-Object VMware.Vim.EventFilterSpec + # Get specific Events based on EventTypeIDs for EventFilterSpec + $tgtEvtTypeIDs = "com.vmware.vc.ha.VmRestartedByHAEvent", "com.vmware.vc.HA.DasHostFailedEvent" + $EventFilterSpec.EventTypeId = $tgtEvtTypeIDs + # Time range to look for specific IDs (current day specified) for EventFilterSpec + $EventFilterSpecByTime = New-Object VMware.Vim.EventFilterSpecByTime + # [datetime]::Today is midnight on the date the ClusterFailoverActionCompletedEvent fired + $EventFilterSpecByTime.BeginTime = [datetime]::Today + # Searches the entire 24 hour period on the date the ClusterFailoverActionCompletedEvent fired + $EventFilterSpecByTime.EndTime = ([datetime]::Today).AddDays(+1) + $EventFilterSpec.Time = $EventFilterSpecByTime + + # Create an Event Collector and loop through events storing them into an array + try { + $eCollector = Get-View ($em.CreateCollectorForEvents($EventFilterSpec)) + } + catch { + Write-Host "$(Get-Date) - ERROR: Could not create event collector" + throw $_ + } + + try { + $events = $eCollector.ReadNextEvents($eventnumber) + } + catch { + Write-Host "$(Get-Date) - ERROR: Could not retrieve events with event collector" + throw $_ + } + + While ($events) { + $events | %{ + $report += $_ + } + try { + $events = $eCollector.ReadNextEvents($eventnumber) + } + catch { + Write-Host "$(Get-Date) - ERROR: Could not retrieve events with event collector" + throw $_ + } + } + + $eCollector.DestroyCollector() + + $SMTP_SERVER = ${jsonSecrets}.SMTP_SERVER + $SMTP_PORT = ${jsonSecrets}.SMTP_PORT + $SMTP_USERNAME = ${jsonSecrets}.SMTP_USERNAME + $SMTP_PASSWORD = ${jsonSecrets}.SMTP_PASSWORD + $EMAIL_TO = ${jsonSecrets}.EMAIL_TO + $EMAIL_FROM = ${jsonSecrets}.EMAIL_FROM + $DISPLAY_HOST_FQDN = ${jsonSecrets}.DISPLAY_HOST_FQDN + + # Retrieve Host name of ESXi host which crashed - used for email report + if ($report | Where-Object{$_.EventTypeId -eq "com.vmware.vc.HA.DasHostFailedEvent"}) { + if ($DISPLAY_HOST_FQDN -eq $false){ + # Displays just hostname - strips FQDN + $vmHost = ($report | Where-Object {$_.EventTypeId -eq "com.vmware.vc.HA.DasHostFailedEvent"} | Select-Object ObjectName -ExpandProperty ObjectName -First 1).split(".")[0] + } else { + # Displays host FQDN + $vmHost = $report | Where-Object {$_.EventTypeId -eq "com.vmware.vc.HA.DasHostFailedEvent"} | Select-Object ObjectName -ExpandProperty ObjectName -First 1 + } + } + + # Set up fields for email body and send email - only sending VMname, time VM restarted on another host, and VM description + $output = $report | Where-Object {$_.ObjectType -eq "VirtualMachine" } | Select-Object ObjectName, @{N = "Date"; E = { $_.CreatedTime } }, @{N = "Description"; E = { (" - " + (Get-view -id $_.vm.vm).config.annotation | Out-String) } } | Sort-Object ObjectName + $msgBody = $output | ConvertTo-Html | Out-String + $subject = "** $vmHost Failure - VMs Restarted **" + + if (${env:HA_DEBUG} -eq "true") { + Write-Host "$(Get-Date) - DEBUG: From - $($EMAIL_FROM)" + Write-Host "$(Get-Date) - DEBUG: To - $($EMAIL_TO)" + } + + Write-Host "$(Get-Date) - Sending notification for host $($vmHost)" + + # If defined in the config file, send via authenticated SMTP, otherwise use standard SMTP + if ($SMTP_PASSWORD.length -gt 0 -and $SMTP_USERNAME.length -gt 0) + { + $password = ConvertTo-SecureString "$($SMTP_PASSWORD)" -AsPlainText -Force + $credential = New-Object System.Management.Automation.PSCredential($($SMTP_USERNAME), $password) + try { + Send-MailMessage -From $($EMAIL_FROM) -to $($EMAIL_TO) -Subject $subject -Body $msgBody -BodyAsHtml -SmtpServer $($SMTP_SERVER) -port $($SMTP_PORT) -UseSsl -Credential $credential #-Encoding UTF32 + } + catch { + Write-Host "$(Get-Date) - ERROR: Could not send authenticated email" + throw $_ + } + } + else + { + try { + Send-MailMessage -From $($EMAIL_FROM) -to $($EMAIL_TO) -Subject $subject -Body $msgBody -BodyAsHtml -SmtpServer $($SMTP_SERVER) -port $($SMTP_PORT) -Encoding UTF32 + } + catch { + Write-Host "$(Get-Date) - ERROR: Could not send email" + throw $_ + } + } + + Write-Host "$(Get-Date) - Handler Processing Completed ...`n" +} \ No newline at end of file diff --git a/examples/knative/powercli/kn-pcli-ha-restarted-vms/test/docker-test-env-variable b/examples/knative/powercli/kn-pcli-ha-restarted-vms/test/docker-test-env-variable new file mode 100644 index 00000000..e3d584e7 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-ha-restarted-vms/test/docker-test-env-variable @@ -0,0 +1 @@ +HA_SECRET={"VCENTER_SERVER":"FILL-ME-IN","VCENTER_USERNAME":"FILL-ME-IN","VCENTER_PASSWORD":"FILL-ME-IN","VCENTER_CERTIFICATE_ACTION":"Fail","SMTP_SERVER":"FILL-ME-IN","SMTP_PORT":"25","SMTP_USERNAME":"","SMTP_PASSWORD":"","EMAIL_TO":["email_recipients@abc.com"],"EMAIL_FROM":"email_sender@abc.com","DISPLAY_HOST_FQDN":"false"} \ No newline at end of file diff --git a/examples/knative/powercli/kn-pcli-ha-restarted-vms/test/send-cloudevent-test.ps1 b/examples/knative/powercli/kn-pcli-ha-restarted-vms/test/send-cloudevent-test.ps1 new file mode 100644 index 00000000..bce26745 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-ha-restarted-vms/test/send-cloudevent-test.ps1 @@ -0,0 +1,32 @@ +# The ce-subject value should match the event router subject in function.yaml +$subject = "com.vmware.vc.HA.ClusterFailoverActionCompletedEvent" +$payloadPath = "./test-payload.json" + +if ( $args.Count -gt 0 ) { + if ( Test-Path $args[0] ) { + $payloadPath = $args[0] + } + else { + Write-Host "$(Get-Date) - ERROR: Invalid path"$args[0]"`n" + exit + } + + if ( $args.Count -gt 1 ) { + $subject = $args[1] + } +} + +$headers = @{ + "Content-Type" = "application/json"; + "ce-specversion" = "1.0"; + "ce-id" = "id-123"; + "ce-source" = "source-123"; + "ce-type" = "com.vmware.event.router/eventex"; + "ce-subject" = $($subject); +} +$body = Get-Content -Raw -Path $payloadPath + +Write-Host "Testing Function ..." +Invoke-WebRequest -Uri http://localhost:8080 -Method POST -Headers $headers -Body $body + +Write-host "See docker container console for output" \ No newline at end of file diff --git a/examples/knative/powercli/kn-pcli-ha-restarted-vms/test/send-cloudevent-test.sh b/examples/knative/powercli/kn-pcli-ha-restarted-vms/test/send-cloudevent-test.sh new file mode 100644 index 00000000..09a183d2 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-ha-restarted-vms/test/send-cloudevent-test.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# The ce-subject value should match the event router subject in function.yaml +echo "Testing Function ..." +PAYLOAD_PATH="test-payload.json" +SUBJECT="com.vmware.vc.HA.ClusterFailoverActionCompletedEvent" + +if [ $# -gt 0 ]; then + if test -f "$1"; then + PAYLOAD_PATH=$1 + else + echo "$1 not found" + exit 1 + fi + + if [ $# -gt 1 ]; then + SUBJECT=$2 + fi +fi +curl -d@$PAYLOAD_PATH \ + -H "Content-Type: application/json" \ + -H 'ce-specversion: 1.0' \ + -H 'ce-id: d70079f9-fddd-4b7f-aa76-1193f28b0611' \ + -H 'ce-source: https://vcenter.local/sdk' \ + -H 'ce-type: com.vmware.event.router/eventex' \ + -H 'ce-subject: '$SUBJECT \ + -X POST localhost:8080 + +echo "See docker container console for output" diff --git a/examples/knative/powercli/kn-pcli-ha-restarted-vms/test/test-payload.json b/examples/knative/powercli/kn-pcli-ha-restarted-vms/test/test-payload.json new file mode 100644 index 00000000..dc1a028d --- /dev/null +++ b/examples/knative/powercli/kn-pcli-ha-restarted-vms/test/test-payload.json @@ -0,0 +1,35 @@ +{ + "Key": 929015, + "ChainId": 929015, + "CreatedTime": "2022-04-25T17:29:48.171999Z", + "UserName": "", + "Datacenter": { + "Name": "DC1", + "Datacenter": { + "Type": "Datacenter", + "Value": "datacenter-47" + } + }, + "ComputeResource": { + "Name": "CL1", + "ComputeResource": { + "Type": "ClusterComputeResource", + "Value": "domain-c52" + } + }, + "Host": null, + "Vm": null, + "Ds": null, + "Net": null, + "Dvs": null, + "FullFormattedMessage": "vSphere HA completed a virtual machine failover action in cluster CL1 in datacenter DC1", + "ChangeTag": "", + "EventTypeId": "com.vmware.vc.HA.ClusterFailoverActionCompletedEvent", + "Severity": "info", + "Message": "", + "Arguments": null, + "ObjectId": "domain-c52", + "ObjectType": "ClusterComputeResource", + "ObjectName": "CL1", + "Fault": null + } \ No newline at end of file From 253c3ffad18ae3150c294018e8fd3685d364f8c3 Mon Sep 17 00:00:00 2001 From: Patrick Kremer Date: Thu, 28 Apr 2022 18:47:45 -0500 Subject: [PATCH 23/54] fix: Update container image link Closes #878 Signed-off-by: Patrick Kremer --- examples/knative/powercli/kn-pcli-pg-check/function.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/knative/powercli/kn-pcli-pg-check/function.yaml b/examples/knative/powercli/kn-pcli-pg-check/function.yaml index 7f501ddb..0958e03f 100644 --- a/examples/knative/powercli/kn-pcli-pg-check/function.yaml +++ b/examples/knative/powercli/kn-pcli-pg-check/function.yaml @@ -12,7 +12,7 @@ spec: autoscaling.knative.dev/minScale: "1" spec: containers: - - image: kremerpatrick/kn-pcli-pg-check:1.0 + - image: us.gcr.io/daisy-284300/veba/kn-pcli-pg-check:1.0 envFrom: - secretRef: name: pg-check-secret From cb9d2eaee59d7c083018fd4d5f0f0ceb9ad53df3 Mon Sep 17 00:00:00 2001 From: Patrick Kremer Date: Wed, 27 Apr 2022 16:49:56 -0500 Subject: [PATCH 24/54] feat: Port datastore usage email function to Knative Closes #870 Signed-off-by: Patrick Kremer --- .../kn-pcli-datastore-usage-email/Dockerfile | 5 + .../kn-pcli-datastore-usage-email/README.md | 180 +++++++++++++++ .../datastore_secret.json | 16 ++ .../function.yaml | 41 ++++ .../kn-pcli-datastore-usage-email/handler.ps1 | 216 ++++++++++++++++++ .../test/docker-test-env-variable | 1 + .../test/send-cloudevent-test.ps1 | 32 +++ .../test/send-cloudevent-test.sh | 29 +++ .../test/test-payload.json | 50 ++++ 9 files changed, 570 insertions(+) create mode 100644 examples/knative/powercli/kn-pcli-datastore-usage-email/Dockerfile create mode 100644 examples/knative/powercli/kn-pcli-datastore-usage-email/README.md create mode 100644 examples/knative/powercli/kn-pcli-datastore-usage-email/datastore_secret.json create mode 100644 examples/knative/powercli/kn-pcli-datastore-usage-email/function.yaml create mode 100644 examples/knative/powercli/kn-pcli-datastore-usage-email/handler.ps1 create mode 100644 examples/knative/powercli/kn-pcli-datastore-usage-email/test/docker-test-env-variable create mode 100644 examples/knative/powercli/kn-pcli-datastore-usage-email/test/send-cloudevent-test.ps1 create mode 100644 examples/knative/powercli/kn-pcli-datastore-usage-email/test/send-cloudevent-test.sh create mode 100644 examples/knative/powercli/kn-pcli-datastore-usage-email/test/test-payload.json diff --git a/examples/knative/powercli/kn-pcli-datastore-usage-email/Dockerfile b/examples/knative/powercli/kn-pcli-datastore-usage-email/Dockerfile new file mode 100644 index 00000000..49b2c980 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-datastore-usage-email/Dockerfile @@ -0,0 +1,5 @@ +FROM us.gcr.io/daisy-284300/veba/ce-pcli-base:1.4 + +COPY handler.ps1 handler.ps1 + +CMD ["pwsh","./server.ps1"] diff --git a/examples/knative/powercli/kn-pcli-datastore-usage-email/README.md b/examples/knative/powercli/kn-pcli-datastore-usage-email/README.md new file mode 100644 index 00000000..4697bfe0 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-datastore-usage-email/README.md @@ -0,0 +1,180 @@ +# kn-pcli-datastore-usage-email +Example Knative PowerCLI function kn-pcli-datastore-usage-email. Sends email notifications to a specified email address for datastore usage on disk alarms. Optional configuration for a per-datastore email, enabling different recipients for different datastores. + +# Step 1 - Build + +> **Note:** This step is only required if you made code changes to `handler.ps1` +> or `Dockerfile`. + +Create the container image locally to test your function logic. + +Mac/Linux +``` +# change the IMAGE name accordingly, example below for Docker +export IMAGE=/kn-pcli-datastore-usage-email:1.0 +docker build -t ${IMAGE} . +``` + +Windows +``` +# change the IMAGE name accordingly, example below for Docker +$IMAGE="/kn-pcli-datastore-usage-email:1.0" +docker build -t ${IMAGE} . +``` +# Step 2 - Test + +Verify the container image works by executing it locally. + +Change into the `test` directory +```console +cd test +``` + +Update the following variable names within the `docker-test-env-variable` file. + +* `VCENTER_SERVER` - IP Address or FQDN of the vCenter Server to connect to +* `VCENTER_USERNAME` - vCenter account with permission to reconfigure distributed virtual switches +* `VCENTER_PASSWORD` - vCenter password associated with the username +* `VCENTER_CERTIFCATE_ACTION` - Set-PowerCLIConfiguration Action to configure when connection fails due to certificate error, default is `Fail`. (Possible values: `Fail`, `Ignore` or `Warn`) +* `VC_ALARM_NAME` - The alarm to trigger alerts for. The default is the default datastore usage alarm for all vCenter installations. If you have a custom +* `DATASTORE_NAMES` - A list of datastore names that you want monitored by the function +* `SMTP_SERVER` - SMTP server IP or FQDN +* `SMTP_PORT` - SMTP port, typically 25 for unauthenticated and 587 for authenticated +* `SMTP_USERNAME` - Optional. Username for authenticated SMTP +* `SMTP_PASSWORD` - Optional. Password for authenticated SMTP +* `EMAIL_SUBJECT` - The subject line of the notification email +* `EMAIL_TO` - A list of recipients for the notification +* `EMAIL_FROM` - The email address the notification email comes from +* `DATASTORE_CUSTOM_PROP_EMAIL_TO` - Optional. The name of a custom attribute containing datastore-specific notification email addresses. See the `Custom Recipients` section for details. +## Custom recipients + +The function always sends notification emails to `EMAIL_TO`. Some customers want a specific group notified based on the datastore. For example, you might want a group of database administrators notified if a datastore dedicated to MySQL databses begins to fill. + +You can accomplish per-datastore notifications by configuring any datastore with a custom attribute. For example, you might add a custom attribute named `notify_email` to `datastore1`. Another datastore, `datastore2`, does not have the custom attribute. You then update `DATASTORE_CUSTOM_PROP_EMAIL_TO` with a value of `notify_email`. When the function runs, it will check the alarming datastore for the presence of a custom attribute named `notify_email`. If the custom attribute is found, notifications for `datastore1` will be sent to `EMAIL_TO` and the email address found in custom attribute `notify_email`. Notifications for `datastore2` will be sent only to `EMAIL_TO`. + +## Run Container + +If you built a custom image in Step 1, comment out the default `IMAGE` command below - the `docker run` command will then use use the value previously stored in the `IMAGE` variable. Otherwise, use the default image as shown below. Start the container image by running the following commands: + +Mac/Linux +```console +export IMAGE=us.gcr.io/daisy-284300/veba/kn-pcli-datastore-usage-email:1.0 +docker run -e FUNCTION_DEBUG=true -e PORT=8080 --env-file docker-test-env-variable -it --rm -p 8080:8080 ${IMAGE} +``` +Windows +```console +$IMAGE="us.gcr.io/daisy-284300/veba/kn-pcli-datastore-usage-email:1.0" +docker run -e FUNCTION_DEBUG=true -e PORT=8080 --env-file docker-test-env-variable -it --rm -p 8080:8080 ${IMAGE} +``` +If you are not using the custom attribute functionality, you can test the function without making any changes. You can skip the next section. + +## Payload changes for custom attribute functionality + +In the `test` directory, edit `test-payload.json`. Locate the `Ds` section of the JSON file. Change the `Name:` property from `ProdDatastore1` to the name of the datastore in your vCenter inventory that contains the custom attribute. If you do not make this change, the function will still be invoked, but no notifications will be sent because the datastore will not be found. + +```json + "Ds": { + "Name": "ProdDatastore1", + "Datastore": { + "Type": "Datastore", + "Value": "datastore-60" + } + }, +``` + +You must also edit `docker-test-env-variable`, replace one of the existing values with the same datastore name you used in `test-payload.json` +```json +"DATASTORE_NAMES":["ProdDatastore1","ProdDatastore2"] +``` +>Note : If you make a change to `docker-test-env-variable`, you must run the `docker build` command again. +## Using the test scripts + +In a separate terminal, run either `send-cloudevent-test.ps1` (PowerShell Script) or `send-cloudevent-test.sh` (Bash Script) to simulate a CloudEvent payload being sent to the local container image. When run with no arguments, the scripts will send the contents of `test-payload.json` as the payload. If you pass the scripts a different filename as an argument, they will send the contents of the specified file instead. Example: `send-cloudevent-test.ps1 test-payload2.json`. This technique can be useful if you want to test notifications for multiple datastores. + +```console +Testing Function ... +See docker container console for output + +# Output from docker container console +04/28/2022 22:01:44 - DEBUG: Alarm Name: Datastore usage on disk +04/28/2022 22:01:44 - DEBUG: DS Name: VEBA-DS-01 +04/28/2022 22:01:44 - DEBUG: Alarm Status: yellow +04/28/2022 22:01:44 - DEBUG: vCenter: source-123 +04/28/2022 22:01:44 - DEBUG: Data Center: DataCenter1 +04/28/2022 22:01:44 - DEBUG: Alarm to Monitor: Datastore usage on disk +04/28/2022 22:01:44 - DEBUG: Datastores to Monitor: VEBA-DS-01 VEBA-DS-02 +04/28/2022 22:01:44 - DEBUG: Message Subject: ⚠️ [VMC Datastore Notification Alarm] ⚠️ +04/28/2022 22:01:44 - DEBUG: Message Body: Datastore usage on disk VEBA-DS-01 has reached warning threshold. +Please log in to source-123 and ensure that everything is operating as expected. + vCenter Server: source-123 + Datacenter: DataCenter1 + Datastore: VEBA-DS-01 +04/28/2022 22:01:44 - DEBUG: custom prop: notify_email +04/28/2022 22:01:44 - DEBUG: email Key: 101 +04/28/2022 22:01:44 - INFO: Datastore VEBA-DS-01 has Custom Field: notify_email with value: notify2@vmweventbroker.io + +04/28/2022 22:01:44 - DEBUG: Found key 101 with value notify2@vmweventbroker.io +04/28/2022 22:01:44 - Sending notification to notify1@vmweventbroker.io notify2@vmweventbroker.io ... + +04/28/2022 22:01:45 - datastore-usage-email operation complete ... + +04/28/2022 22:01:45 - Handler Processing Completed ... +``` +--- +> Pro Tip - If you are rapidly iterating on the code and want to easily rebuild and launch the container, +> you can chain all of the commands together with ampersands. This will allow you to re-run +> the commands by simply pressing the `up` arrow and `Enter`. + +```console +cd .. && docker build -t ${IMAGE} . && cd test && docker run -e FUNCTION_DEBUG=true -e PORT=8080 --env-file docker-test-env-variable -it --rm -p 8080:8080 ${IMAGE} +``` + +# Step 3 - Deploy + +> **Note:** The following steps assume a working Knative environment using the +`default` Rabbit `broker`. The Knative `service` and `trigger` will be installed in the +`vmware-functions` Kubernetes namespace, assuming that the `broker` is also available there. + +If you built a custom image, push it to an accessible registry such as Docker once you're done developing and testing your function logic. + +```console +docker push ${IMAGE} +``` + +Update the `datastore_secret.json` file with your vCenter Server credentials and configurations and then create the kubernetes secret which can then be accessed from within the function by using the environment variable named called `DATASTORE_SECRET`. + +```console +# create secret +kubectl -n vmware-functions create secret generic datastore-secret --from-file=DATASTORE_SECRET=datastore_secret.json + +# update label for secret to show up in VEBA UI +kubectl -n vmware-functions label secret function-secret app=veba-ui +``` + +Edit the `function.yaml` file with the name of the container image from Step 1 if you made any changes. If not, the default VMware container image will suffice. By default, the function deployment will filter on the `AlarmStatusChangedEventt` vCenter Server Event. If you wish to change this, update the `subject` field within `function.yaml` to the desired event type. + + +Deploy the function to the VMware Event Broker Appliance (VEBA). + +```console +# deploy function +kubectl -n vmware-functions apply -f function.yaml +``` + +For testing purposes, the `function.yaml` contains the following annotations, which will ensure the Knative Service Pod will always run **exactly** one instance for debugging purposes. Functions deployed through through the VMware Event Broker Appliance UI defaults to scale to 0, which means the pods will only run when it is triggered by an vCenter Event. + +```yaml +annotations: + autoscaling.knative.dev/maxScale: "1" + autoscaling.knative.dev/minScale: "1" +``` +# Step 4 - Undeploy + +```console +# undeploy function + +kubectl -n vmware-functions delete -f function.yaml + +# delete secret +kubectl -n vmware-functions delete secret function-secret +``` \ No newline at end of file diff --git a/examples/knative/powercli/kn-pcli-datastore-usage-email/datastore_secret.json b/examples/knative/powercli/kn-pcli-datastore-usage-email/datastore_secret.json new file mode 100644 index 00000000..5e11f3a0 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-datastore-usage-email/datastore_secret.json @@ -0,0 +1,16 @@ +{ + "VCENTER_SERVER": "FILL-ME-IN", + "VCENTER_USERNAME" : "FILL-ME-IN", + "VCENTER_PASSWORD" : "FILL-ME-IN", + "VCENTER_CERTIFICATE_ACTION" : "Fail", + "VC_ALARM_NAME" : "Datastore usage on disk", + "DATASTORE_NAMES" : ["ProdDatastore1","ProdDatastore2"], + "SMTP_SERVER" : "FILL-ME-IN", + "SMTP_PORT" : "FILL-ME-IN", + "SMTP_USERNAME" : "", + "SMTP_PASSWORD" : "", + "EMAIL_SUBJECT" : "[VMC Datastore Notification Alarm]", + "EMAIL_TO": ["FILL-ME-IN"], + "EMAIL_FROM" : "FILL-ME-IN", + "DATASTORE_CUSTOM_PROP_EMAIL_TO" : "" +} diff --git a/examples/knative/powercli/kn-pcli-datastore-usage-email/function.yaml b/examples/knative/powercli/kn-pcli-datastore-usage-email/function.yaml new file mode 100644 index 00000000..7f953771 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-datastore-usage-email/function.yaml @@ -0,0 +1,41 @@ +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: kn-pcli-datastore-usage-email + labels: + app: veba-ui +spec: + template: + metadata: + annotations: + autoscaling.knative.dev/maxScale: "1" + autoscaling.knative.dev/minScale: "1" + spec: + containers: + - image: us.gcr.io/daisy-284300/veba/kn-pcli-datastore-usage-email:1.0 + envFrom: + - secretRef: + name: datastore-secret + env: + - name: FUNCTION_DEBUG + value: "true" +--- +apiVersion: eventing.knative.dev/v1 +kind: Trigger +metadata: + name: kn-pcli-datastore-usage-email-trigger + labels: + app: veba-ui +spec: + broker: default + filter: + attributes: + type: com.vmware.event.router/event + # Replace this subject with the event you need to trigger on + # Then, edit send-cloudevent-test.ps1 and send-cloudevent-test.sh in the /test folder + subject: AlarmStatusChangedEvent + subscriber: + ref: + apiVersion: serving.knative.dev/v1 + kind: Service + name: kn-pcli-datastore-usage-email diff --git a/examples/knative/powercli/kn-pcli-datastore-usage-email/handler.ps1 b/examples/knative/powercli/kn-pcli-datastore-usage-email/handler.ps1 new file mode 100644 index 00000000..f9d69b2f --- /dev/null +++ b/examples/knative/powercli/kn-pcli-datastore-usage-email/handler.ps1 @@ -0,0 +1,216 @@ +Function Process-Init { + [CmdletBinding()] + param() + Write-Host "$(Get-Date) - Processing Init`n" + + try { + $jsonSecrets = ${env:DATASTORE_SECRET} | ConvertFrom-Json + } + catch { + throw "`nK8s secret `$env:DATASTORE_SECRET does not look to be defined" + } + + # Extract all tag secrets for ease of use in function + $VCENTER_SERVER = ${jsonSecrets}.VCENTER_SERVER + $VCENTER_USERNAME = ${jsonSecrets}.VCENTER_USERNAME + $VCENTER_PASSWORD = ${jsonSecrets}.VCENTER_PASSWORD + $VCENTER_CERTIFICATE_ACTION = ${jsonSecrets}.VCENTER_CERTIFICATE_ACTION + + # Configure TLS 1.2/1.3 support as this is required for latest vSphere release + [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor [System.Net.SecurityProtocolType]::Tls12 -bor [System.Net.SecurityProtocolType]::Tls13 + + Write-Host "$(Get-Date) - Configuring PowerCLI Configuration Settings`n" + Set-PowerCLIConfiguration -InvalidCertificateAction:${VCENTER_CERTIFICATE_ACTION} -ParticipateInCeip:$true -Confirm:$false + + Write-Host "$(Get-Date) - Connecting to vCenter Server $VCENTER_SERVER`n" + + try { + Connect-VIServer -Server $VCENTER_SERVER -User $VCENTER_USERNAME -Password $VCENTER_PASSWORD + } + catch { + Write-Error "$(Get-Date) - ERROR: Failed to connect to vCenter Server" + throw $_ + } + + Write-Host "$(Get-Date) - Successfully connected to $VCENTER_SERVER`n" + + Write-Host "$(Get-Date) - Init Processing Completed`n" +} + +Function Process-Shutdown { + [CmdletBinding()] + param() + Write-Host "$(Get-Date) - Processing Shutdown`n" + + Write-Host "$(Get-Date) - Disconnecting from vCenter Server`n" + + try { + Disconnect-VIServer * -Confirm:$false + } + catch { + Write-Error "$(Get-Date) - Error: Failed to Disconnect from vCenter Server" + } + + Write-Host "$(Get-Date) - Shutdown Processing Completed`n" +} + +Function Process-Handler { + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=$true)][CloudNative.CloudEvents.CloudEvent]$CloudEvent + ) + + # Decode CloudEvent + try { + $cloudEventData = $cloudEvent | Read-CloudEventJsonData -Depth 10 + } + catch { + throw "`nPayload must be JSON encoded" + } + + try { + $jsonSecrets = ${env:DATASTORE_SECRET} | ConvertFrom-Json + } + catch { + throw "`nK8s secret `$env:DATASTORE_SECRET does not look to be defined" + } + + $alarmName = $($cloudEventData.Alarm.Name -replace "\n"," ") + $datastoreName = $($cloudEventData.Ds.Name) + $alarmStatus = $($cloudEventData.To) + $vcenter = $($cloudEvent.source -replace "/sdk","") + $datacenter = $($cloudEventData.Datacenter.Name) + $alarmToMonitor = ${jsonSecrets}.VC_ALARM_NAME + $datastoresToMonitor = ${jsonSecrets}.DATASTORE_NAMES + + if (${env:FUNCTION_DEBUG} -eq "true") { + Write-Host "$(Get-Date) - DEBUG: Alarm Name: $alarmName" + Write-Host "$(Get-Date) - DEBUG: DS Name: $datastoreName" + Write-Host "$(Get-Date) - DEBUG: Alarm Status: $alarmStatus" + Write-Host "$(Get-Date) - DEBUG: vCenter: $vcenter" + Write-Host "$(Get-Date) - DEBUG: Data Center: $datacenter" + Write-Host "$(Get-Date) - DEBUG: Alarm to Monitor: $alarmToMonitor" + Write-Host "$(Get-Date) - DEBUG: Datastores to Monitor: $datastoresToMonitor" + } + + if( ("$alarmName" -match $($alarmToMonitor)) -and ([bool]($datastoresToMonitor -match "$datastoreName")) -and ($alarmStatus -eq "yellow" -or $alarmStatus -eq "red" -or $alarmStatus -eq "green") ) { + # Warning Email Body + if($alarmStatus -eq "yellow") { + $subject = "⚠️ $(${jsonSecrets}.EMAIL_SUBJECT) ⚠️ " + $threshold = "warning" + } elseif($alarmStatus -eq "red") { + $subject = "☢️ $(${jsonSecrets}.EMAIL_SUBJECT) ☢️ " + $threshold = "error" + } elseif($alarmStatus -eq "green") { + $subject = "$(${jsonSecrets}.EMAIL_SUBJECT)" + $threshold = "normal" + } + + $Body = "$alarmName $datastoreName has reached $threshold threshold.`r`n" + + if ( $threshold -ne "normal") { + $Body = $Body + "Please log in to $($vcenter) and ensure that everything is operating as expected.`r`n" + } + + $Body = $Body + @" + vCenter Server: $vCenter + Datacenter: $datacenter + Datastore: $datastoreName +"@ + + if (${env:FUNCTION_DEBUG} -eq "true") { + Write-Host "$(Get-Date) - DEBUG: Message Subject: $($Subject)" + Write-Host "$(Get-Date) - DEBUG: Message Body: $($Body)" + } + + $emailTo = ${jsonSecrets}.EMAIL_TO + + # If the JSON file has a custom property email field defined, find the value + # This is used to allow admins to add an email address as a custom property on a datastore for storage alarms independent of the EMAIL_TO value + if (${jsonSecrets}.DATASTORE_CUSTOM_PROP_EMAIL_TO.length -gt 0) + { + # This object has all defined custom fields in the vCenter + try { + $customFieldMgr = Get-View ($global:DefaultVIServer.ExtensionData.Content.CustomFieldsManager) + } + catch { + Write-Host "$(Get-Date) - ERROR: unable to retrieve CustomFieldsManager view`n" + throw $_ + } + + try { + $datastoreView = Get-View -ViewType Datastore -Property Name, Value -Filter @{"name"=$datastoreName} + } + catch { + Write-Host "$(Get-Date) - ERROR: unable to retrieve Datastore view view`n" + throw $_ + } + + # Build 2 hash tables for the key-value pairs in the Custom Fields Manager, one to search by Custom Field ID and one to search by Name + $customKeyLookup = @{} + $customNameLookup = @{} + $customFieldMgr.Field | ForEach-Object { + $customKeyLookup.Add($_.Key, $_.Name) + $customNameLookup.Add($_.Name, $_.Key) + } + + # This is the custom field that we're looking to pull an email address out of + $emailKey = $customNameLookup[$(${jsonSecrets}.DATASTORE_CUSTOM_PROP_EMAIL_TO)] + if(${env:FUNCTION_DEBUG} -eq "true") { + Write-Host "$(Get-Date) - DEBUG: custom prop: $(${jsonSecrets}.DATASTORE_CUSTOM_PROP_EMAIL_TO)" + Write-Host "$(Get-Date) - DEBUG: email Key: $emailKey" + } + + #If we find one, this is the email address we will add to the "To" field in the email + $addEmailAddress = "" + foreach ($row in $datastoreView.Value) { + Write-Host "$(Get-Date) - INFO: Datastore" $datastoreName "has Custom Field:" $customKeyLookup[$row.Key] "with value:" $row.Value "`n" + if ($row.Key -eq $emailKey) + { + if(${env:FUNCTION_DEBUG} -eq "true") { + write-host "$(Get-Date) - DEBUG: Found key" $emailKey "with value" $row.value + } + $addEmailAddress = $row.value + break + } + } + + if ($addEmailAddress.length -gt 0){ + $emailTo = $emailTo + $addEmailAddress + } + else { + Write-Host "$(Get-Date) - WARN: DATASTORE_CUSTOM_PROP_EMAIL_TO value '"${jsonSecrets}.DATASTORE_CUSTOM_PROP_EMAIL_TO "' found in JSON config but not found on datastore" + } + + } + + Write-Host "$(Get-Date) - Sending notification to $($emailTo) ...`n" + # If defined in the config file, send via authenticated SMTP, otherwise use standard SMTP + if (${jsonSecrets}.SMTP_PASSWORD.length -gt 0 -and ${jsonSecrets}.SMTP_USERNAME.length -gt 0) + { + $password = ConvertTo-SecureString "$(${jsonSecrets}.SMTP_PASSWORD)" -AsPlainText -Force + $credential = New-Object System.Management.Automation.PSCredential($(${jsonSecrets}.SMTP_USERNAME), $password) + try { + Send-MailMessage -From $(${jsonSecrets}.EMAIL_FROM) -to $($emailTo) -Subject $Subject -Body $Body -SmtpServer $(${jsonSecrets}.SMTP_SERVER) -port $(${jsonSecrets}.SMTP_PORT) -UseSsl -Credential $credential -Encoding UTF32 + } + catch { + Write-Host "$(Get-Date) - ERROR: Unable to send email message`n" + throw $_ + } + } + else + { + try { + Send-MailMessage -From $(${jsonSecrets}.EMAIL_FROM) -to $($emailTo) -Subject $Subject -Body $Body -SmtpServer $(${jsonSecrets}.SMTP_SERVER) -port $(${jsonSecrets}.SMTP_PORT) -Encoding UTF32 + } + catch { + Write-Host "$(Get-Date) - ERROR: Unable to send email message`n" + throw $_ + } + } + } + + Write-Host "$(Get-Date) - datastore-usage-email operation complete ...`n" + + Write-Host "$(Get-Date) - Handler Processing Completed ...`n" +} diff --git a/examples/knative/powercli/kn-pcli-datastore-usage-email/test/docker-test-env-variable b/examples/knative/powercli/kn-pcli-datastore-usage-email/test/docker-test-env-variable new file mode 100644 index 00000000..ef9d9dd7 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-datastore-usage-email/test/docker-test-env-variable @@ -0,0 +1 @@ +DATASTORE_SECRET={"VCENTER_SERVER":"FILL-ME-IN","VCENTER_USERNAME":"FILL-ME-IN","VCENTER_PASSWORD":"FILL-ME-IN","VCENTER_CERTIFICATE_ACTION":"Fail","VC_ALARM_NAME":"Datastore usage on disk","DATASTORE_NAMES":["Datastore1","Datastore2"],"SMTP_SERVER":"FILL-ME-IN","SMTP_PORT":"FILL-ME-IN","SMTP_USERNAME":"","SMTP_PASSWORD":"","EMAIL_SUBJECT":"[VMC Datastore Notification Alarm]","EMAIL_TO":["FILL-ME-IN"],"EMAIL_FROM":"FILL-ME-IN","DATASTORE_CUSTOM_PROP_EMAIL_TO":""} \ No newline at end of file diff --git a/examples/knative/powercli/kn-pcli-datastore-usage-email/test/send-cloudevent-test.ps1 b/examples/knative/powercli/kn-pcli-datastore-usage-email/test/send-cloudevent-test.ps1 new file mode 100644 index 00000000..5f51466a --- /dev/null +++ b/examples/knative/powercli/kn-pcli-datastore-usage-email/test/send-cloudevent-test.ps1 @@ -0,0 +1,32 @@ +# The ce-subject value should match the event router subject in function.yaml +$subject = "AlarmStatusChangedEvent" +$payloadPath = "./test-payload.json" + +if ( $args.Count -gt 0 ) { + if ( Test-Path $args[0] ) { + $payloadPath = $args[0] + } + else { + Write-Host "$(Get-Date) - ERROR: Invalid path"$args[0]"`n" + exit + } + + if ( $args.Count -gt 1 ) { + $subject = $args[1] + } +} + +$headers = @{ + "Content-Type" = "application/json"; + "ce-specversion" = "1.0"; + "ce-id" = "id-123"; + "ce-source" = "source-123"; + "ce-type" = "com.vmware.event.router/event"; + "ce-subject" = $($subject); +} +$body = Get-Content -Raw -Path $payloadPath + +Write-Host "Testing Function ..." +Invoke-WebRequest -Uri http://localhost:8080 -Method POST -Headers $headers -Body $body + +Write-host "See docker container console for output" \ No newline at end of file diff --git a/examples/knative/powercli/kn-pcli-datastore-usage-email/test/send-cloudevent-test.sh b/examples/knative/powercli/kn-pcli-datastore-usage-email/test/send-cloudevent-test.sh new file mode 100644 index 00000000..591f9251 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-datastore-usage-email/test/send-cloudevent-test.sh @@ -0,0 +1,29 @@ +#!/bin/bash + +# The ce-subject value should match the event router subject in function.yaml +echo "Testing Function ..." +PAYLOAD_PATH="test-payload.json" +SUBJECT="AlarmStatusChangedEvent" + +if [ $# -gt 0 ]; then + if test -f "$1"; then + PAYLOAD_PATH=$1 + else + echo "$1 not found" + exit 1 + fi + + if [ $# -gt 1 ]; then + SUBJECT=$2 + fi +fi +curl -d@$PAYLOAD_PATH \ + -H "Content-Type: application/json" \ + -H 'ce-specversion: 1.0' \ + -H 'ce-id: d70079f9-fddd-4b7f-aa76-1193f28b0611' \ + -H 'ce-source: https://vcenter.local/sdk' \ + -H 'ce-type: com.vmware.event.router/event' \ + -H 'ce-subject: '$SUBJECT \ + -X POST localhost:8080 + +echo "See docker container console for output" diff --git a/examples/knative/powercli/kn-pcli-datastore-usage-email/test/test-payload.json b/examples/knative/powercli/kn-pcli-datastore-usage-email/test/test-payload.json new file mode 100644 index 00000000..63a790e4 --- /dev/null +++ b/examples/knative/powercli/kn-pcli-datastore-usage-email/test/test-payload.json @@ -0,0 +1,50 @@ +{ + "Key": 952175, + "ChainId": 952175, + "CreatedTime": "2022-04-27T14:14:13.089999Z", + "UserName": "", + "Datacenter": { + "Name": "DataCenter1", + "Datacenter": { + "Type": "Datacenter", + "Value": "datacenter-47" + } + }, + "ComputeResource": null, + "Host": null, + "Vm": null, + "Ds": { + "Name": "ProdDatastore1", + "Datastore": { + "Type": "Datastore", + "Value": "datastore-60" + } + }, + "Net": null, + "Dvs": null, + "FullFormattedMessage": "Alarm 'Datastore usage on disk' on ProdDatstore1 changed from Gray to Yellow", + "ChangeTag": "", + "Alarm": { + "Name": "Datastore usage on disk", + "Alarm": { + "Type": "Alarm", + "Value": "alarm-7" + } + }, + "Source": { + "Name": "Datacenters", + "Entity": { + "Type": "Folder", + "Value": "group-d1" + } + }, + "Entity": { + "Name": "ProdDatastore1", + "Entity": { + "Type": "Datastore", + "Value": "datastore-60" + } + }, + "From": "gray", + "To": "yellow" + } \ No newline at end of file From 624f24c02ac75f7f5fa02b2780afccbcb09f3e92 Mon Sep 17 00:00:00 2001 From: Patrick Kremer Date: Fri, 29 Apr 2022 11:26:52 -0500 Subject: [PATCH 25/54] docs: Add datastore usage email function to website Closes #870 Signed-off-by: Patrick Kremer --- docs/site/examples.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/site/examples.md b/docs/site/examples.md index d60a7a65..dc637dbd 100644 --- a/docs/site/examples.md +++ b/docs/site/examples.md @@ -11,6 +11,15 @@ images: go: /assets/img/languages/go.png powershell: /assets/img/languages/powershell.png examples: + - title: Datastore Usage Alarm Email Notification + usecases: + - item: notification + id: kn-pcli-datastore-usage-email + description: Sends email notifications to a specified email address for datastore usage on disk alarms. Optional configuration for a per-datastore email, enabling different recipients for different datastores. + links: + - language: powercli + url: "/tree/master/examples/knative/powercli/kn-pcli-datastore-usage-email" + - title: Echo Cloud Event for Knative usecases: - item: other From 178060e2756f7ee6776370d3b617b5e0ae8155b7 Mon Sep 17 00:00:00 2001 From: Patrick Kremer Date: Tue, 3 May 2022 14:32:34 -0500 Subject: [PATCH 26/54] docs: Make concurrency values consistent Closes #876 Signed-off-by: Patrick Kremer --- examples/knative/go/kn-go-nsx-tag-sync/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/knative/go/kn-go-nsx-tag-sync/README.md b/examples/knative/go/kn-go-nsx-tag-sync/README.md index 71f1d573..efddd7f9 100644 --- a/examples/knative/go/kn-go-nsx-tag-sync/README.md +++ b/examples/knative/go/kn-go-nsx-tag-sync/README.md @@ -242,7 +242,7 @@ autoscaler will then create multiple instances of the function. ### Example -To handle up to **100 concurrent** tagging events if the default values (FIFO +To handle up to **200 concurrent** tagging events if the default values (FIFO semantics) are not appropriate: *Trigger settings:* From 1d2150c3853bb3423e5d8e1ebc9b7931716ae0bf Mon Sep 17 00:00:00 2001 From: David Bibby Date: Thu, 31 Mar 2022 22:45:44 +0100 Subject: [PATCH 27/54] docs: Improvements to appliance build process and documentation Closes: 841 Signed-off-by: David Bibby --- build.sh | 2 +- docs/kb/contribute-appliance.md | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/build.sh b/build.sh index 8de6aa63..7becb27a 100755 --- a/build.sh +++ b/build.sh @@ -16,7 +16,7 @@ if ! hash jq 2>/dev/null; then exit 1 fi -if [[ ! -z $(git status -s) ]]; then +if [[ ! -z $(git status -s | grep -vE 'photon-builder.json|test/.*\.sh') ]]; then echo "Dirty Git repository, please clean up any untracked files or commit them before building" exit fi diff --git a/docs/kb/contribute-appliance.md b/docs/kb/contribute-appliance.md index 2d8a2517..6eee0110 100644 --- a/docs/kb/contribute-appliance.md +++ b/docs/kb/contribute-appliance.md @@ -13,8 +13,9 @@ cta: ## Requirements -* 4 vCPU and 8GB of memory for VMware Event Broker Appliance +* 6 vCPU and 8GB of memory for VMware Event Broker Appliance * ESXi host v6.7 or greater + * Datastore with at least 60GB of free space * SSH must be enabled on the host * Enable GuestIPHack on the host by running `esxcli system settings advanced set -o /Net/GuestIPHack -i 1` * The following must be installed on your development machine: @@ -23,6 +24,7 @@ cta: * [Packer](https://www.packer.io/intro/getting-started/install.html){:target="_blank"} (v1.6.3 or greater) * [jq](https://stedolan.github.io/jq/){:target="_blank"} * Development machine must have the firewall disabled for the duration of the build +> **Note:** It has been seen that Packer can bind to an IPv6 on the development machine - you may wish to disable IPv6! * Development machine must be on the same L2 subnet as the target VM portgroup defined in `builder_host_portgroup` below From b2981aeb34d2cbe79e4357cd930221ea7e428e39 Mon Sep 17 00:00:00 2001 From: Patrick Kremer Date: Tue, 3 May 2022 16:53:20 -0500 Subject: [PATCH 28/54] docs: Add Docker daemon start/stop instructions Closes #882 Signed-off-by: Patrick Kremer --- docs/kb/advanced-certificates.md | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/docs/kb/advanced-certificates.md b/docs/kb/advanced-certificates.md index 8dfee9c4..c066ac57 100644 --- a/docs/kb/advanced-certificates.md +++ b/docs/kb/advanced-certificates.md @@ -209,12 +209,18 @@ This section demonstrates installation of the Let's Encrypt Certbot Docker image ### Steps -Step 1 - Pull the Certbot Docker image +Step 1 - Start the Docker daemon. VEBA uses containerd for its container runtime - the Docker daemon is disabled by default. + +```console +systemctl start docker +``` + +Step 2 - Pull the Certbot Docker image ```console docker pull certbot/certbot ``` -Step 2 - Run certbot. For the `-d` (domain) switch, use your VEBA FQDN. You will be prompted for an e-mail address as well as some yes/no questions. +Step 3 - Run certbot. For the `-d` (domain) switch, use your VEBA FQDN. You will be prompted for an e-mail address as well as some yes/no questions. ```console docker run -it --rm --name certbot -v "/etc/letsencrypt:/etc/letsencrypt" ` -v "/var/lib/letsencrypt:/var/lib/letsencrypt" ` @@ -263,9 +269,9 @@ value(s) you've just added. Press Enter to Continue ``` -Step 3 - Using your public DNS provider's tools, configure the required TXT record as prompted in Step 2. +Step 4 - Using your public DNS provider's tools, configure the required TXT record as prompted in Step 2. -Step 4 - Press Enter to continue. If you have configured DNS properly, the certificate PEM files will be saved in the location specified. +Step 5 - Press Enter to continue. If you have configured DNS properly, the certificate PEM files will be saved in the location specified. ``` - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -281,6 +287,12 @@ NEXT STEPS: - This certificate will not be renewed automatically. Autorenewal of --manual certificates requires the use of an authentication hook script (--manual-auth-hook) but one was not provided. To renew this certificate, repeat this same certbot command before the certificate's expiry date. ``` -Step 5 - Install the certificate - follow the instructions starting with step 2 of [Replacing an Existing Cert on VEBA](#replacestep2). Note from the output above that the public key file is named `fullchain.pem` - you will need to pass this value for the `--cert` argument when creating the Kubernetes TLS certificates. +Step 6 - Install the certificate - follow the instructions starting with step 2 of [Replacing an Existing Cert on VEBA](#replacestep2). Note from the output above that the public key file is named `fullchain.pem` - you will need to pass this value for the `--cert` argument when creating the Kubernetes TLS certificates. + +Step 7 - Stop the Docker daemon + +```console +systemctl stop docker +``` -Step 6 (optional) - If you want to automate renewals, this is an excellent blog on configuring [automated certificate renewals](https://chariotsolutions.com/blog/post/automating-lets-encrypt-certificate-renewal-using-dns-challenge-type/) using DNS validation. \ No newline at end of file +Step 8 (optional) - If you want to automate renewals, this is an excellent blog on configuring [automated certificate renewals](https://chariotsolutions.com/blog/post/automating-lets-encrypt-certificate-renewal-using-dns-challenge-type/) using DNS validation. \ No newline at end of file From a045117a8ce9be375be2bd8601c19fab3f132d10 Mon Sep 17 00:00:00 2001 From: David Bibby Date: Thu, 12 May 2022 15:38:37 +0100 Subject: [PATCH 29/54] fix: update in-memory-channel.yaml to knative/eventing v1.1.0 release Closes: #886 Signed-off-by: David Bibby --- .../kn-ps-slack-vsphere-alarm/README.md | 2 +- .../in-memory-channel.yaml | 1179 +++++++++++++---- 2 files changed, 887 insertions(+), 294 deletions(-) diff --git a/examples/knative/powershell/kn-ps-slack-vsphere-alarm/README.md b/examples/knative/powershell/kn-ps-slack-vsphere-alarm/README.md index 73c65561..947f3473 100644 --- a/examples/knative/powershell/kn-ps-slack-vsphere-alarm/README.md +++ b/examples/knative/powershell/kn-ps-slack-vsphere-alarm/README.md @@ -183,7 +183,7 @@ kubectl apply -f in-memory-channel.yaml ## Deploy the Function and its Components -Edit the `parallel.yaml` file with the name of the container image from Step 1 **if you made any changes to the function code**. If not, the default VMware container image will suffice and you can keep the defaults. +Edit the `function.yaml` file with the name of the container image from Step 1 **if you made any changes to the function code**. If not, the default VMware container image will suffice and you can keep the defaults. Deploy the functions to the VMware Event Broker Appliance (VEBA). diff --git a/examples/knative/powershell/kn-ps-slack-vsphere-alarm/in-memory-channel.yaml b/examples/knative/powershell/kn-ps-slack-vsphere-alarm/in-memory-channel.yaml index 0950e2b7..fda13b3d 100644 --- a/examples/knative/powershell/kn-ps-slack-vsphere-alarm/in-memory-channel.yaml +++ b/examples/knative/powershell/kn-ps-slack-vsphere-alarm/in-memory-channel.yaml @@ -1,3 +1,122 @@ +# Copyright 2021 The Knative Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v1 +kind: ServiceAccount +metadata: + name: imc-controller + namespace: knative-eventing + labels: + eventing.knative.dev/release: "v1.1.0" + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: imc-controller + labels: + eventing.knative.dev/release: "v1.1.0" + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing +subjects: + - kind: ServiceAccount + name: imc-controller + namespace: knative-eventing +roleRef: + kind: ClusterRole + name: imc-controller + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + namespace: knative-eventing + name: imc-controller + labels: + eventing.knative.dev/release: "v1.1.0" + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing +subjects: + - kind: ServiceAccount + name: imc-controller + namespace: knative-eventing +roleRef: + kind: Role + name: knative-inmemorychannel-webhook + apiGroup: rbac.authorization.k8s.io +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: imc-controller-resolver + labels: + eventing.knative.dev/release: "v1.1.0" + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing +subjects: + - kind: ServiceAccount + name: imc-controller + namespace: knative-eventing +roleRef: + kind: ClusterRole + name: addressable-resolver + apiGroup: rbac.authorization.k8s.io + +--- +# Copyright 2021 The Knative Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v1 +kind: ServiceAccount +metadata: + name: imc-dispatcher + namespace: knative-eventing + labels: + eventing.knative.dev/release: "v1.1.0" + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: imc-dispatcher + labels: + eventing.knative.dev/release: "v1.1.0" + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing +subjects: + - kind: ServiceAccount + name: imc-dispatcher + namespace: knative-eventing +roleRef: + kind: ClusterRole + name: imc-dispatcher + apiGroup: rbac.authorization.k8s.io + +--- # Copyright 2020 The Knative Authors # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -18,11 +137,659 @@ metadata: name: config-imc-event-dispatcher namespace: knative-eventing labels: - eventing.knative.dev/release: "v0.20.0" + eventing.knative.dev/release: "v1.1.0" + app.kubernetes.io/component: imc-controller + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing data: MaxIdleConnections: "1000" MaxIdleConnectionsPerHost: "100" +--- +# Copyright 2021 The Knative Authors + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v1 +kind: ConfigMap +metadata: + name: config-observability + namespace: knative-eventing + labels: + eventing.knative.dev/release: "v1.1.0" + knative.dev/config-propagation: original + knative.dev/config-category: eventing + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing + annotations: + knative.dev/example-checksum: "f46cf09d" +data: + _example: | + ################################ + # # + # EXAMPLE CONFIGURATION # + # # + ################################ + + # This block is not actually functional configuration, + # but serves to illustrate the available configuration + # options and document them in a way that is accessible + # to users that `kubectl edit` this config map. + # + # These sample configuration options may be copied out of + # this example block and unindented to be in the data block + # to actually change the configuration. + + # metrics.backend-destination field specifies the system metrics destination. + # It supports either prometheus (the default) or stackdriver. + # Note: Using stackdriver will incur additional charges + metrics.backend-destination: prometheus + + # metrics.request-metrics-backend-destination specifies the request metrics + # destination. If non-empty, it enables queue proxy to send request metrics. + # Currently supported values: prometheus, stackdriver. + metrics.request-metrics-backend-destination: prometheus + + # metrics.stackdriver-project-id field specifies the stackdriver project ID. This + # field is optional. When running on GCE, application default credentials will be + # used if this field is not provided. + metrics.stackdriver-project-id: "" + + # metrics.allow-stackdriver-custom-metrics indicates whether it is allowed to send metrics to + # Stackdriver using "global" resource type and custom metric type if the + # metrics are not supported by "knative_broker", "knative_trigger", and "knative_source" resource types. + # Setting this flag to "true" could cause extra Stackdriver charge. + # If metrics.backend-destination is not Stackdriver, this is ignored. + metrics.allow-stackdriver-custom-metrics: "false" + + # profiling.enable indicates whether it is allowed to retrieve runtime profiling data from + # the pods via an HTTP server in the format expected by the pprof visualization tool. When + # enabled, the Knative Eventing pods expose the profiling data on an alternate HTTP port 8008. + # The HTTP context root for profiling is then /debug/pprof/. + profiling.enable: "false" + + # sink-event-error-reporting.enable whether the adapter reports a kube event to the CRD indicating + # a failure to send a cloud event to the sink. + sink-event-error-reporting.enable: "false" + +--- +# Copyright 2021 The Knative Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: v1 +kind: ConfigMap +metadata: + name: config-tracing + namespace: knative-eventing + labels: + eventing.knative.dev/release: "v1.1.0" + knative.dev/config-propagation: original + knative.dev/config-category: eventing + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing + annotations: + knative.dev/example-checksum: "c8f8c47b" +data: + _example: | + ################################ + # # + # EXAMPLE CONFIGURATION # + # # + ################################ + # This block is not actually functional configuration, + # but serves to illustrate the available configuration + # options and document them in a way that is accessible + # to users that `kubectl edit` this config map. + # + # These sample configuration options may be copied out of + # this example block and unindented to be in the data block + # to actually change the configuration. + # + # This may be "zipkin" or "none", the default is "none" + backend: "none" + + # URL to zipkin collector where traces are sent. + # This must be specified when backend is "zipkin" + zipkin-endpoint: "http://zipkin.istio-system.svc.cluster.local:9411/api/v2/spans" + + # Enable zipkin debug mode. This allows all spans to be sent to the server + # bypassing sampling. + debug: "false" + + # Percentage (0-1) of requests to trace + sample-rate: "0.1" + +--- +# Copyright 2019 The Knative Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: imc-controller + namespace: knative-eventing + labels: + eventing.knative.dev/release: "v1.1.0" + knative.dev/high-availability: "true" + app.kubernetes.io/component: imc-controller + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing +spec: + selector: + matchLabels: &labels + messaging.knative.dev/channel: in-memory-channel + messaging.knative.dev/role: controller + template: + metadata: + labels: + !!merge <<: *labels + app.kubernetes.io/component: imc-controller + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchLabels: *labels + topologyKey: kubernetes.io/hostname + weight: 100 + serviceAccountName: imc-controller + enableServiceLinks: false + containers: + - name: controller + image: gcr.io/knative-releases/knative.dev/eventing/cmd/in_memory/channel_controller@sha256:4fe048c1454e129dda9b32abac4da7bfdc7ca1b53316169e1748873095f5f565 + env: + - name: WEBHOOK_NAME + value: inmemorychannel-webhook + - name: WEBHOOK_PORT + value: "8443" + - name: CONFIG_LOGGING_NAME + value: config-logging + - name: CONFIG_OBSERVABILITY_NAME + value: config-observability + - name: METRICS_DOMAIN + value: knative.dev/inmemorychannel-controller + - name: SYSTEM_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: DISPATCHER_IMAGE + value: gcr.io/knative-releases/knative.dev/eventing/cmd/in_memory/channel_dispatcher@sha256:f68e7b2f99b590e60f034d91b771eef02ff9303da749da4aec389f4c8286d903 + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + capabilities: + drop: + - all + ports: + - name: metrics + containerPort: 9090 + - name: profiling + containerPort: 8008 + - name: https-webhook + containerPort: 8443 + readinessProbe: &probe + periodSeconds: 1 + httpGet: + scheme: HTTPS + port: 8443 + httpHeaders: + - name: k-kubelet-probe + value: "webhook" + livenessProbe: + !!merge <<: *probe + initialDelaySeconds: 20 + # Our webhook should gracefully terminate by lame ducking first, set this to a sufficiently + # high value that we respect whatever value it has configured for the lame duck grace period. + terminationGracePeriodSeconds: 300 +--- +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/component: imc-controller + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing + eventing.knative.dev/release: "v1.1.0" + name: inmemorychannel-webhook + namespace: knative-eventing +spec: + ports: + - name: https-webhook + port: 443 + targetPort: 8443 + - name: http-metrics + port: 9090 + targetPort: 9090 + - name: http-profiling + port: 8008 + targetPort: 8008 + selector: + messaging.knative.dev/channel: in-memory-channel + messaging.knative.dev/role: controller + +--- +# Copyright 2019 The Knative Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +apiVersion: v1 +kind: Service +metadata: + name: imc-dispatcher + namespace: knative-eventing + labels: + eventing.knative.dev/release: "v1.1.0" + messaging.knative.dev/channel: in-memory-channel + messaging.knative.dev/role: dispatcher + app.kubernetes.io/component: imc-dispatcher + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing +spec: + selector: + messaging.knative.dev/channel: in-memory-channel + messaging.knative.dev/role: dispatcher + ports: + - name: http-dispatcher + port: 80 + protocol: TCP + targetPort: 8080 + - name: http-metrics + port: 9090 + targetPort: 9090 + +--- +# Copyright 2019 The Knative Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: imc-dispatcher + namespace: knative-eventing + labels: + eventing.knative.dev/release: "v1.1.0" + knative.dev/high-availability: "true" + app.kubernetes.io/component: imc-dispatcher + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing +spec: + selector: + matchLabels: &labels + messaging.knative.dev/channel: in-memory-channel + messaging.knative.dev/role: dispatcher + template: + metadata: + labels: + !!merge <<: *labels + app.kubernetes.io/component: imc-dispatcher + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing + spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchLabels: *labels + topologyKey: kubernetes.io/hostname + weight: 100 + serviceAccountName: imc-dispatcher + enableServiceLinks: false + containers: + - name: dispatcher + image: gcr.io/knative-releases/knative.dev/eventing/cmd/in_memory/channel_dispatcher@sha256:f68e7b2f99b590e60f034d91b771eef02ff9303da749da4aec389f4c8286d903 + readinessProbe: &probe + failureThreshold: 3 + httpGet: + path: /healthz + port: 8080 + scheme: HTTP + periodSeconds: 2 + successThreshold: 1 + timeoutSeconds: 1 + livenessProbe: + !!merge <<: *probe + initialDelaySeconds: 5 + env: + - name: CONFIG_LOGGING_NAME + value: config-logging + - name: CONFIG_OBSERVABILITY_NAME + value: config-observability + - name: METRICS_DOMAIN + value: knative.dev/inmemorychannel-dispatcher + - name: SYSTEM_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: CONTAINER_NAME + value: dispatcher + - name: MAX_IDLE_CONNS + value: "1000" + - name: MAX_IDLE_CONNS_PER_HOST + value: "1000" + ports: + - containerPort: 8080 + name: http + protocol: TCP + - containerPort: 9090 + name: metrics + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + runAsNonRoot: true + capabilities: + drop: + - all + +--- +# Copyright 2019 The Knative Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: inmemorychannels.messaging.knative.dev + labels: + eventing.knative.dev/release: "v1.1.0" + knative.dev/crd-install: "true" + messaging.knative.dev/subscribable: "true" + duck.knative.dev/addressable: "true" + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing +spec: + group: messaging.knative.dev + versions: + - name: v1 + served: true + storage: true + subresources: + status: {} + schema: + openAPIV3Schema: + description: 'InMemoryChannel is a resource representing an in memory channel' + type: object + properties: + spec: + description: Spec defines the desired state of the Channel. + type: object + properties: + delivery: + description: DeliverySpec contains the default delivery spec for each subscription to this Channelable. Each subscription delivery spec, if any, overrides this global delivery spec. + type: object + properties: + backoffDelay: + description: 'BackoffDelay is the delay before retrying. More information on Duration format: - https://www.iso.org/iso-8601-date-and-time-format.html - https://en.wikipedia.org/wiki/ISO_8601 For linear policy, backoff delay is backoffDelay*. For exponential policy, backoff delay is backoffDelay*2^.' + type: string + backoffPolicy: + description: BackoffPolicy is the retry backoff policy (linear, exponential). + type: string + deadLetterSink: + description: DeadLetterSink is the sink receiving event that could not be sent to a destination. + type: object + properties: + ref: + description: Ref points to an Addressable. + type: object + properties: + apiVersion: + description: API version of the referent. + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ This is optional field, it gets defaulted to the object holding it if left out.' + type: string + uri: + description: URI can be an absolute URL(non-empty scheme and non-empty host) pointing to the target or a relative URI. Relative URIs will be resolved using the base URI retrieved from Ref. + type: string + retry: + description: Retry is the minimum number of retries the sender should attempt when sending an event before moving it to the dead letter sink. + type: integer + format: int32 + x-kubernetes-preserve-unknown-fields: true # This is necessary to enable the experimental feature delivery-timeout + subscribers: + description: This is the list of subscriptions for this subscribable. + type: array + items: + type: object + properties: + delivery: + description: DeliverySpec contains options controlling the event delivery + type: object + properties: + backoffDelay: + description: 'BackoffDelay is the delay before retrying. More information on Duration format: - https://www.iso.org/iso-8601-date-and-time-format.html - https://en.wikipedia.org/wiki/ISO_8601 For linear policy, backoff delay is backoffDelay*. For exponential policy, backoff delay is backoffDelay*2^.' + type: string + backoffPolicy: + description: BackoffPolicy is the retry backoff policy (linear, exponential). + type: string + deadLetterSink: + description: DeadLetterSink is the sink receiving event that could not be sent to a destination. + type: object + properties: + ref: + description: Ref points to an Addressable. + type: object + properties: + apiVersion: + description: API version of the referent. + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ This is optional field, it gets defaulted to the object holding it if left out.' + type: string + uri: + description: URI can be an absolute URL(non-empty scheme and non-empty host) pointing to the target or a relative URI. Relative URIs will be resolved using the base URI retrieved from Ref. + type: string + retry: + description: Retry is the minimum number of retries the sender should attempt when sending an event before moving it to the dead letter sink. + type: integer + format: int32 + x-kubernetes-preserve-unknown-fields: true # This is necessary to enable the experimental feature + generation: + description: Generation of the origin of the subscriber with uid:UID. + type: integer + format: int64 + replyUri: + description: ReplyURI is the endpoint for the reply + type: string + subscriberUri: + description: SubscriberURI is the endpoint for the subscriber + type: string + uid: + description: UID is used to understand the origin of the subscriber. + type: string + status: + description: Status represents the current state of the Channel. This data may be out of date. + type: object + properties: + address: + type: object + properties: + url: + type: string + annotations: + description: Annotations is additional Status fields for the Resource to save some additional State as well as convey more information to the user. This is roughly akin to Annotations on any k8s resource, just the reconciler conveying richer information outwards. + type: object + x-kubernetes-preserve-unknown-fields: true + conditions: + description: Conditions the latest available observations of a resource's current state. + type: array + items: + type: object + required: + - type + - status + properties: + lastTransitionTime: + description: LastTransitionTime is the last time the condition transitioned from one status to another. We use VolatileTime in place of metav1.Time to exclude this from creating equality.Semantic differences (all other things held constant). + type: string + message: + description: A human readable message indicating details about the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + severity: + description: Severity with which to treat failures of this type of condition. When this is not specified, it defaults to Error. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of condition. + type: string + deadLetterChannel: + description: DeadLetterChannel is a KReference and is set by the channel when it supports native error handling via a channel Failed messages are delivered here. + type: object + properties: + apiVersion: + description: API version of the referent. + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ This is optional field, it gets defaulted to the object holding it if left out.' + type: string + deadLetterSinkUri: + description: DeadLetterSinkURI is the resolved URI of the dead letter ref if one is specified in the Spec.Delivery. + type: string + observedGeneration: + description: ObservedGeneration is the 'Generation' of the Service that was last processed by the controller. + type: integer + format: int64 + subscribers: + description: This is the list of subscription's statuses for this channel. + type: array + items: + type: object + properties: + message: + description: A human readable message indicating details of Ready status. + type: string + observedGeneration: + description: Generation of the origin of the subscriber with uid:UID. + type: integer + format: int64 + ready: + description: Status of the subscriber. + type: string + uid: + description: UID is used to understand the origin of the subscriber. + type: string + additionalPrinterColumns: + - name: URL + type: string + jsonPath: .status.address.url + - name: Age + type: date + jsonPath: .metadata.creationTimestamp + - name: Ready + type: string + jsonPath: ".status.conditions[?(@.type==\"Ready\")].status" + - name: Reason + type: string + jsonPath: ".status.conditions[?(@.type==\"Ready\")].reason" + names: + kind: InMemoryChannel + plural: inmemorychannels + singular: inmemorychannel + categories: + - all + - knative + - messaging + - channel + shortNames: + - imc + scope: Namespaced + --- # Copyright 2019 The Knative Authors # @@ -43,8 +810,10 @@ kind: ClusterRole metadata: name: imc-addressable-resolver labels: - eventing.knative.dev/release: "v0.20.0" + eventing.knative.dev/release: "v1.1.0" duck.knative.dev/addressable: "true" + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing # Do not use this role directly. These rules will be added to the "addressable-resolver" role. rules: - apiGroups: @@ -77,8 +846,10 @@ kind: ClusterRole metadata: name: imc-channelable-manipulator labels: - eventing.knative.dev/release: "v0.20.0" + eventing.knative.dev/release: "v1.1.0" duck.knative.dev/channelable: "true" + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing # Do not use this role directly. These rules will be added to the "channelable-manipulator" role. rules: - apiGroups: @@ -93,6 +864,7 @@ rules: - watch - update - patch + - delete --- # Copyright 2019 The Knative Authors @@ -114,7 +886,9 @@ kind: ClusterRole metadata: name: imc-controller labels: - eventing.knative.dev/release: "v0.20.0" + eventing.knative.dev/release: "v1.1.0" + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing rules: - apiGroups: - messaging.knative.dev @@ -132,6 +906,14 @@ rules: - inmemorychannels/finalizers verbs: - update + - apiGroups: + - messaging.knative.dev + resources: + - inmemorychannels/finalizers + - inmemorychannels/status + - inmemorychannels + verbs: + - patch - apiGroups: - "" resources: @@ -190,28 +972,39 @@ rules: resources: - leases verbs: *everything - ---- -# Copyright 2019 The Knative Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -apiVersion: v1 -kind: ServiceAccount -metadata: - name: imc-controller - namespace: knative-eventing - labels: - eventing.knative.dev/release: "v0.20.0" + # For actually registering our webhook. + - apiGroups: + - "admissionregistration.k8s.io" + resources: + - "mutatingwebhookconfigurations" + - "validatingwebhookconfigurations" + verbs: &everything + - "get" + - "list" + - "create" + - "update" + - "delete" + - "patch" + - "watch" + # For manipulating certs into secrets. + - apiGroups: + - "" + resources: + - "namespaces" + verbs: + - "get" + - "create" + - "update" + - "list" + - "watch" + - "patch" + # finalizers are needed for the owner reference of the webhook + - apiGroups: + - "" + resources: + - "namespaces/finalizers" + verbs: + - "update" --- # Copyright 2019 The Knative Authors @@ -232,7 +1025,9 @@ kind: ClusterRole metadata: name: imc-dispatcher labels: - eventing.knative.dev/release: "v0.20.0" + eventing.knative.dev/release: "v1.1.0" + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing rules: - apiGroups: - messaging.knative.dev @@ -281,39 +1076,6 @@ rules: - update - patch ---- -# Copyright 2019 The Knative Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -apiVersion: v1 -kind: Service -metadata: - name: imc-dispatcher - namespace: knative-eventing - labels: - eventing.knative.dev/release: "v0.20.0" - messaging.knative.dev/channel: in-memory-channel - messaging.knative.dev/role: dispatcher -spec: - selector: - messaging.knative.dev/channel: in-memory-channel - messaging.knative.dev/role: dispatcher - ports: - - name: http-dispatcher - port: 80 - protocol: TCP - targetPort: 8080 - --- # Copyright 2020 The Knative Authors # @@ -329,51 +1091,37 @@ spec: # See the License for the specific language governing permissions and # limitations under the License. -apiVersion: v1 -kind: ServiceAccount -metadata: - name: imc-dispatcher - namespace: knative-eventing - labels: - eventing.knative.dev/release: "v0.20.0" - ---- -# Copyright 2019 The Knative Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding +kind: Role metadata: - name: imc-controller + namespace: knative-eventing + name: knative-inmemorychannel-webhook labels: - eventing.knative.dev/release: "v0.20.0" -subjects: - - kind: ServiceAccount - name: imc-controller - namespace: knative-eventing -roleRef: - kind: ClusterRole - name: imc-controller - apiGroup: rbac.authorization.k8s.io + eventing.knative.dev/release: "v1.1.0" + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing +rules: + # For manipulating certs into secrets. + - apiGroups: + - "" + resources: + - "secrets" + verbs: + - "get" + - "create" + - "update" + - "list" + - "watch" + - "patch" --- -# Copyright 2020 The Knative Authors +# Copyright 2021 The Knative Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -381,107 +1129,33 @@ roleRef: # See the License for the specific language governing permissions and # limitations under the License. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: imc-dispatcher - labels: - eventing.knative.dev/release: "v0.20.0" -subjects: - - kind: ServiceAccount - name: imc-dispatcher - namespace: knative-eventing -roleRef: - kind: ClusterRole - name: imc-dispatcher - apiGroup: rbac.authorization.k8s.io - ---- -# Copyright 2019 The Knative Authors -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration metadata: - name: inmemorychannels.messaging.knative.dev + name: inmemorychannel.eventing.knative.dev labels: - eventing.knative.dev/release: "v0.20.0" - knative.dev/crd-install: "true" - messaging.knative.dev/subscribable: "true" - duck.knative.dev/addressable: "true" -spec: - group: messaging.knative.dev - versions: - - &version - name: v1beta1 - served: true - storage: false - subresources: - status: {} - schema: - openAPIV3Schema: - type: object - # this is a work around so we don't need to flush out the - # schema for each version at this time - # - # see issue: https://github.com/knative/serving/issues/912 - x-kubernetes-preserve-unknown-fields: true - additionalPrinterColumns: - - name: URL - type: string - jsonPath: .status.address.url - - name: Age - type: date - jsonPath: .metadata.creationTimestamp - - name: Ready - type: string - jsonPath: ".status.conditions[?(@.type==\"Ready\")].status" - - name: Reason - type: string - jsonPath: ".status.conditions[?(@.type==\"Ready\")].reason" - - !!merge <<: *version - name: v1 - served: true - storage: true - names: - kind: InMemoryChannel - plural: inmemorychannels - singular: inmemorychannel - categories: - - all - - knative - - messaging - - channel - shortNames: - - imc - scope: Namespaced - conversion: - strategy: Webhook - webhook: - conversionReviewVersions: ["v1", "v1beta1"] - clientConfig: - service: - name: eventing-webhook - namespace: knative-eventing + eventing.knative.dev/release: "v1.1.0" + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing +webhooks: + - admissionReviewVersions: ["v1"] + clientConfig: + service: + name: inmemorychannel-webhook + namespace: knative-eventing + sideEffects: None + failurePolicy: Fail + name: inmemorychannel.eventing.knative.dev + timeoutSeconds: 10 --- -# Copyright 2019 The Knative Authors +# Copyright 2021 The Knative Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -489,60 +1163,33 @@ spec: # See the License for the specific language governing permissions and # limitations under the License. -apiVersion: apps/v1 -kind: Deployment +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration metadata: - name: imc-controller - namespace: knative-eventing + name: validation.inmemorychannel.eventing.knative.dev labels: - eventing.knative.dev/release: "v0.20.0" - knative.dev/high-availability: "true" -spec: - selector: - matchLabels: &labels - messaging.knative.dev/channel: in-memory-channel - messaging.knative.dev/role: controller - template: - metadata: - labels: *labels - spec: - serviceAccountName: imc-controller - containers: - - name: controller - image: gcr.io/knative-releases/channel_controller-3a8067a195a7aaea27173c325c9c8070@sha256:28faaeafc5dcf36cc6f7454d004fa9ec2657ee261642128d1bb47a972540ce57 - env: - - name: CONFIG_LOGGING_NAME - value: config-logging - - name: CONFIG_OBSERVABILITY_NAME - value: config-observability - - name: METRICS_DOMAIN - value: knative.dev/inmemorychannel-controller - - name: SYSTEM_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: DISPATCHER_IMAGE - value: gcr.io/knative-releases/channel_dispatcher-9c2b1af1e7c55c4005384a6b4a52bbda@sha256:93a02d57daee7f848706277a970ce18dd9afbcc6cd65195796baaa7e06d52969 - - name: POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name - securityContext: - allowPrivilegeEscalation: false - ports: - - name: metrics - containerPort: 9090 - - name: profiling - containerPort: 8008 + eventing.knative.dev/release: "v1.1.0" + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing +webhooks: + - admissionReviewVersions: ["v1"] + clientConfig: + service: + name: inmemorychannel-webhook + namespace: knative-eventing + sideEffects: None + failurePolicy: Fail + name: validation.inmemorychannel.eventing.knative.dev + timeoutSeconds: 10 --- -# Copyright 2019 The Knative Authors +# Copyright 2021 The Knative Authors # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # -# http://www.apache.org/licenses/LICENSE-2.0 +# https://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, @@ -550,69 +1197,15 @@ spec: # See the License for the specific language governing permissions and # limitations under the License. -apiVersion: apps/v1 -kind: Deployment +apiVersion: v1 +kind: Secret metadata: - name: imc-dispatcher + name: inmemorychannel-webhook-certs namespace: knative-eventing labels: - eventing.knative.dev/release: "v0.20.0" - knative.dev/high-availability: "true" -spec: - selector: - matchLabels: &labels - messaging.knative.dev/channel: in-memory-channel - messaging.knative.dev/role: dispatcher - template: - metadata: - labels: *labels - spec: - affinity: - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - podAffinityTerm: - labelSelector: - matchLabels: *labels - topologyKey: kubernetes.io/hostname - weight: 100 - serviceAccountName: imc-dispatcher - containers: - - name: dispatcher - image: gcr.io/knative-releases/channel_dispatcher-9c2b1af1e7c55c4005384a6b4a52bbda@sha256:93a02d57daee7f848706277a970ce18dd9afbcc6cd65195796baaa7e06d52969 - readinessProbe: &probe - failureThreshold: 3 - httpGet: - path: /healthz - port: 8080 - scheme: HTTP - periodSeconds: 2 - successThreshold: 1 - timeoutSeconds: 1 - livenessProbe: - !!merge <<: *probe - initialDelaySeconds: 5 - env: - - name: CONFIG_LOGGING_NAME - value: config-logging - - name: CONFIG_OBSERVABILITY_NAME - value: config-observability - - name: METRICS_DOMAIN - value: knative.dev/inmemorychannel-dispatcher - - name: SYSTEM_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: CONTAINER_NAME - value: dispatcher - ports: - - containerPort: 8080 - name: http - protocol: TCP - - containerPort: 9090 - name: metrics + eventing.knative.dev/release: "v1.1.0" + app.kubernetes.io/version: "1.1.0" + app.kubernetes.io/name: knative-eventing +# The data is populated at install time. --- From 0d710e8369756a232a06ab6def22374a13fd2016 Mon Sep 17 00:00:00 2001 From: David Bibby Date: Thu, 31 Mar 2022 22:41:34 +0100 Subject: [PATCH 30/54] fix: correctly handle quoted ovf properties Closes: #824 Signed-off-by: David Bibby --- .gitignore | 5 ++ files/getOvfProperty.py | 29 ++++--- files/setup-02-proxy.sh | 4 +- files/setup.sh | 4 +- files/test_getOvfProperty.py | 158 +++++++++++++++++++++++++++++++++++ manual/photon.xml.template | 4 +- 6 files changed, 188 insertions(+), 16 deletions(-) create mode 100644 files/test_getOvfProperty.py diff --git a/.gitignore b/.gitignore index c6782593..850302c3 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,8 @@ checkpoints/ # ignore RELEASE-specific CHANGELOG RELEASE_CHANGELOG.md + +# ignore python bytecode artifacts +*.pyc +*.pyo +__pycache__ \ No newline at end of file diff --git a/files/getOvfProperty.py b/files/getOvfProperty.py index 69505a50..c859349a 100755 --- a/files/getOvfProperty.py +++ b/files/getOvfProperty.py @@ -1,10 +1,13 @@ #!/usr/bin/env python3 -import subprocess +from subprocess import Popen, PIPE import sys from xml.dom.minidom import parseString +import xml.parsers.expat +import re -ovfenv_cmd="/usr/bin/vmtoolsd --cmd 'info-get guestinfo.ovfEnv'" + +ovfenv_cmd = "/usr/bin/vmtoolsd --cmd 'info-get guestinfo.ovfEnv'" def debug(s): @@ -17,16 +20,16 @@ def get_ovf_properties(): Return a dict of OVF properties in the ovfenv """ properties = {} - xml_parts = subprocess.Popen(ovfenv_cmd, shell=True, - stdout=subprocess.PIPE).stdout.read() + xml_parts = Popen(ovfenv_cmd, shell=True, + stdout=PIPE).stdout.read() try: raw_data = parseString(xml_parts) except xml.parsers.expat.ExpatError as err: - debug(e) + debug(xml.parsers.expat.ErrorString(err.code)) sys.exit(1) for property in raw_data.getElementsByTagName('Property'): - key, value = [ property.attributes['oe:key'].value, - property.attributes['oe:value'].value ] + key, value = [property.attributes['oe:key'].value, + property.attributes['oe:value'].value] properties[key] = value return properties @@ -35,18 +38,22 @@ def main(argv): if len(argv) != 1: debug('usage: getOvfProperty.py > ${PROXY_CONF} + echo "HTTP_PROXY='${HTTP_PROXY_URL}'" >> ${PROXY_CONF} sed -i "/^\[Install\]/i Environment=HTTP_PROXY=${HTTP_PROXY_URL}" ${CONTAINERD_CONF} else echo -e "\e[91mInvalid HTTP Proxy URL supplied" > /dev/console @@ -49,7 +49,7 @@ if [ -n "${HTTP_PROXY}" ] || [ -n "${HTTPS_PROXY}" ]; then else HTTPS_PROXY_URL="${HTTPS_PROXY_PROTOCOL}://${HTTPS_PROXY_SERVER_PORT}" fi - echo "HTTPS_PROXY=\"${HTTPS_PROXY_URL}\"" >> ${PROXY_CONF} + echo "HTTPS_PROXY='${HTTPS_PROXY_URL}'" >> ${PROXY_CONF} sed -i "/^\[Install\]/i Environment=HTTPS_PROXY=${HTTPS_PROXY_URL}" ${CONTAINERD_CONF} else echo -e "\e[91mInvalid HTTPS Proxy URL supplied" > /dev/console diff --git a/files/setup.sh b/files/setup.sh index 661ad6d3..cbf93969 100755 --- a/files/setup.sh +++ b/files/setup.sh @@ -94,6 +94,8 @@ else ESCAPED_WEBHOOK_USERNAME=$(eval echo -n '${WEBHOOK_USERNAME}' | jq -Rs .) ESCAPED_WEBHOOK_PASSWORD=$(eval echo -n '${WEBHOOK_PASSWORD}' | jq -Rs .) + ESCAPED_PROXY_PASSWORD=$(eval echo -n '${PROXY_PASSWORD}' | jq -Rs .) + cat > /root/config/veba-config.json < ' \"" + + +def test_doublequotedvalue(capsys): + with pytest.raises(SystemExit) as pytest_wrapped_e: + getOvfProperty.main(["guestinfo.doublequotedvalue"]) + captured = capsys.readouterr() + assert pytest_wrapped_e.type == SystemExit + assert pytest_wrapped_e.value.code == 0 + assert captured.out == 'Hello World' + + +def test_singlequotedvalue(capsys): + with pytest.raises(SystemExit) as pytest_wrapped_e: + getOvfProperty.main(["guestinfo.singlequotedvalue"]) + captured = capsys.readouterr() + assert pytest_wrapped_e.type == SystemExit + assert pytest_wrapped_e.value.code == 0 + assert captured.out == 'Hello World' + + +def test_mismatchedquotesvalue(capsys): + with pytest.raises(SystemExit) as pytest_wrapped_e: + getOvfProperty.main(["guestinfo.mismatchedquotes"]) + captured = capsys.readouterr() + assert pytest_wrapped_e.type == SystemExit + assert pytest_wrapped_e.value.code == 0 + assert captured.out == '\'Hello World"' + + +def test_onequotevalue(capsys): + with pytest.raises(SystemExit) as pytest_wrapped_e: + getOvfProperty.main(["guestinfo.onequote"]) + captured = capsys.readouterr() + assert pytest_wrapped_e.type == SystemExit + assert pytest_wrapped_e.value.code == 0 + assert captured.out == 'Hello World"' + + +def test_passwordvalue(capsys): + with pytest.raises(SystemExit) as pytest_wrapped_e: + getOvfProperty.main(["guestinfo.test_password"]) + captured = capsys.readouterr() + assert pytest_wrapped_e.type == SystemExit + assert pytest_wrapped_e.value.code == 0 + assert captured.out == '"My&Quoted!Password"' + + +# Patch the ovfenv_cmd global to return a dumy ovf env XML +@pytest.fixture(autouse=True) +def ovfenv(monkeypatch): + monkeypatch.setattr(getOvfProperty, 'ovfenv_cmd', """cat << 'EOF' + + + + VMware ESXi + 7.0.3 + VMware, Inc. + en_US + + + + + + + + + + + + + + + + +EOF +""") diff --git a/manual/photon.xml.template b/manual/photon.xml.template index 1de4762a..b17c6963 100644 --- a/manual/photon.xml.template +++ b/manual/photon.xml.template @@ -37,11 +37,11 @@ Proxy Settings (optional) - Enter HTTP Proxy URL followed by the port. Example: "http://proxy.provider.com:3128" + Enter HTTP Proxy URL followed by the port. Example: http://proxy.provider.com:3128 - Enter HTTPS Proxy URL followed by the port. Example: "https://proxy.provider.com:3128" + Enter HTTPS Proxy URL followed by the port. Example: https://proxy.provider.com:3128 From 152f26e4073f71866e9c85d2d3ee6413bc05cbb3 Mon Sep 17 00:00:00 2001 From: David Bibby Date: Tue, 31 May 2022 23:48:03 +0100 Subject: [PATCH 31/54] fix: Resolved issues with old Flask versions Signed-off-by: David Bibby --- .../python/kn-py-echo/requirements.txt | 14 ++++++++++- .../knative/python/kn-py-slack/handler.py | 2 +- .../python/kn-py-slack/requirements.txt | 20 +++++++++++++-- .../python/kn-py-vm-attr/requirements.txt | 22 ++++++++++++++-- .../knative/python/kn-py-vro/requirements.txt | 25 +++++++++++++++---- 5 files changed, 72 insertions(+), 11 deletions(-) diff --git a/examples/knative/python/kn-py-echo/requirements.txt b/examples/knative/python/kn-py-echo/requirements.txt index c2e97b25..db853534 100644 --- a/examples/knative/python/kn-py-echo/requirements.txt +++ b/examples/knative/python/kn-py-echo/requirements.txt @@ -1,2 +1,14 @@ -flask==1.1.2 cloudevents==1.2.0 + deprecation==2.1.0 + packaging==21.3 + pyparsing==3.0.9 +Flask==2.1.2 + click==8.1.3 + importlib-metadata==4.11.4 + zipp==3.8.0 + itsdangerous==2.1.2 + Jinja2==3.1.2 + MarkupSafe==2.1.1 + Werkzeug==2.1.2 +pip==20.2.3 +setuptools==49.2.1 diff --git a/examples/knative/python/kn-py-slack/handler.py b/examples/knative/python/kn-py-slack/handler.py index 16ac1ddb..119dbba2 100644 --- a/examples/knative/python/kn-py-slack/handler.py +++ b/examples/knative/python/kn-py-slack/handler.py @@ -8,7 +8,7 @@ app = Flask(__name__) #Change the value to match the secret key in the VEBA appliance where you enter the Slack webook url information -#url = os.environ.get('SLACK_SECRET') +url = os.environ.get('SLACK_SECRET') @app.route('/', methods=['POST']) def slack(): diff --git a/examples/knative/python/kn-py-slack/requirements.txt b/examples/knative/python/kn-py-slack/requirements.txt index 65f72562..2667cec8 100644 --- a/examples/knative/python/kn-py-slack/requirements.txt +++ b/examples/knative/python/kn-py-slack/requirements.txt @@ -1,3 +1,19 @@ -flask==1.1.2 cloudevents==1.2.0 -requests==2.20.0 \ No newline at end of file + deprecation==2.1.0 + packaging==21.3 + pyparsing==3.0.9 +Flask==2.1.2 + click==8.1.3 + importlib-metadata==4.11.4 + zipp==3.8.0 + itsdangerous==2.1.2 + Jinja2==3.1.2 + MarkupSafe==2.1.1 + Werkzeug==2.1.2 +pip==20.2.3 +requests==2.20.0 + certifi==2022.5.18.1 + chardet==3.0.4 + idna==2.7 + urllib3==1.24.3 +setuptools==49.2.1 diff --git a/examples/knative/python/kn-py-vm-attr/requirements.txt b/examples/knative/python/kn-py-vm-attr/requirements.txt index d0ace9b9..045e6f6b 100644 --- a/examples/knative/python/kn-py-vm-attr/requirements.txt +++ b/examples/knative/python/kn-py-vm-attr/requirements.txt @@ -1,4 +1,22 @@ -flask==1.1.4 cloudevents==1.2.0 + deprecation==2.1.0 + packaging==21.3 + pyparsing==3.0.9 +Flask==2.1.2 + click==8.1.3 + importlib-metadata==4.11.4 + zipp==3.8.0 + itsdangerous==2.1.2 + Jinja2==3.1.2 + MarkupSafe==2.1.1 + Werkzeug==2.1.2 +pip==20.2.3 +python-dotenv==0.17.1 pyvmomi==7.0.2 -python-dotenv==0.17.1 \ No newline at end of file + requests==2.27.1 + certifi==2022.5.18.1 + charset-normalizer==2.0.12 + idna==3.3 + urllib3==1.26.9 + six==1.16.0 +setuptools==49.2.1 diff --git a/examples/knative/python/kn-py-vro/requirements.txt b/examples/knative/python/kn-py-vro/requirements.txt index 8198d74c..b9e94c70 100644 --- a/examples/knative/python/kn-py-vro/requirements.txt +++ b/examples/knative/python/kn-py-vro/requirements.txt @@ -1,7 +1,22 @@ -flask==2.0.2 -werkzeug==2.0.2 cloudevents==1.2.0 -urllib3==1.26.7 -requests==2.26.0 + deprecation==2.1.0 + packaging==21.3 + pyparsing==3.0.9 +Flask==2.1.2 + click==8.1.3 + importlib-metadata==4.11.4 + zipp==3.8.0 + itsdangerous==2.1.2 + Jinja2==3.1.2 + MarkupSafe==2.1.1 + Werkzeug==2.1.2 +pip==20.2.3 python-dateutil==2.8.2 -regex==2021.11.10 \ No newline at end of file + six==1.16.0 +regex==2021.11.10 +requests==2.26.0 + certifi==2022.5.18.1 + charset-normalizer==2.0.12 + idna==3.3 + urllib3==1.26.7 +setuptools==49.2.1 From f4728fffb5472a3126d5f75ec2e4666653c19c30 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jun 2022 07:52:38 +0000 Subject: [PATCH 32/54] chore(deps): Bump urllib3 in /examples/knative/python/kn-py-slack Bumps [urllib3](https://github.com/urllib3/urllib3) from 1.24.3 to 1.26.5. - [Release notes](https://github.com/urllib3/urllib3/releases) - [Changelog](https://github.com/urllib3/urllib3/blob/main/CHANGES.rst) - [Commits](https://github.com/urllib3/urllib3/compare/1.24.3...1.26.5) --- updated-dependencies: - dependency-name: urllib3 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- examples/knative/python/kn-py-slack/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/knative/python/kn-py-slack/requirements.txt b/examples/knative/python/kn-py-slack/requirements.txt index 2667cec8..2b54c1d7 100644 --- a/examples/knative/python/kn-py-slack/requirements.txt +++ b/examples/knative/python/kn-py-slack/requirements.txt @@ -15,5 +15,5 @@ requests==2.20.0 certifi==2022.5.18.1 chardet==3.0.4 idna==2.7 - urllib3==1.24.3 + urllib3==1.26.5 setuptools==49.2.1 From 58eeabe70cb632ac81e50b2c413ddf48a7852369 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jun 2022 07:52:39 +0000 Subject: [PATCH 33/54] chore(deps): Bump pip in /examples/knative/python/kn-py-echo Bumps [pip](https://github.com/pypa/pip) from 20.2.3 to 21.1. - [Release notes](https://github.com/pypa/pip/releases) - [Changelog](https://github.com/pypa/pip/blob/main/NEWS.rst) - [Commits](https://github.com/pypa/pip/compare/20.2.3...21.1) --- updated-dependencies: - dependency-name: pip dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- examples/knative/python/kn-py-echo/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/knative/python/kn-py-echo/requirements.txt b/examples/knative/python/kn-py-echo/requirements.txt index db853534..24025336 100644 --- a/examples/knative/python/kn-py-echo/requirements.txt +++ b/examples/knative/python/kn-py-echo/requirements.txt @@ -10,5 +10,5 @@ Flask==2.1.2 Jinja2==3.1.2 MarkupSafe==2.1.1 Werkzeug==2.1.2 -pip==20.2.3 +pip==21.1 setuptools==49.2.1 From 22caf50bd3aafadae1ce88847aa9c74de37daf24 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jun 2022 07:52:39 +0000 Subject: [PATCH 34/54] chore(deps): Bump pip in /examples/knative/python/kn-py-vm-attr Bumps [pip](https://github.com/pypa/pip) from 20.2.3 to 21.1. - [Release notes](https://github.com/pypa/pip/releases) - [Changelog](https://github.com/pypa/pip/blob/main/NEWS.rst) - [Commits](https://github.com/pypa/pip/compare/20.2.3...21.1) --- updated-dependencies: - dependency-name: pip dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- examples/knative/python/kn-py-vm-attr/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/knative/python/kn-py-vm-attr/requirements.txt b/examples/knative/python/kn-py-vm-attr/requirements.txt index 045e6f6b..b5cd185d 100644 --- a/examples/knative/python/kn-py-vm-attr/requirements.txt +++ b/examples/knative/python/kn-py-vm-attr/requirements.txt @@ -10,7 +10,7 @@ Flask==2.1.2 Jinja2==3.1.2 MarkupSafe==2.1.1 Werkzeug==2.1.2 -pip==20.2.3 +pip==21.1 python-dotenv==0.17.1 pyvmomi==7.0.2 requests==2.27.1 From f3f3036b01e664e2b82e43d38764969578c5a860 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jun 2022 07:52:40 +0000 Subject: [PATCH 35/54] chore(deps): Bump pip in /examples/knative/python/kn-py-vro Bumps [pip](https://github.com/pypa/pip) from 20.2.3 to 21.1. - [Release notes](https://github.com/pypa/pip/releases) - [Changelog](https://github.com/pypa/pip/blob/main/NEWS.rst) - [Commits](https://github.com/pypa/pip/compare/20.2.3...21.1) --- updated-dependencies: - dependency-name: pip dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- examples/knative/python/kn-py-vro/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/knative/python/kn-py-vro/requirements.txt b/examples/knative/python/kn-py-vro/requirements.txt index b9e94c70..906a33f6 100644 --- a/examples/knative/python/kn-py-vro/requirements.txt +++ b/examples/knative/python/kn-py-vro/requirements.txt @@ -10,7 +10,7 @@ Flask==2.1.2 Jinja2==3.1.2 MarkupSafe==2.1.1 Werkzeug==2.1.2 -pip==20.2.3 +pip==21.1 python-dateutil==2.8.2 six==1.16.0 regex==2021.11.10 From d4acb9487d8714fd7d27425db1c055efeae29788 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 2 Jun 2022 07:52:40 +0000 Subject: [PATCH 36/54] chore(deps): Bump pip in /examples/knative/python/kn-py-slack Bumps [pip](https://github.com/pypa/pip) from 20.2.3 to 21.1. - [Release notes](https://github.com/pypa/pip/releases) - [Changelog](https://github.com/pypa/pip/blob/main/NEWS.rst) - [Commits](https://github.com/pypa/pip/compare/20.2.3...21.1) --- updated-dependencies: - dependency-name: pip dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- examples/knative/python/kn-py-slack/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/knative/python/kn-py-slack/requirements.txt b/examples/knative/python/kn-py-slack/requirements.txt index 2667cec8..dce6533e 100644 --- a/examples/knative/python/kn-py-slack/requirements.txt +++ b/examples/knative/python/kn-py-slack/requirements.txt @@ -10,7 +10,7 @@ Flask==2.1.2 Jinja2==3.1.2 MarkupSafe==2.1.1 Werkzeug==2.1.2 -pip==20.2.3 +pip==21.1 requests==2.20.0 certifi==2022.5.18.1 chardet==3.0.4 From 038a2f35b9df95d89d335dc2d8013c4a9d781956 Mon Sep 17 00:00:00 2001 From: Michael Gasch Date: Thu, 2 Jun 2022 10:03:41 +0200 Subject: [PATCH 37/54] chore: Update Python examples Signed-off-by: Michael Gasch --- examples/knative/python/kn-py-echo/README.md | 6 +++--- examples/knative/python/kn-py-echo/function.yaml | 2 +- examples/knative/python/kn-py-slack/README.md | 6 +++--- examples/knative/python/kn-py-slack/function.yaml | 2 +- examples/knative/python/kn-py-slack/requirements.txt | 2 +- examples/knative/python/kn-py-vm-attr/function.yaml | 2 +- examples/knative/python/kn-py-vro/README.md | 2 +- examples/knative/python/kn-py-vro/function.yaml | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/knative/python/kn-py-echo/README.md b/examples/knative/python/kn-py-echo/README.md index 05822251..5d934eb3 100644 --- a/examples/knative/python/kn-py-echo/README.md +++ b/examples/knative/python/kn-py-echo/README.md @@ -7,7 +7,7 @@ Example Python function with `Flask` REST API running in Knative to echo [Buildpacks](https://buildpacks.io) are used to create the container image. ```bash -IMAGE=/kn-py-echo:1.0 +IMAGE=/kn-py-echo:1.1 pack build -B gcr.io/buildpacks/builder:v1 ${IMAGE} ``` @@ -16,7 +16,7 @@ pack build -B gcr.io/buildpacks/builder:v1 ${IMAGE} Verify the container image works by executing it locally. ```bash -docker run -e PORT=8080 -it --rm -p 8080:8080 /kn-py-echo:1.0 +docker run -e PORT=8080 -it --rm -p 8080:8080 /kn-py-echo:1.1 ``` You should see output similar to the following: ``` @@ -72,7 +72,7 @@ Return to the previous terminal window where you started the docker image, and y Push your container image to an accessible registry such as Docker once you're done developing and testing your function logic. ```console -docker push /kn-py-echo:1.0 +docker push /kn-py-echo:1.1 ``` Edit the `function.yaml` file with the name of the container image from Step 1 if you made any changes. If not, the default VMware container image will suffice. By default, the function deployment will filter on the `VmPoweredOffEvent` vCenter Server Event. If you wish to change this, update the `subject` field within `function.yaml` to the desired event type. diff --git a/examples/knative/python/kn-py-echo/function.yaml b/examples/knative/python/kn-py-echo/function.yaml index 73546172..c83e7a9c 100644 --- a/examples/knative/python/kn-py-echo/function.yaml +++ b/examples/knative/python/kn-py-echo/function.yaml @@ -12,7 +12,7 @@ spec: autoscaling.knative.dev/minScale: "1" spec: containers: - - image: us.gcr.io/daisy-284300/veba/kn-py-echo:1.0 + - image: us.gcr.io/daisy-284300/veba/kn-py-echo:1.1 --- apiVersion: eventing.knative.dev/v1 kind: Trigger diff --git a/examples/knative/python/kn-py-slack/README.md b/examples/knative/python/kn-py-slack/README.md index fbeefe69..5f661927 100644 --- a/examples/knative/python/kn-py-slack/README.md +++ b/examples/knative/python/kn-py-slack/README.md @@ -6,7 +6,7 @@ Example Knative Python function for sending to a Slack webhook when a Virtual Ma [Buildpacks](https://buildpacks.io) are used to create the container image. ```bash -IMAGE=/kn-py-slack:1.0 +IMAGE=/kn-py-slack:1.1 pack build -B gcr.io/buildpacks/builder:v1 ${IMAGE} ``` @@ -25,7 +25,7 @@ Update the `docker-test-env-variable` file with your Slack webook URL. Start the container image by running the following command: ```console -docker run -e PORT=8080 --env-file docker-test-env-variable -it --rm -p 8080:8080 /kn-py-slack:1.0 +docker run -e PORT=8080 --env-file docker-test-env-variable -it --rm -p 8080:8080 /kn-py-slack:1.1 Serving Flask app "handler.py" (lazy loading) * Environment: development @@ -73,7 +73,7 @@ Finally, check your Slack channel to see if the test event posted. Push your container image to an accessible registry such as Docker once you're done developing and testing your function logic. ```console -docker push /kn-py-slack:1.0 +docker push /kn-py-slack:1.1 ``` Update the `slack_secret.json` file with your Slack webhook configurations and then create the kubernetes secret which can then be accessed from within the function by using the environment variable named called `SLACK_SECRET`. diff --git a/examples/knative/python/kn-py-slack/function.yaml b/examples/knative/python/kn-py-slack/function.yaml index 0d22fadd..5081dc20 100644 --- a/examples/knative/python/kn-py-slack/function.yaml +++ b/examples/knative/python/kn-py-slack/function.yaml @@ -12,7 +12,7 @@ spec: autoscaling.knative.dev/minScale: "1" spec: containers: - - image: us.gcr.io/daisy-284300/veba/kn-py-slack:1.0 + - image: us.gcr.io/daisy-284300/veba/kn-py-slack:1.1 envFrom: - secretRef: name: slack-secret diff --git a/examples/knative/python/kn-py-slack/requirements.txt b/examples/knative/python/kn-py-slack/requirements.txt index 0b2b958a..54c1ccb6 100644 --- a/examples/knative/python/kn-py-slack/requirements.txt +++ b/examples/knative/python/kn-py-slack/requirements.txt @@ -11,7 +11,7 @@ Flask==2.1.2 MarkupSafe==2.1.1 Werkzeug==2.1.2 pip==21.1 -requests==2.20.0 +requests==2.27.1 certifi==2022.5.18.1 chardet==3.0.4 idna==2.7 diff --git a/examples/knative/python/kn-py-vm-attr/function.yaml b/examples/knative/python/kn-py-vm-attr/function.yaml index c6060ce5..41fa6c7b 100644 --- a/examples/knative/python/kn-py-vm-attr/function.yaml +++ b/examples/knative/python/kn-py-vm-attr/function.yaml @@ -13,7 +13,7 @@ spec: autoscaling.knative.dev/minScale: "1" spec: containers: - - image: us.gcr.io/daisy-284300/veba/kn-py-vm-attr:1.0 + - image: us.gcr.io/daisy-284300/veba/kn-py-vm-attr:1.1 envFrom: - secretRef: name: vcconfig-secret diff --git a/examples/knative/python/kn-py-vro/README.md b/examples/knative/python/kn-py-vro/README.md index cd96b42d..95862bdc 100644 --- a/examples/knative/python/kn-py-vro/README.md +++ b/examples/knative/python/kn-py-vro/README.md @@ -35,7 +35,7 @@ Requirements: - Docker ```bash -export IMAGE=/kn-py-vro:1.0 +export IMAGE=/kn-py-vro:1.1 pack build --builder gcr.io/buildpacks/builder:v1 ${IMAGE} ``` diff --git a/examples/knative/python/kn-py-vro/function.yaml b/examples/knative/python/kn-py-vro/function.yaml index fa01ba87..873a7e35 100644 --- a/examples/knative/python/kn-py-vro/function.yaml +++ b/examples/knative/python/kn-py-vro/function.yaml @@ -12,7 +12,7 @@ spec: autoscaling.knative.dev/minScale: "1" spec: containers: - - image: us.gcr.io/daisy-284300/veba/kn-py-vro:1.0 + - image: us.gcr.io/daisy-284300/veba/kn-py-vro:1.1 envFrom: - secretRef: name: vroconfig-secret From 2d632a0459437ed50e62dc0d64d89f572827087f Mon Sep 17 00:00:00 2001 From: Michael Gasch Date: Thu, 2 Jun 2022 10:10:14 +0200 Subject: [PATCH 38/54] docs: Update documentation Closes: #861 Closes: #875 Signed-off-by: Michael Gasch --- docs/_functions/sample1.md | 2 +- docs/_functions/sample3.md | 2 +- docs/kb/deploy-event-router-kind.md | 2 +- docs/kb/function-tutorial-intro.md | 20 +++++--- docs/kb/intro-event-router.md | 9 ++-- docs/site/community.md | 78 ++++++++++++++++++++++++++--- vmware-event-router/README.MD | 8 +-- 7 files changed, 96 insertions(+), 25 deletions(-) diff --git a/docs/_functions/sample1.md b/docs/_functions/sample1.md index fd269455..01c94186 100644 --- a/docs/_functions/sample1.md +++ b/docs/_functions/sample1.md @@ -3,6 +3,6 @@ title: vSphere Alarm Function icon: case-study-2.svg #subtitle: Subheading goes here links: - Deploy Function: https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/development/examples/knative/powershell/kn-ps-slack-vsphere-alarm + Deploy Function: https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/examples/knative/powershell/kn-ps-slack-vsphere-alarm --- vSphere alarm events do not provide metadata about defined triggers and thresholds. This example demonstrates how Knative flows can be used to enrich events and send it to other functions for consumption. \ No newline at end of file diff --git a/docs/_functions/sample3.md b/docs/_functions/sample3.md index 87bd80c2..428a1dc3 100644 --- a/docs/_functions/sample3.md +++ b/docs/_functions/sample3.md @@ -3,6 +3,6 @@ title: Slack Notification Function icon: case-study-3.svg #subtitle: Subheading goes here links: - Deploy Function: https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/development/examples/knative/python/kn-py-slack + Deploy Function: https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/examples/knative/python/kn-py-slack --- Based on a Virtual Machine powered off event, this function will send a message to Slack with details about the virtual machine, e.g. when and by whom was this event triggered. \ No newline at end of file diff --git a/docs/kb/deploy-event-router-kind.md b/docs/kb/deploy-event-router-kind.md index 00185a7c..465326ce 100644 --- a/docs/kb/deploy-event-router-kind.md +++ b/docs/kb/deploy-event-router-kind.md @@ -9,7 +9,7 @@ permalink: /kb/deploy-event-router-kind # Installation of VMware Event Router with `kind` The following steps describe the installation of the [VMware Event -Router](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/development/vmware-event-router) +Router](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/vmware-event-router) in a local [kind](https://kind.sigs.k8s.io/) cluster and [Knative](https://knative.dev/) environment. The steps assume a Mac OSX environment but the links provide resources to diff --git a/docs/kb/function-tutorial-intro.md b/docs/kb/function-tutorial-intro.md index 5bdc589f..bb3086de 100644 --- a/docs/kb/function-tutorial-intro.md +++ b/docs/kb/function-tutorial-intro.md @@ -22,16 +22,24 @@ This tutorial will go over: - How to deploy the function to the Kubernetes cluster in your VEBA appliance ## Table of Contents -- [The big picture](#the-big-picture) -- [The anatomy of a VEBA function](#the-anatomy-of-a-veba-function) -- [Installing required tools on your workstation](#installing-required-tools-on-your-workstation) -- [macOS Instructions](#macos-instructions) -- [Windows Instructions](#windows-instructions) +- [In-depth Function Tutorial - Intro](#in-depth-function-tutorial---intro) + - [Table of Contents](#table-of-contents) + - [The big picture](#the-big-picture) + - [The anatomy of a VEBA function](#the-anatomy-of-a-veba-function) + - [Installing required tools on your workstation](#installing-required-tools-on-your-workstation) + - [macOS Instructions](#macos-instructions) + - [Install `git` and clone the repo to your workstation](#install-git-and-clone-the-repo-to-your-workstation) + - [Install Docker](#install-docker) + - [Install and Configure `kubectl`](#install-and-configure-kubectl) + - [Windows Instructions](#windows-instructions) + - [Install `git` and clone the repo to your workstation](#install-git-and-clone-the-repo-to-your-workstation-1) + - [Install Docker](#install-docker-1) + - [Install and Configure `kubectl`](#install-and-configure-kubectl-1) ## The big picture VEBA functions provide the "custom" logic to the VEBA appliance to fulfil your business requirements. Functions are packaged as Docker images. The functions can be written in PowerShell, Python, Go, or just about any language, and are packaged and distributed as Docker images. This is because the VEBA appliance runs Kubernetes (on top of the Photon OS) and functions are deployed as containers on the Kubernetes system. Kubernetes provides an abstraction layer to allow containers to run seamlessly on disparate hardware/OSes. -The VEBA team has provided a library of sample functions for you to get started with: [VEBA Functions](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/development/examples/knative). As you can see, they are assembled under the "knative" folder. Knative is a framework of building blocks for Kubernetes that provides basic services to the VEBA application. So the first step would be to review the provided functions and determine if there are any functions that match your use case requirements. If your use case was to send an email message when a specific vCenter event occurred, the function: [kn-ps-email](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/development/examples/knative/powershell/kn-ps-email) would be a good solution to begin with. In looking at the function, you can see that it is currently configured to fire/trigger for a VM deletion event. Don't worry if your use case is a different vCenter event - this is easily modified in the function regardless of if you are adept with PowerShell programming. +The VEBA team has provided a library of sample functions for you to get started with: [VEBA Functions](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/examples/knative). As you can see, they are assembled under the "knative" folder. Knative is a framework of building blocks for Kubernetes that provides basic services to the VEBA application. So the first step would be to review the provided functions and determine if there are any functions that match your use case requirements. If your use case was to send an email message when a specific vCenter event occurred, the function: [kn-ps-email](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/examples/knative/powershell/kn-ps-email) would be a good solution to begin with. In looking at the function, you can see that it is currently configured to fire/trigger for a VM deletion event. Don't worry if your use case is a different vCenter event - this is easily modified in the function regardless of if you are adept with PowerShell programming. **A word about programming languages:** If you find a function that meets your requirements but is written in a language you are not comfortable with, don't worry, you will still be able to deploy that function. There are two parts to the VEBA functions: the programming logic and the input variables. Variables contain things like: IP addresses, Event types, authentication parameters, email addresses, or Slack keys. Variables are easily input and modified without knowledge of the specific programming language. You will find examples of this later in the tutorial. diff --git a/docs/kb/intro-event-router.md b/docs/kb/intro-event-router.md index eb92443d..4a53469c 100644 --- a/docs/kb/intro-event-router.md +++ b/docs/kb/intro-event-router.md @@ -144,7 +144,7 @@ version: The following sections describe the layout of the configuration file (YAML) and specific options for the supported event `providers`, `processors` and `metrics` -endpoint. Configuration examples are provided [here](deploy/). +endpoint. Configuration examples are provided [here](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/vmware-event-router/deploy). > **Note:** Currently only one event `provider` and one event `processor` can be > configured at a time, e.g. one vCenter Server instance streaming events to @@ -198,7 +198,7 @@ metricsProvider: ## JSON Schema Validation In order to simplify the configuration and validation of the YAML configuration -file a JSON schema [file](README.MD) is provided. Many editors/IDEs offer +file a JSON schema [file](https://github.com/vmware-samples/vcenter-event-broker-appliance/blob/master/vmware-event-router/routerconfig.schema.json) is provided. Many editors/IDEs offer support for registering a schema file, e.g. [Jetbrains](https://www.jetbrains.com/help/rider/Settings_Languages_JSON_Schema.html) and [VS @@ -636,7 +636,7 @@ using the Knative backend. ### Helm Deployment -The Helm files are located in the [chart](chart/) directory. The `values.yaml` +The Helm files are located in the [chart](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/vmware-event-router/chart) directory. The `values.yaml` file contains the allowed parameters and parameter descriptions which map to the VMware Event Router [configuration](#overview-configuration-file-structure-yaml) file. @@ -800,7 +800,7 @@ Create a namespace where the VMware Event Router will be deployed to: $ kubectl create namespace vmware ``` -Use one of the configuration files provided [here](deploy/) to configure the +Use one of the configuration files provided [here](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/vmware-event-router/deploy) to configure the router with **one** VMware vCenter Server `eventProvider` and **one** OpenFaaS **or** AWS EventBridge `eventProcessor`. Change the values to match your environment. The following example will use the OpenFaaS config sample. @@ -892,7 +892,6 @@ Deploy the VMware Event Router: ```console $ kubectl -n vmware create -f release.yaml ``` -``` Check the logs of the VMware Event Router to validate it started correctly: diff --git a/docs/site/community.md b/docs/site/community.md index d50681b2..157f3302 100644 --- a/docs/site/community.md +++ b/docs/site/community.md @@ -30,23 +30,87 @@ The VMware Event Broker Appliance team welcomes contributions from the community # Guidelines -Following the guidelines helps to make the contribution process easy, collaborative, and productive. +Following the guidelines helps to make the contribution process easy, +collaborative, and productive. -Before you start working with the VMware Event Broker Appliance, please read our [Developer Certificate of Origin](https://cla.vmware.com/dco){:target="_blank"}. All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch. +Before you start working with the VMware Event Broker Appliance, please read our +[Developer Certificate of Origin](https://cla.vmware.com/dco){:target="_blank"}. +All contributions to this repository must be signed as described on that page. +Your signature certifies that you wrote the patch or have the right to pass it +on as an open-source patch. ## Submitting Bug Reports and Feature Requests -Please submit bug reports and feature requests by using our GitHub [Issues](https://github.com/vmware-samples/vcenter-event-broker-appliance/issues){:target="_blank"} page. +Please submit bug reports and feature requests by using our GitHub +[Issues](https://github.com/vmware-samples/vcenter-event-broker-appliance/issues){:target="_blank"} +page. -Before you submit a bug report about the code in the repository, please check the Issues page to see whether someone has already reported the problem. In the bug report, be as specific as possible about the error and the conditions under which it occurred. On what version and build did it occur? What are the steps to reproduce the bug? +Before you submit a bug report about the code in the repository, please check +the Issues page to see whether someone has already reported the problem. In the +bug report, be as specific as possible about the error and the conditions under +which it occurred. On what version and build did it occur? What are the steps to +reproduce the bug? Feature requests should fall within the scope of the project. ## Pull Requests -Before submitting a pull request, please make sure that your change satisfies the following requirements: -- The change is signed as described by the [Developer Certificate of Origin](https://cla.vmware.com/dco){:target="_blank"} doc. -- The change is clearly documented and follows Git commit [best practices](https://chris.beams.io/posts/git-commit/){:target="_blank"} +Before submitting a pull request, please make sure that your change satisfies +the following requirements: +- The change is signed as described by the [Developer Certificate of + Origin](https://cla.vmware.com/dco){:target="_blank"} doc. +- The change is clearly documented and follows Git commit best practices (see + below) + +### Git Commit Best Practices + +#### Contribution Flow + +This is a rough outline of what a contributor's workflow looks like: + +- Create an issue describing the feature/fix. +- Create a topic branch from where you want to base your work. +- Make commits of logical units. +- Make sure your commit messages are in the proper format (see below). +- Push your changes to a topic branch in your fork of the repository. +- Submit a pull request to `vmware-samples/vcenter-event-broker-appliance`. + +See [below](#format-of-the-commit-message) for details on commit best practices +and **supported prefixes**, e.g. `fix: `. + +> **Note:** If you are new to Git(hub) check out [Git rebase, squash...oh +> my!](https://www.mgasch.com/2021/05/git-basics/) for more details on how to +> successfully contribute to an open source project. + + +#### Format of the Commit Message + +We follow the conventions described in [How to Write a Git Commit +Message](http://chris.beams.io/posts/git-commit/). + +Be sure to include any related GitHub issue references in the commit message, +e.g. `Closes: #`. + +The `CHANGELOG.md` and release page use **commit message prefixes** for grouping +and highlighting. A commit message that starts with `[prefix:] ` will place this +commit under the respective section in the `CHANGELOG`. + +The following example creates a commit referencing the `issue: 1234` and puts +the commit message in the `Feature` `CHANGELOG` section: + +```bash +git commit -s -m "feat: Add Slack Example" -m "Closes: #1234" +``` + +Currently the following prefixes are used: + +- `fix:` - Use for bug fixes +- `feat:` - A new feature +- `chore:` - Use for repository related activities +- `docs:` - Use for changes to the documentation + +If your contribution falls into multiple categories, e.g. `feat` and `fix` it is +recommended to break up your commits using distinct prefixes. ### Contributions to the Appliance - See the Build Appliance document [here](/kb/contribute-appliance) diff --git a/vmware-event-router/README.MD b/vmware-event-router/README.MD index 252511f4..027193f0 100644 --- a/vmware-event-router/README.MD +++ b/vmware-event-router/README.MD @@ -93,7 +93,7 @@ version: The following sections describe the layout of the configuration file (YAML) and specific options for the supported event `providers`, `processors` and `metrics` -endpoint. Configuration examples are provided [here](deploy/). +endpoint. Configuration examples are provided [here](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/vmware-event-router/deploy). > **Note:** Currently only one event `provider` and one event `processor` can be > configured at a time, e.g. one vCenter Server instance streaming events to @@ -151,7 +151,7 @@ metricsProvider: ## JSON Schema Validation In order to simplify the configuration and validation of the YAML configuration -file a JSON schema [file](README.MD) is provided. Many editors/IDEs offer +file a JSON schema [file](https://github.com/vmware-samples/vcenter-event-broker-appliance/blob/master/vmware-event-router/routerconfig.schema.json) is provided. Many editors/IDEs offer support for registering a schema file, e.g. [Jetbrains](https://www.jetbrains.com/help/rider/Settings_Languages_JSON_Schema.html) and [VS @@ -589,7 +589,7 @@ using the Knative backend. ### Helm Deployment -The Helm files are located in the [chart](chart/) directory. The `values.yaml` +The Helm files are located in the [chart](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/vmware-event-router/chart) directory. The `values.yaml` file contains the allowed parameters and parameter descriptions which map to the VMware Event Router [configuration](#overview-configuration-file-structure-yaml) file. @@ -753,7 +753,7 @@ Create a namespace where the VMware Event Router will be deployed to: $ kubectl create namespace vmware ``` -Use one of the configuration files provided [here](deploy/) to configure the +Use one of the configuration files provided [here](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/master/vmware-event-router/deploy) to configure the router with **one** VMware vCenter Server `eventProvider` and **one** OpenFaaS **or** AWS EventBridge `eventProcessor`. Change the values to match your environment. The following example will use the OpenFaaS config sample. From 6687c71622b11fa8315506f094c928e310d6db7d Mon Sep 17 00:00:00 2001 From: Robert Guske Date: Mon, 6 Jun 2022 17:44:07 +0200 Subject: [PATCH 39/54] docs: update additional resources page with latest content closes: #897 Signed-off-by: Robert Guske --- docs/_data/resources.yml | 130 +++++++++++++++++++++++++++------------ 1 file changed, 89 insertions(+), 41 deletions(-) diff --git a/docs/_data/resources.yml b/docs/_data/resources.yml index 21aada15..4a5bc609 100644 --- a/docs/_data/resources.yml +++ b/docs/_data/resources.yml @@ -3,53 +3,80 @@ # Featured links with the below details populated. Current max limit of links set to 5 # use the flag display: true/false to control if the link needs to show up on the site. Videos show up in the same order as below # -linklimit: 7 #Avoid high number to reduce vertical scroll +linklimit: 10 #Avoid high number to reduce vertical scroll videolimit: 3 #Optimal number, DONT CHANGE links: - - title: VMware Event Broker (aka VEBA) on Kubernetes – First steps + - title: Achieve Event-Driven Automation with VMware Event Broker Appliance (VEBA) display: true details: url_text: blog post - external_url: https://vuptime.io/2020/11/02/vmware-event-broker-on-k8s-first-steps/ - external_image: https://vuptime.io/images/veba-first-steps/veba_otto_the_orca_md.png - author_name: Ludovic Rivallain - excerpt: In the following post, we will discover how to deploy the VMware Event Broker services (VEBA) within an existing Kubernetes (K8S) cluster and use it to add/edit custom attributes information to virtual machines... + external_url: https://www.altaro.com/vmware/vmware-event-broker-appliance/ + external_image: https://prodwewpstorageaccount.s3.eu-central-1.amazonaws.com/wp-content/uploads/sites/7/2022/03/14090840/VMware-Event-Broker-Appliance-841x281-1.jpg + author_name: Brandon Lee + excerpt: Organizations worldwide are in the middle of a paradigm shift in provisioning, managing, monitoring, and configuring their infrastructure. The cloud revolution has prompted a change in the way businesses think about infrastructure ... - - title: Self Service for your DataCenter + Auto Remediation + - title: vSphere Event-Driven Automation using Tanzu Application Platform (TAP) on Tanzu Community Edition display: true - details: + details: url_text: blog post - external_url: https://bit.ly/vmselfservepk - external_image: https://miro.medium.com/max/1506/1*l2CTz_sUAjRmWugTV_952A.png - author_name: Partheeban Kandasamy (PK) & Frankie Gold - excerpt: For the Command/User driven use case, What if IT enables Business Stakeholders to interact with vSphere SDDC through a Slack command...For the event-driven use case, what if certain crises are handled automatically thus increasing operational efficiency... + external_url: https://williamlam.com/2022/01/vsphere-event-driven-automation-using-tanzu-application-platform-tap-on-tanzu-community-edition.html + external_image: https://i0.wp.com/williamlam.com/wp-content/uploads/2022/01/vsphere-event-driven-automation-using-tanzu-application-platform-on-tanzu-kubernetes-grid-5.png + author_name: William Lam + excerpt: One of the core components of TAP is the Cloud Native Runtime (CNR), which is VMware's commercial offering of the popular open source project Knative. The VMware Event Broker Appliance (VEBA) project also makes use of Knative as our backend to provide customers with an event-driven automation solution. - - title: Integrating VMware Cloud Notification Gateway with VMware Event Broker Appliance + - title: vSphere Event-Driven Automation using VMware Event Router on VMware Cloud on AWS with Knative or AWS EventBridge display: true - details: + details: url_text: blog post - external_url: https://www.williamlam.com/2020/07/integrating-vmware-cloud-notification-gateway-with-vmware-event-broker-appliance-veba.html - external_image: https://i2.wp.com/www.williamlam.com/wp-content/uploads/2020/07/vmware-cloud-notification-to-veba-diagram.png?ssl=1 + external_url: https://williamlam.com/2022/05/vsphere-event-driven-automation-using-vmware-event-router-on-vmware-cloud-on-aws-with-knative-or-aws-eventbridge.html + external_image: https://i0.wp.com/williamlam.com/wp-content/uploads/2022/05/vmware-event-router-vmc-on-aws-event-bridge-0.png author_name: William Lam - excerpt: advantage of the NGW webhook capability but instead of forwarding the NGW notification to a cloud service that supports an incoming webhook, we are sending it to VEBA for processing. Once the notification has been received by VEBA, customers can apply... + excerpt: I wanted to spend some time covering the native Kubernetes deployment model, as it there are actually a couple of options and most recently, this came up in a customer discussions as they were interested in forwarding vSphere Events from VEBA to AWS EventBridge. - - title: Bursting to the Cloud with VEBA, AWS Lambda & VMC on AWS + - title: How to modernize your vSphere Alarm actions using the VMware Event Broker Appliance (VEBA)? display: true - details: + details: url_text: blog post - external_url: https://nicovibert.com/2020/05/20/cloudbursting-vmwarecloud-veba-lambda-eventbridge/ - external_image: https://nicovmc.files.wordpress.com/2020/05/screenshot-2020-05-20-at-12.17.07.png?w=1024 - author_name: Nico Vibert - excerpt: We need an engine to trigger an action based on an event. For VMware events, the best way to do that would be to leverage the VMware Event Broker Appliance (VEBA). I will leverage EventBridge as it integrates easily with AWS Lambda... + external_url: https://williamlam.com/2021/07/how-to-modernize-your-vsphere-alarm-actions-using-the-vmware-event-broker-appliance-veba.html + external_image: https://i0.wp.com/williamlam.com/wp-content/uploads/2021/07/modernizing-vsphere-alarm-actions-0.png + author_name: William Lam + excerpt: The benefits of VEBA can extend beyond just vSphere Events and can also be used with both new and existing vSphere Alarms. In fact, vSphere Alarms is just another a type of vSphere Event, which then makes it super easy to work with if you are already familiar with VEBA. - - title: Deploying the VMware Event Broker Appliance (VEBA) using Cloud Assembly + - title: VMware Event Broker Appliance – Part VIII – Building a New PowerCLI Function – Preparation display: true - details: + details: url_text: blog post - external_url: https://blogs.vmware.com/services-education-insights/2020/07/deploying-the-vmware-event-broker-appliance-veba-using-cloud-assembly.html - external_image: https://blogs.vmware.com/services-education-insights/files/2020/07/ovfScreenshot.png - author_name: VMware Blogs - James Wirth + external_url: http://www.patrickkremer.com/vmware-event-broker-appliance-part-viii-building-a-new-powercli-function-preparation/ + external_image: http://www.patrickkremer.com/wp-content/uploads/2022/03/kn-pcli-template.png + author_name: Patrick Kremer + excerpt: A Knative PowerCLI function template was committed to the repo in February 2022, and was released in 0.7.2. This template makes it easy to build a brand new function complete with a full set of documentation. This post walks through the process of using the template to build the kn-pcli-pg-check function, which was published in March 2022. + + - title: Custom webhook function to publish events into VMware Event Broker Appliance (VEBA) + display: true + details: + url_text: blog post + external_url: https://williamlam.com/2021/09/custom-webhook-function-to-publish-events-into-vmware-event-broker-appliance-veba.html + external_image: https://i0.wp.com/williamlam.com/wp-content/uploads/2021/09/vmware-event-broker-appliance-webhook-support-0.png + author_name: William Lam + excerpt: In my previous article, I demonstrated how you can leverage the upcoming v0.7 release of the VMware Event Broker Appliance (VEBA) to publish and consume custom events to easily extend your event-driven automation to other event sources. Once you have setup a wildcard DNS for your VEBA deployment, you can refer to this sample PowerShell function which demonstrates how to create and test a custom webhook function. + + - title: Managing VM snapshot retention policies using the VMware Event Broker Appliance (VEBA) + display: true + details: + url_text: blog post + external_url: https://williamlam.com/2021/10/managing-vm-snapshot-retention-policies-using-the-vmware-event-broker-appliance-veba.html + external_image: https://i0.wp.com/williamlam.com/wp-content/uploads/2021/10/snapshot-policy-retention-using-veba.png + author_name: William Lam + excerpt: Imagine if you could implement a snapshot retention policy for your VM(s) based on the size of a given snapshot or maybe the number of days the snapshot has existed? I realized we can easily do so with a bit of event-driven automation using our VMware Event Broker Appliance (VEBA) solution to run scheduled job for managing snapshot policies for a set of VM(s) + + - title: Bursting to the Cloud with VEBA, AWS Lambda & VMC on AWS + display: true + details: + url_text: blog post + external_url: https://nicovibert.com/2020/05/20/cloudbursting-vmwarecloud-veba-lambda-eventbridge/ + external_image: https://nicovmc.files.wordpress.com/2020/05/screenshot-2020-05-20-at-12.17.07.png?w=1024 + author_name: Nico Vibert excerpt: We need an engine to trigger an action based on an event. For VMware events, the best way to do that would be to leverage the VMware Event Broker Appliance (VEBA). I will leverage EventBridge as it integrates easily with AWS Lambda... - title: Using the VEBA to Automatically Expand a Pure Storage FlashArray Datastore @@ -59,25 +86,46 @@ links: external_url: https://davidstamen.com/using-veba-to-expand-pure-datastore/ external_image: https://davidstamen.com/images/veba05.png author_name: David Stamen - excerpt: can we automatically expand a datastore when it gets full? The answer is yes! With all of the integrations with automation platformsm, Pure Storage Arrays have many options. This blog will cover how to handle this with the VMware Event Broker Appliance (VEBA)... + excerpt: ...can we automatically expand a datastore when it gets full? The answer is yes! With all of the integrations with automation platformsm, Pure Storage Arrays have many options. This blog will cover how to handle this with the VMware Event Broker Appliance (VEBA)... - title: VMware Event Broker Appliance - vSphere HA Event Notification Function display: true - details: + details: url_text: blog post external_url: https://rguske.github.io/post/vsphere-ha-event-notification-function/ external_image: https://rguske.github.io/img/posts/202006_harestartfunction/CapturFiles-20200701_022720.jpg author_name: Robert Guske - excerpt: to send out a notification via Email after a vSphere HA event occurred. This Email will have the affected host mentioned as well as all affected VMs which has been restarted through vSphere HA. The problem was, that the execution of the script was a manual task every time a ESXi host outage took place…at least until he became aware of VEBA... - + excerpt: This post is about a great function contribution from a VEBA community member. His function example is about sending an email which notifies a recipient about the affected host(s) and VM(s) after an outage. + # # All other links with the below details populated. # use the flag display: true/false to control if the link needs to show up on the site. Videos show up in the same order as below # otherlinks: + - title: Quick Tip - Enabling vCenter Events for NTP (Network Time Protocol) or PTP (Precision Time Protocol) operations + display: true + url: https://williamlam.com/2022/05/quick-tip-enabling-vcenter-events-for-ntp-network-time-protocol-or-ptp-precision-time-protocol-operations.html + - title: Integrating VMware Event Broker Appliance (VEBA) with Zapier + display: true + url: https://williamlam.com/2022/04/integrating-vmware-event-broker-appliance-veba-with-zapier.html + - title: Publishing and consuming custom events with VMware Event Broker Appliance (VEBA) + display: true + url: https://williamlam.com/2021/09/publishing-and-consuming-custom-events-with-vmware-event-broker-appliance-veba.html + - title: Heads Up - No healthy upstream error with VEBA vSphere UI plugin with vSphere 7.0 Update 3 + display: true + url: https://williamlam.com/2021/10/heads-up-no-healthy-upstream-error-with-veba-vsphere-ui-plugin-with-vsphere-7-0-update-3.html + - title: Leveraging Fluent Bit on Tanzu Kubernetes Grid to send VEBA logs to vRealize Log Insight + display: true + url: https://rguske.github.io/post/leveraging-fluent-bit-on-tkg-to-send-veba-logs-to-vrli/ + - title: (German) VMware Event Broker Appliance - das Mega Release 0.6 bringt Simplifizierung und Optimierung + display: true + url: https://youtu.be/Zqp3Z5uV1n4 - title: VMworld 2020 - VEBA and the Power of Event-Driven Automation – Reloaded display: true url: https://www.vmworld.com/en/video-library/video-landing.html?sessionid=1586353214997001Abo2 + - title: Deploying the VMware Event Broker Appliance (VEBA) using Cloud Assembly + display: true + url: https://blogs.vmware.com/services-education-insights/2020/07/deploying-the-vmware-event-broker-appliance-veba-using-cloud-assembly.html - title: VMworld 2020 - Arm Yourself with Event-Driven Functions and Reimagine SDDC Capabilities display: true url: https://www.vmworld.com/en/video-library/video-landing.html?sessionid=15863800295950014HrA @@ -148,14 +196,14 @@ tweet_collection: # Current max limit of videos set to 3. Videos require the video_id # videos: -- title: VEBA team talk about Event Driven Automation, usecases and showcase a demo - video_id: tOjp5_qn-Fg - url: https://www.youtube.com/watch?v=tOjp5_qn-Fg&feature=emb_logo +- title: VEBA Revolutions - Unleashing the Power of Event-Driven Automation + video_id: jwgJpZM68mA + url: https://www.youtube.com/watch?v=jwgJpZM68mA&feature=emb_logo -- title: Learn how to deploy VEBA and deploy your own functions - video_id: /videoseries?list=PLQvw_QGg8w6qlcCxXnighoK780ElUrOFj - url: https://www.youtube.com/playlist?list=PLQvw_QGg8w6qlcCxXnighoK780ElUrOFj +- title: Optimize Kubernetes on vSphere with Event-Driven Automation + video_id: NJYBwJemdoY + url: https://youtu.be/NJYBwJemdoY -- title: DEMO - Veeam VM Backup FaaS Function - video_id: OtAG9C8FM4U - url: https://www.youtube.com/watch?v=OtAG9C8FM4U&feature=emb_logo \ No newline at end of file +- title: DIY Deployment of Event-Driven Automation in vSphere Environments + video_id: ieUqfir5Oag + url: https://youtu.be/ieUqfir5Oag&feature=emb_logo \ No newline at end of file From 4dbcb9cb40750f8e3046139afd00eaccce47cf03 Mon Sep 17 00:00:00 2001 From: Robert Guske Date: Mon, 6 Jun 2022 18:15:05 +0200 Subject: [PATCH 40/54] docs: updated faqs 3 and 6 on veba website Closes: #899 Signed-off-by: Robert Guske --- docs/site/faq.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/site/faq.md b/docs/site/faq.md index 4478536b..5e37eb89 100644 --- a/docs/site/faq.md +++ b/docs/site/faq.md @@ -17,7 +17,7 @@ faqs: A: Yes! Follow the steps provided [here](/kb/advanced-certificates). - Q: What happens if vCenter Server and VMware Event Broker connectivity is lost? A: > - VMware [Event Router](https://vmweventbroker.io/kb/contribute-eventrouter) streams vCenter events as they get generated and being stateless, does not persist any event information. To provide a certain level of reliability, the following Event Delivery Guarantees exists:
+ VMware [Event Router](https://vmweventbroker.io/kb/event-router) streams vCenter events as they get generated and being stateless, does not persist any event information. To provide a certain level of reliability, the following Event Delivery Guarantees exists:
- At-least-once event delivery semantics for the vCenter event provider by checkpointing the event stream into a file. In case of disconnection, the Event Router will replay all vCenter events of the last 10 minutes (10m reiteration) after a successful reconnection.
- At-least-once event delivery semantics are not guaranteed if the event router crashes within seconds right after startup and having received *n* events but before creating the first valid checkpoint (current checkpoint interval is 5s).
- Q: How long does it take for the functions to be invoked upon an event being generated? @@ -25,7 +25,7 @@ faqs: - Q: Can I setup the VMware Event Broker Appliance components on Kubernetes? A: Yes! Follow the steps provided [here](/kb/event-router#deployment). - Q: Can I use a private registry like e.g. [Harbor](https://goharbor.io/) to have a source of truth for my functions (images)? - A: Yes! Follow the steps provided [here](https://rguske.github.io/post/using-harbor-with-the-vcenter-event-broker-appliance/). + A: Yes! Follow the steps provided [here](https://vmweventbroker.io/kb/private-registry). - Q: How can I monitor the Appliance, the Kubernetes components as well as the functions (pods) in terms of utilization, performance and state? A: vRealize Operations Manager provides these capabilities as described [here](https://rguske.github.io/post/monitoring-the-vmware-event-broker-appliance-with-vrealize-operations-manager/). - title: Common Questions - Functions From f4c6c00782f9093412295cd37bdd8f78ae3be6c9 Mon Sep 17 00:00:00 2001 From: Robert Guske Date: Mon, 6 Jun 2022 20:37:08 +0200 Subject: [PATCH 41/54] docs: Added link to dead letter queue explanation to event-router section Closes: #901 Signed-off-by: Robert Guske --- docs/kb/intro-event-router.md | 4 ++-- vmware-event-router/README.MD | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/kb/intro-event-router.md b/docs/kb/intro-event-router.md index eb92443d..a1093dc6 100644 --- a/docs/kb/intro-event-router.md +++ b/docs/kb/intro-event-router.md @@ -64,8 +64,8 @@ not retried and discarded. router crashes **within seconds** right after startup and having received *n* events but before creating the first valid checkpoint (current checkpoint interval is 5s) - If an event cannot be successfully delivered (retried) by an event `processor` it is - logged and discarded, i.e. there is currently no support for dead letter - queues (see note below) + logged and discarded, i.e. there is currently no support for [dead letter + queues](https://en.wikipedia.org/wiki/Dead_letter_queue) (see note below) - Retries in the [OpenFaaS event processor](#processor-type-openfaas) are only supported when running in synchronous mode, i.e. `async: false` (see this OpenFaaS [issue](https://github.com/openfaas/nats-queue-worker/issues/84)) diff --git a/vmware-event-router/README.MD b/vmware-event-router/README.MD index 252511f4..2bd7a547 100644 --- a/vmware-event-router/README.MD +++ b/vmware-event-router/README.MD @@ -49,8 +49,8 @@ not retried and discarded. router crashes **within seconds** right after startup and having received *n* events but before creating the first valid checkpoint (current checkpoint interval is 5s) - If an event cannot be successfully delivered (retried) by an event `processor` it is - logged and discarded, i.e. there is currently no support for dead letter - queues (see note below) + logged and discarded, i.e. there is currently no support for [dead letter + queues](https://en.wikipedia.org/wiki/Dead_letter_queue) (see note below) - Retries in the [OpenFaaS event processor](#processor-type-openfaas) are only supported when running in synchronous mode, i.e. `async: false` (see this OpenFaaS [issue](https://github.com/openfaas/nats-queue-worker/issues/84)) From 68c9bc4c6f9588172cf7c5c26c30abb66efa2421 Mon Sep 17 00:00:00 2001 From: Robert Guske Date: Tue, 7 Jun 2022 17:34:57 +0200 Subject: [PATCH 42/54] docs: Updated vcenter events page with U3 references and data Closes: #903 Signed-off-by: Robert Guske --- docs/kb/use-vcenter-events.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/kb/use-vcenter-events.md b/docs/kb/use-vcenter-events.md index 2b3bfddb..e5aeda87 100644 --- a/docs/kb/use-vcenter-events.md +++ b/docs/kb/use-vcenter-events.md @@ -15,19 +15,19 @@ cta: # vCenter Events -vCenter produces events that get generated in response to actions taken on an entity such as VM, Host, Datastore, etc. These events contain immutable facts documenting the entity state changes such as who initiated the change, what action was performed, which object was modified, and when was the change initiated. +vCenter produces events that get generated in response to actions taken on an entity such as VM, Host, Datastore, etc. These events contain immutable facts documenting the entity state changes such as who initiated the change, what action was performed, which object was modified, and when was the change initiated. Events naturally serve as auditing and troubleshooting tools, allowing an administrator to retrieve details on a specific change. Event Driven Automation builds on the construct of events and enables advanced distributed design patterns driven through Events. VMware Event Broker Appliance aims to enable this for VMware SDDC by enabling VI Administrators to write lean functions (script or code) that are triggered by vCenter Events. ## Overview of the vCenter events -vCenter Events are categorized by the Objects and the actions that are allowed on these objects and are documented under the vSphere API [7.0U2 reference](https://vdc-download.vmware.com/vmwb-repository/dcr-public/8946c1b6-2861-4c12-a45f-f14ae0d3b1b9/a5b8094c-c222-4307-9399-3b606a04af55/vim.event.Event.html){:target="_blank"}. +vCenter Events are categorized by the Objects and the actions that are allowed on these objects and are documented under the vSphere API [7.0U3 reference](https://vdc-download.vmware.com/vmwb-repository/dcr-public/bf660c0a-f060-46e8-a94d-4b5e6ffc77ad/208bc706-e281-49b6-a0ce-b402ec19ef82/SDK/vsphere-ws/docs/ReferenceGuide/vim.event.Event.html){:target="_blank"}. * Event * ClusterEvent * ClusterCreatedEvent, ClusterDestroyedEvent, ClusterOvercommittedEvent... * DatastoreEvent - * DatastoreCapacityIncreasedEvent, DatastoreDestroyedEvent, DatastoreDuplicatedEvent... + * DatastoreCapacityIncreasedEvent, DatastoreDestroyedEvent, DatastoreDuplicatedEvent... * DatacenterEvent * DatacenterCreatedEvent, DatacenterRenamedEvent * HostEvent @@ -36,7 +36,7 @@ vCenter Events are categorized by the Objects and the actions that are allowed o * VmNoNetworkAccessEvent, VmOrphanedEvent, VmPoweredOffEvent... * ... -There are over 1650+ events available on an out of the box install of vCenter that are provided [here](https://github.com/lamw/vcenter-event-mapping/){:target="_blank"}. You can also get the complete list of events for your specific vCenter using the following powershell script below. +There are over 1900+ events available on an out of the box install of vCenter that are provided [here](https://github.com/lamw/vcenter-event-mapping/){:target="_blank"}. You can also get the complete list of events for your specific vCenter using the following powershell script below. ```powershell $vcNames = "hostname" From 90fcd0dce2865f7c322ef0378908403a465df620 Mon Sep 17 00:00:00 2001 From: Robert Guske Date: Wed, 8 Jun 2022 15:36:09 +0200 Subject: [PATCH 43/54] docs: Updated websites community page with veba calls infos Closes: #905 Signed-off-by: Robert Guske --- README.md | 7 +- docs/assets/img/icons/github.svg | 3 + docs/site/community.md | 124 +++++++++++++++++-------------- 3 files changed, 77 insertions(+), 57 deletions(-) create mode 100644 docs/assets/img/icons/github.svg diff --git a/README.md b/README.md index 94054d6d..857eaa47 100644 --- a/README.md +++ b/README.md @@ -128,9 +128,10 @@ page](https://vmweventbroker.io/kb/architecture). Public VEBA community meetings are held every **last Tuesday** in the month at **8AM Pacific Time (US)**. -- **Zoom:** https://via.vmw.com/veba-ama - - **Note:** The meeting is **password protected** to mitigate abuse. Please join the VEBA Slack [channel](#other-channels) to receive the Zoom password or contact us in case of issues. -- **Notes**: https://via.vmw.com/veba-notes +- **Zoom:** + - **Note:** The meeting is **password protected** to mitigate abuse. Please join the VEBA Slack [channel](https://vmwarecode.slack.com/archives/CQLT9B5AA) to receive the Zoom password or contact us in case of issues. +- **Notes**: +- **Recording Playlist**: [VEBA Community Calls](https://youtube.com/playlist?list=PLnopqt07fPn3hspeQvarWuFH3IiwkMpDJ) ### Other Channels diff --git a/docs/assets/img/icons/github.svg b/docs/assets/img/icons/github.svg new file mode 100644 index 00000000..36e758fb --- /dev/null +++ b/docs/assets/img/icons/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/site/community.md b/docs/site/community.md index 157f3302..6ddf00f9 100644 --- a/docs/site/community.md +++ b/docs/site/community.md @@ -11,9 +11,15 @@ links: - description: "Follow us at " url: "https://twitter.com/VMWEventBroker" label: "@VMWEventBroker" +- title: Github + image: /assets/img/icons/github.svg + items: + - description: "Follow us at " + url: "https://github.com/vmware-samples/vcenter-event-broker-appliance" + label: vcenter-event-broker-appliance - title: Slack image: /assets/img/icons/slack.svg - items: + items: - description: "Join us at" url: "https://vmwarecode.slack.com/archives/CQLT9B5AA" label: "#vcenter-event-broker-appliance" @@ -25,10 +31,40 @@ links: label: dl-veba@vmware.com --- +# Get in touch + +
+
+ {% for link in page.links %} +
+
+ {{ link.title}} +
+

{{link.title}}

+ {% for item in link.items %} + + {% endfor %} +
+ {% endfor %} +
+
The VMware Event Broker Appliance team welcomes contributions from the community and this page presents the guidelines for contributing to VMware Event Broker Appliance. -# Guidelines +## Community Calls + +Public VEBA community meetings are held every **last Tuesday** in the month at +**8AM Pacific Time (US)**. + +- **Zoom:** {:target="_blank"} + - **Note:** The meeting is **password protected** to mitigate abuse. Please join the VEBA Slack [channel](https://vmwarecode.slack.com/archives/CQLT9B5AA){:target="_blank"} to receive the Zoom password or contact us in case of issues. +- **Notes**: {:target="_blank"} +- **Recording Playlist**: [VEBA Community Calls](https://youtube.com/playlist?list=PLnopqt07fPn3hspeQvarWuFH3IiwkMpDJ){:target="_blank"} + +## Contributing Following the guidelines helps to make the contribution process easy, collaborative, and productive. @@ -39,7 +75,23 @@ All contributions to this repository must be signed as described on that page. Your signature certifies that you wrote the patch or have the right to pass it on as an open-source patch. -## Submitting Bug Reports and Feature Requests +This is a rough outline of what a contributor's workflow looks like: + +- Create an issue describing the feature/fix. +- Create a topic branch from where you want to base your work. +- Make commits of logical units. +- Make sure your commit messages are in the proper format (see below). +- Push your changes to a topic branch in your fork of the repository. +- Submit a pull request to `vmware-samples/vcenter-event-broker-appliance`. + +See [below](#format-of-the-commit-message) for details on commit best practices +and **supported prefixes**, e.g. `fix: `. + +> **Note:** If you are new to Git(hub) check out [Git rebase, squash...oh +> my!](https://www.mgasch.com/2021/05/git-basics/) for more details on how to +> successfully contribute to an open source project. + +### Submitting Bug Reports and Feature Requests Please submit bug reports and feature requests by using our GitHub [Issues](https://github.com/vmware-samples/vcenter-event-broker-appliance/issues){:target="_blank"} @@ -53,37 +105,18 @@ reproduce the bug? Feature requests should fall within the scope of the project. -## Pull Requests +### Pull Requests Before submitting a pull request, please make sure that your change satisfies the following requirements: + - The change is signed as described by the [Developer Certificate of Origin](https://cla.vmware.com/dco){:target="_blank"} doc. - The change is clearly documented and follows Git commit best practices (see below) -### Git Commit Best Practices - -#### Contribution Flow - -This is a rough outline of what a contributor's workflow looks like: - -- Create an issue describing the feature/fix. -- Create a topic branch from where you want to base your work. -- Make commits of logical units. -- Make sure your commit messages are in the proper format (see below). -- Push your changes to a topic branch in your fork of the repository. -- Submit a pull request to `vmware-samples/vcenter-event-broker-appliance`. - -See [below](#format-of-the-commit-message) for details on commit best practices -and **supported prefixes**, e.g. `fix: `. - -> **Note:** If you are new to Git(hub) check out [Git rebase, squash...oh -> my!](https://www.mgasch.com/2021/05/git-basics/) for more details on how to -> successfully contribute to an open source project. - -#### Format of the Commit Message +### Format of the Commit Message We follow the conventions described in [How to Write a Git Commit Message](http://chris.beams.io/posts/git-commit/). @@ -112,19 +145,22 @@ Currently the following prefixes are used: If your contribution falls into multiple categories, e.g. `feat` and `fix` it is recommended to break up your commits using distinct prefixes. -### Contributions to the Appliance - - See the Build Appliance document [here](/kb/contribute-appliance) - - See the Build Event Router document [here](/kb/contribute-eventrouter) - - Requestor must verify that the VMware Event Broker Appliance can be built and deployed. +### Contributions to the Appliance + +- See the Build Appliance document [here](/kb/contribute-appliance) +- See the Build Event Router document [here](/kb/contribute-eventrouter) +- Requestor must verify that the VMware Event Broker Appliance can be built and deployed. ### Contributions to the Functions - - See the Build Functions document [here](/kb/contribute-functions) - - PR should contain information on how the function was tested (environment, version etc) - - PR should contain a titled readme and the title is listed in the [Functions](/examples) page + +- See the Build Functions document [here](/kb/contribute-functions) +- PR should contain information on how the function was tested (environment, version etc) +- PR should contain a titled readme and the title is listed in the [Functions](/examples) page ### Contributions to the Website - - See the Build Website document [here](/kb/contribute-functions) - - Requestor must verify that the website change was built and tested locally + +- See the Build Website document [here](/kb/contribute-functions) +- Requestor must verify that the website change was built and tested locally Get started quickly with your contributions with our [getting started](/kb/contribute-start) guide @@ -133,23 +169,3 @@ Get started quickly with your contributions with our [getting started](/kb/contr
{% include contributors.html %}
- -## Get in touch -
-
- {% for link in page.links %} -
-
- {{ link.title}} -
-

{{link.title}}

- {% for item in link.items %} - - {% endfor %} -
- {% endfor %} -
-
\ No newline at end of file From 1248bb343c1fa36ba7ec86bc003983008dc0aef3 Mon Sep 17 00:00:00 2001 From: Michael Gasch Date: Tue, 21 Jun 2022 16:20:11 +0200 Subject: [PATCH 44/54] chore: Update workflows Closes: #911 Signed-off-by: Michael Gasch --- .github/dependabot.yml | 11 ++++++++++ .github/workflows/codeql-analysis.yml | 2 +- .github/workflows/gcr-dev-image.yml | 8 ++++---- .github/workflows/gcr-prerelease-image.yml | 8 ++++---- .github/workflows/issue-greeting.yml | 6 +++--- .github/workflows/protect-master.yml | 4 +++- .github/workflows/release.yml | 20 +++++++++---------- .github/workflows/router-build.yml | 18 ++++++++--------- .github/workflows/router-helm.yml | 16 +++++++-------- .../workflows/router-integration-tests.yml | 6 +++--- .github/workflows/router-lint.yml | 8 ++++---- .github/workflows/router-unit-tests.yml | 6 +++--- .github/workflows/stale.yml | 2 +- .github/workflows/verify-gcr-login.yaml | 4 ++-- 14 files changed, 66 insertions(+), 53 deletions(-) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..0d72e79a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: +- package-ecosystem: gomod + directory: "/" + schedule: + interval: daily + open-pull-requests-limit: 10 +- package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "daily" \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index fc6f17d8..ed62dece 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -39,7 +39,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v2 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL diff --git a/.github/workflows/gcr-dev-image.yml b/.github/workflows/gcr-dev-image.yml index d620fa62..2547bfd7 100644 --- a/.github/workflows/gcr-dev-image.yml +++ b/.github/workflows/gcr-dev-image.yml @@ -19,7 +19,7 @@ jobs: image: strategy: matrix: - go-version: ["1.17"] + go-version: ["1.18"] platform: ["ubuntu-latest"] runs-on: ${{ matrix.platform }} @@ -32,15 +32,15 @@ jobs: uses: imjasonh/setup-ko@2c3450ca27f6e6f2b02e72a40f2163c281a1f675 # v0.4 - name: Set up Go ${{ matrix.go-version }} - uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4 # v2.1.5 + uses: actions/setup-go@b22fbbc2921299758641fab08929b4ac52b32923 with: go-version: ${{ matrix.go-version }} id: go - - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b - name: Login to GCP - uses: google-github-actions/setup-gcloud@37a9333538a8350a13fe9d8fa03e0d4742a1ad2e # 0.5.0 + uses: google-github-actions/setup-gcloud@877d4953d2c70a0ba7ef3290ae968eb24af233bb with: project_id: ${{ secrets.GCP_PROJECT_ID }} service_account_email: ${{ secrets.GCP_EMAIL }} diff --git a/.github/workflows/gcr-prerelease-image.yml b/.github/workflows/gcr-prerelease-image.yml index b227881f..d6b8925b 100644 --- a/.github/workflows/gcr-prerelease-image.yml +++ b/.github/workflows/gcr-prerelease-image.yml @@ -14,7 +14,7 @@ jobs: image: strategy: matrix: - go-version: ["1.17"] + go-version: ["1.18"] platform: ["ubuntu-latest"] runs-on: ${{ matrix.platform }} @@ -27,15 +27,15 @@ jobs: uses: imjasonh/setup-ko@2c3450ca27f6e6f2b02e72a40f2163c281a1f675 # v0.4 - name: Set up Go ${{ matrix.go-version }} - uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4 # v2.1.5 + uses: actions/setup-go@b22fbbc2921299758641fab08929b4ac52b32923 with: go-version: ${{ matrix.go-version }} id: go - - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b - name: Login to GCP - uses: google-github-actions/setup-gcloud@37a9333538a8350a13fe9d8fa03e0d4742a1ad2e # 0.5.0 + uses: google-github-actions/setup-gcloud@877d4953d2c70a0ba7ef3290ae968eb24af233bb with: project_id: ${{ secrets.GCP_PROJECT_ID }} service_account_email: ${{ secrets.GCP_EMAIL }} diff --git a/.github/workflows/issue-greeting.yml b/.github/workflows/issue-greeting.yml index 7e7edb07..3d67acc5 100644 --- a/.github/workflows/issue-greeting.yml +++ b/.github/workflows/issue-greeting.yml @@ -14,20 +14,20 @@ jobs: steps: - name: Checkout - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b with: fetch-depth: 1 - name: Render template id: template - uses: chuhlomin/render-template@f828bb5c72a3e3af89cb79808cea490166c6f1ce # v1.4 + uses: chuhlomin/render-template@69462090a6315efa50069855670b3a4abab20512 with: template: .github/comment-template.md vars: | author: ${{ github.actor }} - name: Create comment - uses: peter-evans/create-or-update-comment@a35cf36e5301d70b76f316e867e7788a55a31dae # v1.4.5 + uses: peter-evans/create-or-update-comment@c9fcb64660bc90ec1cc535646af190c992007c32 with: issue-number: ${{ github.event.issue.number }} body: ${{ steps.template.outputs.result }} diff --git a/.github/workflows/protect-master.yml b/.github/workflows/protect-master.yml index 56a1f521..4a2762cf 100644 --- a/.github/workflows/protect-master.yml +++ b/.github/workflows/protect-master.yml @@ -16,4 +16,6 @@ jobs: if: github.event_name == 'pull_request' && github.base_ref == 'master' steps: - name: Must reject PR - run: exit 1 + run: | + echo "::error:: pull requests must not be made against master branch" + exit 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9f2adc87..e8e4427e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,19 +17,19 @@ jobs: timeout-minutes: 30 strategy: matrix: - go-version: ["1.17"] + go-version: ["1.18"] platform: ["ubuntu-latest"] runs-on: ${{ matrix.platform }} steps: - name: Checkout - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b with: fetch-depth: 0 ref: "master" - name: Set up Go - uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4 # v2.1.5 + uses: actions/setup-go@b22fbbc2921299758641fab08929b4ac52b32923 with: go-version: ${{ matrix.go-version }} @@ -44,7 +44,7 @@ jobs: docker run --rm -v $PWD:/workdir ${IMAGE}@sha256:${IMAGE_SHA} -o vmware-event-router/RELEASE_CHANGELOG.md $(basename "${{ github.ref }}" ) - name: GoReleaser - uses: goreleaser/goreleaser-action@79d4afbba1b4eff8b9a98e3d2e58c4dbaf094e2b # v2.8.1 + uses: goreleaser/goreleaser-action@68acf3b1adf004ac9c2f0a4259e85c5f66e99bef with: args: release --rm-dist --release-notes RELEASE_CHANGELOG.md workdir: vmware-event-router @@ -61,7 +61,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b with: # for changelog fetch-depth: 0 @@ -85,7 +85,7 @@ jobs: - name: Create Pull Request id: cpr - uses: peter-evans/create-pull-request@f22a7da129c901513876a2380e2dae9f8e145330 # v3.12.1 + uses: peter-evans/create-pull-request@923ad837f191474af6b1721408744feb989a4c27 with: delete-branch: true reviewers: embano1 @@ -106,7 +106,7 @@ jobs: KO_DOCKER_REPO: us.gcr.io/daisy-284300/veba # .../router@sha256: strategy: matrix: - go-version: ["1.17"] + go-version: ["1.18"] platform: ["ubuntu-latest"] runs-on: ${{ matrix.platform }} @@ -115,15 +115,15 @@ jobs: uses: imjasonh/setup-ko@2c3450ca27f6e6f2b02e72a40f2163c281a1f675 # v0.4 - name: Set up Go ${{ matrix.go-version }} - uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4 # v2.1.5 + uses: actions/setup-go@b22fbbc2921299758641fab08929b4ac52b32923 with: go-version: ${{ matrix.go-version }} id: go - - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b - name: Login to GCP - uses: google-github-actions/setup-gcloud@37a9333538a8350a13fe9d8fa03e0d4742a1ad2e # 0.5.0 + uses: google-github-actions/setup-gcloud@877d4953d2c70a0ba7ef3290ae968eb24af233bb with: project_id: ${{ secrets.GCP_PROJECT_ID }} service_account_email: ${{ secrets.GCP_EMAIL }} diff --git a/.github/workflows/router-build.yml b/.github/workflows/router-build.yml index da44e293..4fa794ac 100644 --- a/.github/workflows/router-build.yml +++ b/.github/workflows/router-build.yml @@ -22,7 +22,7 @@ jobs: name: Build binaries strategy: matrix: - go-version: ["1.17"] + go-version: ["1.18"] platform: ["ubuntu-latest"] runs-on: ${{ matrix.platform }} @@ -30,16 +30,16 @@ jobs: steps: - name: Set up Go ${{ matrix.go-version }} - uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4 # v2.1.5 + uses: actions/setup-go@b22fbbc2921299758641fab08929b4ac52b32923 with: go-version: ${{ matrix.go-version }} id: go - name: Check out code - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b - name: GoReleaser Snapshot - uses: goreleaser/goreleaser-action@79d4afbba1b4eff8b9a98e3d2e58c4dbaf094e2b # v2.8.1 + uses: goreleaser/goreleaser-action@68acf3b1adf004ac9c2f0a4259e85c5f66e99bef with: version: latest args: release --rm-dist --snapshot @@ -48,7 +48,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Archive run artifacts - uses: actions/upload-artifact@82c141cc518b40d92cc801eee768e7aafc9c2fa2 # v2.3.1 + uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 with: name: dist path: | @@ -60,7 +60,7 @@ jobs: name: Verify Release ko artifact (no upload) strategy: matrix: - go-version: ["1.17"] + go-version: ["1.18"] platform: ["ubuntu-latest"] runs-on: ${{ matrix.platform }} @@ -73,13 +73,13 @@ jobs: uses: imjasonh/setup-ko@2c3450ca27f6e6f2b02e72a40f2163c281a1f675 # v0.4 - name: Set up Go ${{ matrix.go-version }} - uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4 # v2.1.5 + uses: actions/setup-go@b22fbbc2921299758641fab08929b4ac52b32923 with: go-version: ${{ matrix.go-version }} id: go - name: Check out code - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b - name: Get short COMMIT and TAG run: | @@ -92,7 +92,7 @@ jobs: ko resolve --platform=linux/arm64,linux/amd64 --push=false --tags ${KO_TAG},${KO_COMMIT},latest -BRf deploy/event-router-k8s.yaml > release.yaml - name: Archive run artifacts - uses: actions/upload-artifact@82c141cc518b40d92cc801eee768e7aafc9c2fa2 # v2.3.1 + uses: actions/upload-artifact@3cea5372237819ed00197afe530f5a7ea3e805c8 with: name: release path: | diff --git a/.github/workflows/router-helm.yml b/.github/workflows/router-helm.yml index 10a3d9f9..a6315b92 100644 --- a/.github/workflows/router-helm.yml +++ b/.github/workflows/router-helm.yml @@ -23,16 +23,16 @@ jobs: timeout-minutes: 15 steps: - - name: Set up Go 1.17.x - uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4 # v2.1.5 + - name: Set up Go 1.18.x + uses: actions/setup-go@b22fbbc2921299758641fab08929b4ac52b32923 with: - go-version: 1.17 + go-version: 1.18 - name: Setup ko uses: imjasonh/setup-ko@2c3450ca27f6e6f2b02e72a40f2163c281a1f675 # v0.4 - name: Check out code onto GOPATH - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b with: fetch-depth: 1 @@ -135,16 +135,16 @@ jobs: timeout-minutes: 15 steps: - - name: Set up Go 1.17.x - uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4 # v2.1.5 + - name: Set up Go 1.18.x + uses: actions/setup-go@b22fbbc2921299758641fab08929b4ac52b32923 with: - go-version: 1.17 + go-version: 1.18 - name: Setup ko uses: imjasonh/setup-ko@2c3450ca27f6e6f2b02e72a40f2163c281a1f675 # v0.4 - name: Check out code onto GOPATH - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b with: fetch-depth: 1 diff --git a/.github/workflows/router-integration-tests.yml b/.github/workflows/router-integration-tests.yml index 042b8b06..f3e22dd4 100644 --- a/.github/workflows/router-integration-tests.yml +++ b/.github/workflows/router-integration-tests.yml @@ -21,7 +21,7 @@ jobs: integration-tests: strategy: matrix: - go-version: ["1.17"] + go-version: ["1.18"] platform: ["ubuntu-latest"] runs-on: ${{ matrix.platform }} @@ -29,13 +29,13 @@ jobs: steps: - name: Set up Go ${{ matrix.go-version }} - uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4 # v2.1.5 + uses: actions/setup-go@b22fbbc2921299758641fab08929b4ac52b32923 with: go-version: ${{ matrix.go-version }} id: go - name: Check out code - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b - name: Run OpenFaaS integration tests run: hack/run_integration_tests.sh diff --git a/.github/workflows/router-lint.yml b/.github/workflows/router-lint.yml index a2260e0d..9d40df1e 100644 --- a/.github/workflows/router-lint.yml +++ b/.github/workflows/router-lint.yml @@ -16,7 +16,7 @@ jobs: name: lint strategy: matrix: - go-version: ["1.17"] + go-version: ["1.18"] platform: ["ubuntu-latest"] runs-on: ${{ matrix.platform }} @@ -24,15 +24,15 @@ jobs: steps: - name: Set up Go ${{ matrix.go-version }} - uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4 # v2.1.5 + uses: actions/setup-go@b22fbbc2921299758641fab08929b4ac52b32923 with: go-version: ${{ matrix.go-version }} id: go - - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0 + - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b - name: golangci-lint - uses: golangci/golangci-lint-action@5c56cd6c9dc07901af25baab6f2b0d9f3b7c3018 # v2.5.2 + uses: golangci/golangci-lint-action@537aa1903e5d359d0b27dbc19ddd22c5087f3fbc with: # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version. version: latest diff --git a/.github/workflows/router-unit-tests.yml b/.github/workflows/router-unit-tests.yml index 4302509c..2dfed812 100644 --- a/.github/workflows/router-unit-tests.yml +++ b/.github/workflows/router-unit-tests.yml @@ -22,7 +22,7 @@ jobs: name: Unit Tests strategy: matrix: - go-version: ["1.17"] + go-version: ["1.18"] platform: ["ubuntu-latest"] runs-on: ${{ matrix.platform }} @@ -30,13 +30,13 @@ jobs: steps: - name: Set up Go ${{ matrix.go-version }} - uses: actions/setup-go@424fc82d43fa5a37540bae62709ddcc23d9520d4 # v2.1.5 + uses: actions/setup-go@b22fbbc2921299758641fab08929b4ac52b32923 with: go-version: ${{ matrix.go-version }} id: go - name: Check out code - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b - name: run unit tests run: make unit-test diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 9f3ab307..5be71b5e 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -8,7 +8,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@7fb802b3079a276cf3c7e6ba9aa003c665b3f838 # v4.1.0 + - uses: actions/stale@3cc123766321e9f15a6676375c154ccffb12a358 with: repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/verify-gcr-login.yaml b/.github/workflows/verify-gcr-login.yaml index e43166ea..326ae589 100644 --- a/.github/workflows/verify-gcr-login.yaml +++ b/.github/workflows/verify-gcr-login.yaml @@ -14,10 +14,10 @@ jobs: steps: - name: Check out code - uses: actions/checkout@ec3a7ce113134d7a93b817d10a8272cb61118579 # v2.4.0 + uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b - name: Login to GCP - uses: google-github-actions/setup-gcloud@37a9333538a8350a13fe9d8fa03e0d4742a1ad2e # 0.5.0 + uses: google-github-actions/setup-gcloud@877d4953d2c70a0ba7ef3290ae968eb24af233bb with: project_id: ${{ secrets.GCP_PROJECT_ID }} service_account_email: ${{ secrets.GCP_EMAIL }} From 6e821c26768f84540c7343ced22d2ed93a11c004 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Jun 2022 15:03:14 +0000 Subject: [PATCH 45/54] chore(deps): Bump github/codeql-action from 1 to 2 Bumps [github/codeql-action](https://github.com/github/codeql-action) from 1 to 2. - [Release notes](https://github.com/github/codeql-action/releases) - [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/github/codeql-action/compare/v1...v2) --- updated-dependencies: - dependency-name: github/codeql-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/codeql-analysis.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index ed62dece..4f401f0f 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -43,7 +43,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@v1 + uses: github/codeql-action/init@v2 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. @@ -54,7 +54,7 @@ jobs: # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). # If this step fails, then you should remove it and run the build manually (see below) - name: Autobuild - uses: github/codeql-action/autobuild@v1 + uses: github/codeql-action/autobuild@v2 # ℹ️ Command-line programs to run using the OS shell. # 📚 https://git.io/JvXDl @@ -68,4 +68,4 @@ jobs: # make release - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v1 + uses: github/codeql-action/analyze@v2 From e1f7f12c39af2f626ecee1575cde6397a32357b4 Mon Sep 17 00:00:00 2001 From: Michael Gasch Date: Fri, 17 Jun 2022 16:13:30 +0200 Subject: [PATCH 46/54] chore: Go formatting Signed-off-by: Michael Gasch --- vmware-event-router/cmd/schemagen/main.go | 1 - vmware-event-router/internal/events/events_test.go | 3 +-- .../internal/integration/events_test.go | 7 +++---- .../internal/integration/openfaas_test.go | 2 +- .../internal/integration/suite_openfaas_test.go | 2 +- vmware-event-router/internal/metrics/metrics.go | 1 - vmware-event-router/internal/metrics/server.go | 5 +---- .../internal/processor/openfaas/openfaas_test.go | 1 - .../internal/processor/openfaas/retry_test.go | 1 - .../internal/processor/processor_test.go | 1 - .../internal/provider/fake/vcenter_fake_test.go | 1 - .../provider/horizon/client_internal_test.go | 5 ++--- .../internal/provider/horizon/horizon.go | 14 ++++++-------- .../provider/horizon/horizon_internal_test.go | 5 ++--- .../internal/provider/horizon/horizon_test.go | 3 +-- .../internal/provider/vcenter/checkpoint.go | 12 ++++-------- .../internal/provider/vcenter/checkpoint_test.go | 4 ++-- .../internal/provider/vcenter/vcenter.go | 2 +- .../internal/provider/vcenter/vcenter_test.go | 1 - .../internal/provider/vcsim/vcsim_test.go | 4 +--- .../internal/provider/webhook/webhook.go | 6 ++---- .../provider/webhook/webhook_internal_test.go | 1 - .../internal/provider/webhook/webhook_test.go | 10 +++++----- vmware-event-router/internal/util/util_test.go | 1 - 24 files changed, 33 insertions(+), 60 deletions(-) diff --git a/vmware-event-router/cmd/schemagen/main.go b/vmware-event-router/cmd/schemagen/main.go index 4865da76..155ba8fd 100644 --- a/vmware-event-router/cmd/schemagen/main.go +++ b/vmware-event-router/cmd/schemagen/main.go @@ -18,7 +18,6 @@ func main() { s := jsonschema.Reflect(&config.RouterConfig{}) b, err := s.MarshalJSON() - if err != nil { log.Fatalf("could not marshal to JSON: %v", err) } diff --git a/vmware-event-router/internal/events/events_test.go b/vmware-event-router/internal/events/events_test.go index c3d9a25a..3129e237 100644 --- a/vmware-event-router/internal/events/events_test.go +++ b/vmware-event-router/internal/events/events_test.go @@ -1,5 +1,4 @@ //go:build unit -// +build unit package events @@ -9,7 +8,7 @@ import ( cloudevents "github.com/cloudevents/sdk-go/v2" "github.com/vmware/govmomi/vim25/types" - "gotest.tools/assert" + "gotest.tools/v3/assert" ) func Test_GetEventDetails(t *testing.T) { diff --git a/vmware-event-router/internal/integration/events_test.go b/vmware-event-router/internal/integration/events_test.go index 896408ca..ece9b01c 100644 --- a/vmware-event-router/internal/integration/events_test.go +++ b/vmware-event-router/internal/integration/events_test.go @@ -1,11 +1,10 @@ //go:build integration -// +build integration package integration_test import "github.com/vmware/govmomi/vim25/types" -//nolint +// nolint func newVMPoweredOnEvent() types.BaseEvent { return &types.VmPoweredOnEvent{ VmEvent: types.VmEvent{ @@ -24,12 +23,12 @@ func newVMPoweredOnEvent() types.BaseEvent { } } -//nolint +// nolint func newLicenseEvent() types.BaseEvent { return types.BaseEvent(&types.LicenseEvent{}) } -//nolint +// nolint func newClusterCreatedEvent() types.BaseEvent { return types.BaseEvent(&types.ClusterCreatedEvent{}) } diff --git a/vmware-event-router/internal/integration/openfaas_test.go b/vmware-event-router/internal/integration/openfaas_test.go index d3ff6421..ade0781c 100644 --- a/vmware-event-router/internal/integration/openfaas_test.go +++ b/vmware-event-router/internal/integration/openfaas_test.go @@ -1,4 +1,4 @@ -// +build integration,openfaas +//go:build integration && openfaas package integration_test diff --git a/vmware-event-router/internal/integration/suite_openfaas_test.go b/vmware-event-router/internal/integration/suite_openfaas_test.go index b9c85594..57a18bb9 100644 --- a/vmware-event-router/internal/integration/suite_openfaas_test.go +++ b/vmware-event-router/internal/integration/suite_openfaas_test.go @@ -1,4 +1,4 @@ -// +build integration,openfaas +//go:build integration && openfaas package integration_test diff --git a/vmware-event-router/internal/metrics/metrics.go b/vmware-event-router/internal/metrics/metrics.go index 61614f31..b3a9347b 100644 --- a/vmware-event-router/internal/metrics/metrics.go +++ b/vmware-event-router/internal/metrics/metrics.go @@ -84,7 +84,6 @@ func loadAvg(position int) float64 { values := strings.Fields(string(data)) load, err := strconv.ParseFloat(values[position], 64) - if err != nil { return 0 } diff --git a/vmware-event-router/internal/metrics/server.go b/vmware-event-router/internal/metrics/server.go index 9996c510..0bedaecc 100644 --- a/vmware-event-router/internal/metrics/server.go +++ b/vmware-event-router/internal/metrics/server.go @@ -24,9 +24,7 @@ const ( endpoint = "/stats" ) -var ( - eventRouterStats = expvar.NewMap(mapName) -) +var eventRouterStats = expvar.NewMap(mapName) // Receiver receives metrics from metric providers type Receiver interface { @@ -132,7 +130,6 @@ func withBasicAuth(log logger.Logger, next http.Handler, u, p string) http.Handl if !ok || !(p == password && u == user) { w.WriteHeader(http.StatusUnauthorized) _, err := w.Write([]byte("invalid credentials")) - if err != nil { log.Errorf("could not write http response: %v", err) } diff --git a/vmware-event-router/internal/processor/openfaas/openfaas_test.go b/vmware-event-router/internal/processor/openfaas/openfaas_test.go index 700d4850..2a65d9b7 100644 --- a/vmware-event-router/internal/processor/openfaas/openfaas_test.go +++ b/vmware-event-router/internal/processor/openfaas/openfaas_test.go @@ -1,5 +1,4 @@ //go:build unit -// +build unit package openfaas diff --git a/vmware-event-router/internal/processor/openfaas/retry_test.go b/vmware-event-router/internal/processor/openfaas/retry_test.go index 890a8beb..40e0671c 100644 --- a/vmware-event-router/internal/processor/openfaas/retry_test.go +++ b/vmware-event-router/internal/processor/openfaas/retry_test.go @@ -1,5 +1,4 @@ //go:build unit -// +build unit package openfaas diff --git a/vmware-event-router/internal/processor/processor_test.go b/vmware-event-router/internal/processor/processor_test.go index 615b1723..d4ecefa6 100644 --- a/vmware-event-router/internal/processor/processor_test.go +++ b/vmware-event-router/internal/processor/processor_test.go @@ -1,5 +1,4 @@ //go:build unit -// +build unit package processor diff --git a/vmware-event-router/internal/provider/fake/vcenter_fake_test.go b/vmware-event-router/internal/provider/fake/vcenter_fake_test.go index c841ff36..77808dea 100644 --- a/vmware-event-router/internal/provider/fake/vcenter_fake_test.go +++ b/vmware-event-router/internal/provider/fake/vcenter_fake_test.go @@ -1,5 +1,4 @@ //go:build unit -// +build unit package fake diff --git a/vmware-event-router/internal/provider/horizon/client_internal_test.go b/vmware-event-router/internal/provider/horizon/client_internal_test.go index 5deee453..c0c3149f 100644 --- a/vmware-event-router/internal/provider/horizon/client_internal_test.go +++ b/vmware-event-router/internal/provider/horizon/client_internal_test.go @@ -1,5 +1,4 @@ //go:build unit -// +build unit package horizon @@ -14,7 +13,7 @@ import ( "time" "go.uber.org/zap/zaptest" - "gotest.tools/assert" + "gotest.tools/v3/assert" ) const ( @@ -300,7 +299,7 @@ func (h *horizonAPIMock) logoutHandler(w http.ResponseWriter, r *http.Request) { func randomToken(n int) string { rand.Seed(time.Now().Unix()) - var letter = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") + letter := []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789") b := make([]rune, n) for i := range b { b[i] = letter[rand.Intn(len(letter))] diff --git a/vmware-event-router/internal/provider/horizon/horizon.go b/vmware-event-router/internal/provider/horizon/horizon.go index cb772882..22c1aef7 100644 --- a/vmware-event-router/internal/provider/horizon/horizon.go +++ b/vmware-event-router/internal/provider/horizon/horizon.go @@ -30,14 +30,12 @@ const ( eventTypeScheme = "%s/horizon.%s.v0" // router prefix + normalized event type ) -var ( - defaultBackoff = backoff.Backoff{ - Factor: 2, - Jitter: false, - Min: time.Second, - Max: 5 * time.Second, - } -) +var defaultBackoff = backoff.Backoff{ + Factor: 2, + Jitter: false, + Min: time.Second, + Max: 5 * time.Second, +} // EventStream handles the connection to the Horizon events API type EventStream struct { diff --git a/vmware-event-router/internal/provider/horizon/horizon_internal_test.go b/vmware-event-router/internal/provider/horizon/horizon_internal_test.go index 6ce31070..064a229d 100644 --- a/vmware-event-router/internal/provider/horizon/horizon_internal_test.go +++ b/vmware-event-router/internal/provider/horizon/horizon_internal_test.go @@ -1,5 +1,4 @@ //go:build unit -// +build unit package horizon @@ -16,7 +15,7 @@ import ( ce "github.com/cloudevents/sdk-go/v2" "github.com/jpillora/backoff" "go.uber.org/zap/zaptest" - "gotest.tools/assert" + "gotest.tools/v3/assert" config "github.com/vmware-samples/vcenter-event-broker-appliance/vmware-event-router/internal/config/v1alpha1" "github.com/vmware-samples/vcenter-event-broker-appliance/vmware-event-router/internal/logger" @@ -192,7 +191,7 @@ func (f *fakeClient) GetEvents(_ context.Context, _ Timestamp) ([]AuditEventSumm f.log.Debugf("GetEvents invocations: %d", f.invocations) // preserve existing events slice - var newEvents = make([]AuditEventSummary, len(f.events)) + newEvents := make([]AuditEventSummary, len(f.events)) copy(newEvents, f.events) // Horizon API returns events ordered from newest to oldest diff --git a/vmware-event-router/internal/provider/horizon/horizon_test.go b/vmware-event-router/internal/provider/horizon/horizon_test.go index 52212d68..3923b89e 100644 --- a/vmware-event-router/internal/provider/horizon/horizon_test.go +++ b/vmware-event-router/internal/provider/horizon/horizon_test.go @@ -1,5 +1,4 @@ //go:build unit -// +build unit package horizon_test @@ -15,7 +14,7 @@ import ( "github.com/vmware-samples/vcenter-event-broker-appliance/vmware-event-router/internal/logger" "go.uber.org/zap/zaptest" - "gotest.tools/assert" + "gotest.tools/v3/assert" "knative.dev/pkg/logging" config "github.com/vmware-samples/vcenter-event-broker-appliance/vmware-event-router/internal/config/v1alpha1" diff --git a/vmware-event-router/internal/provider/vcenter/checkpoint.go b/vmware-event-router/internal/provider/vcenter/checkpoint.go index 82a924d4..75b9c4e4 100644 --- a/vmware-event-router/internal/provider/vcenter/checkpoint.go +++ b/vmware-event-router/internal/provider/vcenter/checkpoint.go @@ -21,9 +21,7 @@ const ( format = "cp-%s.json" // cp-.json ) -var ( - errInvalidEvent = errors.New("invalid event") -) +var errInvalidEvent = errors.New("invalid event") // checkpoint represents a checkpoint object type checkpoint struct { @@ -48,9 +46,7 @@ type checkpoint struct { // (i.e. default values) and it is the caller's responsibility to check for // validity using time.IsZero() on any timestamp. func getCheckpoint(ctx context.Context, host, dir string) (cp *checkpoint, path string, err error) { - var ( - skip bool - ) + var skip bool file := fileName(host) path = fullPath(file, dir) @@ -90,7 +86,7 @@ func initCheckpoint(_ context.Context, fullPath string) (*checkpoint, error) { dir := filepath.Dir(fullPath) // create if not exists - err := os.MkdirAll(dir, 0755) + err := os.MkdirAll(dir, 0o755) if err != nil { return nil, errors.Wrap(err, "could not create checkpoint directory") } @@ -102,7 +98,7 @@ func initCheckpoint(_ context.Context, fullPath string) (*checkpoint, error) { return nil, errors.Wrap(err, "could not marshal checkpoint to JSON object") } - err = ioutil.WriteFile(fullPath, jsonBytes, 0600) + err = ioutil.WriteFile(fullPath, jsonBytes, 0o600) if err != nil { return nil, errors.Wrap(err, "could not write checkpoint file") } diff --git a/vmware-event-router/internal/provider/vcenter/checkpoint_test.go b/vmware-event-router/internal/provider/vcenter/checkpoint_test.go index 3ff279b2..515855b3 100644 --- a/vmware-event-router/internal/provider/vcenter/checkpoint_test.go +++ b/vmware-event-router/internal/provider/vcenter/checkpoint_test.go @@ -1,5 +1,4 @@ //go:build unit -// +build unit package vcenter @@ -122,7 +121,8 @@ func Test_checkpoint(t *testing.T) { VmEvent: types.VmEvent{ Event: types.Event{ Key: lastEventKey, - CreatedTime: lastEventKeyTime}, + CreatedTime: lastEventKeyTime, + }, }, }, uuid: lastEventUUID, diff --git a/vmware-event-router/internal/provider/vcenter/vcenter.go b/vmware-event-router/internal/provider/vcenter/vcenter.go index 9d88d506..54f99461 100644 --- a/vmware-event-router/internal/provider/vcenter/vcenter.go +++ b/vmware-event-router/internal/provider/vcenter/vcenter.go @@ -283,7 +283,7 @@ func (vc *EventStream) stream(ctx context.Context, p processor.Processor, collec path := fullPath(f, dir) // always create/overwrite (existing) checkpoint - file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600) + file, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0o600) if err != nil { return errors.Wrap(err, "create checkpoint file") } diff --git a/vmware-event-router/internal/provider/vcenter/vcenter_test.go b/vmware-event-router/internal/provider/vcenter/vcenter_test.go index b4e997ff..98857015 100644 --- a/vmware-event-router/internal/provider/vcenter/vcenter_test.go +++ b/vmware-event-router/internal/provider/vcenter/vcenter_test.go @@ -1,5 +1,4 @@ //go:build unit -// +build unit package vcenter diff --git a/vmware-event-router/internal/provider/vcsim/vcsim_test.go b/vmware-event-router/internal/provider/vcsim/vcsim_test.go index f2a291b0..cefbc550 100644 --- a/vmware-event-router/internal/provider/vcsim/vcsim_test.go +++ b/vmware-event-router/internal/provider/vcsim/vcsim_test.go @@ -1,5 +1,4 @@ //go:build unit -// +build unit package vcsim @@ -135,8 +134,7 @@ func Test_eventHandler(t *testing.T) { } // fakeProcessor implements the processor interface -type fakeProcessor struct { -} +type fakeProcessor struct{} func (f fakeProcessor) PushMetrics(_ context.Context, _ metrics.Receiver) { } diff --git a/vmware-event-router/internal/provider/webhook/webhook.go b/vmware-event-router/internal/provider/webhook/webhook.go index fbd8260d..5fbe6e6c 100644 --- a/vmware-event-router/internal/provider/webhook/webhook.go +++ b/vmware-event-router/internal/provider/webhook/webhook.go @@ -41,10 +41,8 @@ const ( allowedRate = 1000 ) -var ( - // ErrInvalidPath is returned on an invalid webhook endpoint path - ErrInvalidPath = errors.New("invalid webhook endpoint path") -) +// ErrInvalidPath is returned on an invalid webhook endpoint path +var ErrInvalidPath = errors.New("invalid webhook endpoint path") // Server is a webhook event provider type Server struct { diff --git a/vmware-event-router/internal/provider/webhook/webhook_internal_test.go b/vmware-event-router/internal/provider/webhook/webhook_internal_test.go index 2466cdb0..83e52c86 100644 --- a/vmware-event-router/internal/provider/webhook/webhook_internal_test.go +++ b/vmware-event-router/internal/provider/webhook/webhook_internal_test.go @@ -1,5 +1,4 @@ //go:build unit -// +build unit package webhook diff --git a/vmware-event-router/internal/provider/webhook/webhook_test.go b/vmware-event-router/internal/provider/webhook/webhook_test.go index e591c1ae..3c2f5f8b 100644 --- a/vmware-event-router/internal/provider/webhook/webhook_test.go +++ b/vmware-event-router/internal/provider/webhook/webhook_test.go @@ -1,5 +1,4 @@ //go:build unit -// +build unit package webhook_test @@ -11,14 +10,15 @@ import ( ce "github.com/cloudevents/sdk-go/v2" cehttp "github.com/cloudevents/sdk-go/v2/protocol/http" - config "github.com/vmware-samples/vcenter-event-broker-appliance/vmware-event-router/internal/config/v1alpha1" - "github.com/vmware-samples/vcenter-event-broker-appliance/vmware-event-router/internal/metrics" - "github.com/vmware-samples/vcenter-event-broker-appliance/vmware-event-router/internal/provider/webhook" "go.uber.org/zap" "go.uber.org/zap/zaptest" "golang.org/x/sync/errgroup" - "gotest.tools/assert" + "gotest.tools/v3/assert" "knative.dev/pkg/logging" + + config "github.com/vmware-samples/vcenter-event-broker-appliance/vmware-event-router/internal/config/v1alpha1" + "github.com/vmware-samples/vcenter-event-broker-appliance/vmware-event-router/internal/metrics" + "github.com/vmware-samples/vcenter-event-broker-appliance/vmware-event-router/internal/provider/webhook" ) func Test_WebhookServer(t *testing.T) { diff --git a/vmware-event-router/internal/util/util_test.go b/vmware-event-router/internal/util/util_test.go index 1b6e16d8..fab8560b 100644 --- a/vmware-event-router/internal/util/util_test.go +++ b/vmware-event-router/internal/util/util_test.go @@ -1,5 +1,4 @@ //go:build unit -// +build unit package util From 643a0e608692029f46799ed7cec8e09f8a4386b9 Mon Sep 17 00:00:00 2001 From: Michael Gasch Date: Fri, 17 Jun 2022 16:13:48 +0200 Subject: [PATCH 47/54] feat: Enhanced pattern matching for EventBridge Changes how the Event Router interprets AWS EventBridge rules (patterns). Previously only simple patterns based on subject field were supported. This change introduces the Quamina library which supports several EventBridge rules and patterns. The router will parse and use the configured rule (ARN) if it's a valid Quamina pattern to decide which events to forward to EventBridge. Closes: #909 Signed-off-by: Michael Gasch --- docs/kb/intro-event-router.md | 56 ++- vmware-event-router/README.MD | 56 ++- vmware-event-router/go.mod | 5 +- vmware-event-router/go.sum | 11 +- .../internal/integration/aws_test.go | 11 +- .../internal/integration/suite_aws_test.go | 41 +- .../processor/aws/aws_event_bridge.go | 359 +++++++---------- .../processor/aws/aws_event_bridge_test.go | 364 ++++++++++++++++++ .../internal/processor/aws/options.go | 16 +- 9 files changed, 660 insertions(+), 259 deletions(-) create mode 100644 vmware-event-router/internal/processor/aws/aws_event_bridge_test.go diff --git a/docs/kb/intro-event-router.md b/docs/kb/intro-event-router.md index ac05cfa8..33509fee 100644 --- a/docs/kb/intro-event-router.md +++ b/docs/kb/intro-event-router.md @@ -50,7 +50,7 @@ to normalize events from the supported event `providers`. See - with the [Horizon event provider](#provider-type-horizon) - with the [vcsim event provider](#provider-type-vcsim) -**Note:** All implemented event `processors` use built-in retry mechanisms so +> **Note:** All implemented event `processors` use built-in retry mechanisms so your function might still be involved multiple times depending on its response code. However, if an event `provider` crashes before sending an event to the configured `processor` or when the `processor` returns an error, the event is @@ -429,15 +429,21 @@ only forwards events configured in the associated `rule` of an event bus. Rules in AWS EventBridge use pattern matching ([docs](https://docs.aws.amazon.com/eventbridge/latest/userguide/filtering-examples-structure.html)). Upon start, VMware Event Router contacts EventBridge (using the given IAM role) -to parse and extract event categories from the configured rule ARN (see -configuration option below). +to parse the configured rule ARN (see configuration option below). -The VMware Event Router uses the `"subject"` field in the event payload to store -the event category, e.g. `"VmPoweredOnEvent"`. Thus it is required that you use -a **specific pattern match** (`"detail->subject"`) that the VMware Event Router -can parse to retrieve the desired event (forwarding) categories. For example, -the following AWS EventBridge event pattern rule matches power on/off events -(including DRS-enabled clusters): +The VMware Event Router uses the pattern match library which supports a subset +of the EventBridge pattern rules. You may only use these supported patterns in +your specified EventBridge rule. Refer to [this +page](https://github.com/timbray/quamina/blob/v0.2.0/PATTERNS.md) for the +currently supported patterns in `Quamina`. + +> **Note:** EventBridge wraps each VMware Event Router event (CloudEvent) into +> an EventBridge message envelop. The `detail` field contains the JSON +> representation of the full CloudEvent as produced by the VMware Event Router. + +The following examples show supported and useful patterns. + +Example: Forward all CloudEvents containing one of the specified `subjects`: ```json { @@ -447,10 +453,34 @@ the following AWS EventBridge event pattern rule matches power on/off events } ``` -`"subject"` can contain one or more event categories. Wildcards (`"*"`) are not -supported. If one wants to modify the event pattern match rule **after** -deploying the VMware Event Router, its internal rules cache is periodically -synchronized with AWS EventBridge at a fixed interval of 5 minutes. +Example: Forward all CloudEvents containing a `subject` with the prefix `Vm`: + +```json +{ + "detail": { + "subject": [{ + "shellstyle": "Vm*" + }] + } +} +``` + +Example: Forward all CloudEvents containing virtual machines with the prefix +`Linux`: + +```json +{ + "detail": { + "data": { + "Vm": { + "Name": [{ + "shellstyle": "Linux*" + }] + } + } + } +} +``` > **Note:** A list of event names (categories) and how to retrieve them can be > found diff --git a/vmware-event-router/README.MD b/vmware-event-router/README.MD index 3614272c..21073dab 100644 --- a/vmware-event-router/README.MD +++ b/vmware-event-router/README.MD @@ -35,7 +35,7 @@ to normalize events from the supported event `providers`. See - with the [Horizon event provider](#provider-type-horizon) - with the [vcsim event provider](#provider-type-vcsim) -**Note:** All implemented event `processors` use built-in retry mechanisms so +> **Note:** All implemented event `processors` use built-in retry mechanisms so your function might still be involved multiple times depending on its response code. However, if an event `provider` crashes before sending an event to the configured `processor` or when the `processor` returns an error, the event is @@ -382,15 +382,21 @@ only forwards events configured in the associated `rule` of an event bus. Rules in AWS EventBridge use pattern matching ([docs](https://docs.aws.amazon.com/eventbridge/latest/userguide/filtering-examples-structure.html)). Upon start, VMware Event Router contacts EventBridge (using the given IAM role) -to parse and extract event categories from the configured rule ARN (see -configuration option below). +to parse the configured rule ARN (see configuration option below). -The VMware Event Router uses the `"subject"` field in the event payload to store -the event category, e.g. `"VmPoweredOnEvent"`. Thus it is required that you use -a **specific pattern match** (`"detail->subject"`) that the VMware Event Router -can parse to retrieve the desired event (forwarding) categories. For example, -the following AWS EventBridge event pattern rule matches power on/off events -(including DRS-enabled clusters): +The VMware Event Router uses the pattern match library which supports a subset +of the EventBridge pattern rules. You may only use these supported patterns in +your specified EventBridge rule. Refer to [this +page](https://github.com/timbray/quamina/blob/v0.2.0/PATTERNS.md) for the +currently supported patterns in `Quamina`. + +> **Note:** EventBridge wraps each VMware Event Router event (CloudEvent) into +> an EventBridge message envelop. The `detail` field contains the JSON +> representation of the full CloudEvent as produced by the VMware Event Router. + +The following examples show supported and useful patterns. + +Example: Forward all CloudEvents containing one of the specified `subjects`: ```json { @@ -400,10 +406,34 @@ the following AWS EventBridge event pattern rule matches power on/off events } ``` -`"subject"` can contain one or more event categories. Wildcards (`"*"`) are not -supported. If one wants to modify the event pattern match rule **after** -deploying the VMware Event Router, its internal rules cache is periodically -synchronized with AWS EventBridge at a fixed interval of 5 minutes. +Example: Forward all CloudEvents containing a `subject` with the prefix `Vm`: + +```json +{ + "detail": { + "subject": [{ + "shellstyle": "Vm*" + }] + } +} +``` + +Example: Forward all CloudEvents containing virtual machines with the prefix +`Linux`: + +```json +{ + "detail": { + "data": { + "Vm": { + "Name": [{ + "shellstyle": "Linux*" + }] + } + } + } +} +``` > **Note:** A list of event names (categories) and how to retrieve them can be > found diff --git a/vmware-event-router/go.mod b/vmware-event-router/go.mod index bf233f26..a0a035a4 100644 --- a/vmware-event-router/go.mod +++ b/vmware-event-router/go.mod @@ -18,10 +18,11 @@ require ( github.com/openfaas-incubator/connector-sdk v0.0.0-20200902074656-7f648543d4aa github.com/openfaas/faas-provider v0.15.1 github.com/pkg/errors v0.9.1 + github.com/timbray/quamina v0.2.0 github.com/vmware/govmomi v0.24.1-0.20210210035757-ed60338583b0 go.uber.org/zap v1.16.0 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9 - gotest.tools v2.2.0+incompatible + gotest.tools/v3 v3.2.0 k8s.io/api v0.18.8 k8s.io/apimachinery v0.18.8 k8s.io/client-go v0.18.8 @@ -45,7 +46,7 @@ require ( github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/protobuf v1.4.3 // indirect - github.com/google/go-cmp v0.5.2 // indirect + github.com/google/go-cmp v0.5.5 // indirect github.com/google/gofuzz v1.1.0 // indirect github.com/googleapis/gax-go/v2 v2.0.5 // indirect github.com/googleapis/gnostic v0.4.0 // indirect diff --git a/vmware-event-router/go.sum b/vmware-event-router/go.sum index d6cdf4d4..13a13dd3 100644 --- a/vmware-event-router/go.sum +++ b/vmware-event-router/go.sum @@ -308,8 +308,9 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ 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.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-github/v27 v27.0.6/go.mod h1:/0Gr8pJ55COkmv+S/yPKCczSkUPIM/LnFyubufRNIS0= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -610,6 +611,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk= +github.com/timbray/quamina v0.2.0 h1:18Bn0FwxBVGcqB/+iTvl/ltjlaOHuSqG+nK9PoAerlQ= +github.com/timbray/quamina v0.2.0/go.mod h1:ThK75zJCw/UZzxE4a1jReh0VBK+OVJbHUBhNSJh87KE= github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tsenart/go-tsz v0.0.0-20180814232043-cdeb9e1e981e/go.mod h1:SWZznP1z5Ki7hDT2ioqiFKEse8K9tU2OUvaRI0NeGQo= github.com/tsenart/vegeta/v12 v12.8.4/go.mod h1:ZiJtwLn/9M4fTPdMY7bdbIeyNeFVE8/AHbWFqCsUuho= @@ -829,6 +832,7 @@ golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44 h1:Bli41pIlzTzf3KEY06n+xnzK/BESIg2ze4Pgfh/aI8c= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -903,8 +907,9 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20200916195026-c9a70fc28ce3/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a h1:CB3a9Nez8M13wwlr/E2YtwoU+qYHKfC+JrDa45RXXoQ= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 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= @@ -1042,6 +1047,8 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20190709130402-674ba3eaed22/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/v3 v3.2.0 h1:I0DwBVMGAx26dttAj1BtJLAkVGncrkkUXfJLC4Flt/I= +gotest.tools/v3 v3.2.0/go.mod h1:Mcr9QNxkg0uMvy/YElmo4SpXgJKWgQvYrT7Kw5RzJ1A= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 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= diff --git a/vmware-event-router/internal/integration/aws_test.go b/vmware-event-router/internal/integration/aws_test.go index ef0ee8af..7410a5aa 100644 --- a/vmware-event-router/internal/integration/aws_test.go +++ b/vmware-event-router/internal/integration/aws_test.go @@ -1,8 +1,9 @@ -// +build integration,aws +//go:build integration && aws package integration_test import ( + "sync/atomic" "time" cloudevents "github.com/cloudevents/sdk-go/v2" @@ -16,7 +17,8 @@ import ( var _ = Describe("AWS Processor", func() { BeforeEach(func() { - p, err := aws.NewEventBridgeProcessor(ctx, cfg, receiver, log) + ebClient = createClient(cfg) + p, err := aws.NewEventBridgeProcessor(ctx, cfg, receiver, log, aws.WithClient(ebClient)) Expect(err).NotTo(HaveOccurred()) awsProcessor = p @@ -47,6 +49,11 @@ var _ = Describe("AWS Processor", func() { It("should not error", func() { Expect(err).ShouldNot(HaveOccurred()) }) + + It("should have sent the event", func() { + current := atomic.LoadInt32(&ebClient.sent) + Expect(current).To(Equal(int32(1))) + }) }) Context("when the EventBridge rule pattern does not match the given event type (LicenseEvent)", func() { diff --git a/vmware-event-router/internal/integration/suite_aws_test.go b/vmware-event-router/internal/integration/suite_aws_test.go index 5e66bfdb..6722a304 100644 --- a/vmware-event-router/internal/integration/suite_aws_test.go +++ b/vmware-event-router/internal/integration/suite_aws_test.go @@ -1,12 +1,19 @@ -// +build integration,aws +//go:build integration && aws package integration_test import ( "context" "os" + "sync/atomic" "testing" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/eventbridge" + "github.com/aws/aws-sdk-go/service/eventbridge/eventbridgeiface" . "github.com/onsi/ginkgo" "go.uber.org/zap" "go.uber.org/zap/zaptest" @@ -28,6 +35,16 @@ func (r receiveFunc) Receive(stats *metrics.EventStats) { r(stats) } +type mockClient struct { + eventbridgeiface.EventBridgeAPI + sent int32 // number events sent +} + +func (m *mockClient) PutEventsWithContext(ctx aws.Context, input *eventbridge.PutEventsInput, opts ...request.Option) (*eventbridge.PutEventsOutput, error) { + atomic.AddInt32(&m.sent, 1) + return m.EventBridgeAPI.PutEventsWithContext(ctx, input, opts...) +} + var ( ctx context.Context log *zap.SugaredLogger @@ -35,6 +52,7 @@ var ( awsProcessor processor.Processor cfg *config.ProcessorConfigEventBridge receiver receiveFunc + ebClient *mockClient ) func TestAWS(t *testing.T) { @@ -75,3 +93,24 @@ var _ = BeforeSuite(func() { }) var _ = AfterSuite(func() {}) + +func createClient(cfg *config.ProcessorConfigEventBridge) *mockClient { + accessKey := cfg.Auth.AWSAccessKeyAuth.AccessKey + secretKey := cfg.Auth.AWSAccessKeyAuth.SecretKey + + awsSessionAccessKey, err := session.NewSession(&aws.Config{ + Region: aws.String(cfg.Region), + Credentials: credentials.NewStaticCredentials( + accessKey, + secretKey, + "", // a token will be created when the session is used. + ), + }) + + Expect(err).ShouldNot(HaveOccurred()) + + client := eventbridge.New(awsSessionAccessKey) + Expect(client).ToNot(BeNil()) + + return &mockClient{EventBridgeAPI: client} +} diff --git a/vmware-event-router/internal/processor/aws/aws_event_bridge.go b/vmware-event-router/internal/processor/aws/aws_event_bridge.go index 4abb1151..ec9c6342 100644 --- a/vmware-event-router/internal/processor/aws/aws_event_bridge.go +++ b/vmware-event-router/internal/processor/aws/aws_event_bridge.go @@ -15,6 +15,7 @@ import ( "github.com/aws/aws-sdk-go/service/eventbridge/eventbridgeiface" cloudevents "github.com/cloudevents/sdk-go/v2" "github.com/pkg/errors" + "github.com/timbray/quamina" "go.uber.org/zap" config "github.com/vmware-samples/vcenter-event-broker-appliance/vmware-event-router/internal/config/v1alpha1" @@ -24,49 +25,23 @@ import ( ) const ( - defaultResyncInterval = time.Minute * 5 // resync rule patterns after interval - defaultPageLimit = 50 // max 50 results per page for list operations - defaultBatchSize = 10 // max 10 input events per batch sent to AWS + defaultPageLimit = 50 // max 50 results per page for list operations ) -// rules pattern to event bus mapping -type patternMap struct { - sync.RWMutex - subjects map[string]string -} - -// matches checks whether the given subject is in the pattern map and returns -// the associated event bus -func (pm *patternMap) matches(subject string) (string, bool) { - pm.RLock() - defer pm.RUnlock() - bus, matched := pm.subjects[subject] - return bus, matched -} - -// addRule adds a subject from the specified event bus to the pattern map -func (pm *patternMap) addSubject(subject, bus string) { - pm.Lock() - defer pm.Unlock() - pm.subjects[subject] = bus -} - -// init initializes the pattern map -func (pm *patternMap) init() { - pm.Lock() - defer pm.Unlock() - pm.subjects = map[string]string{} +// TODO(@mgasch): allow for multiple event rules for configured bus +type matcher struct { + *quamina.Quamina + bus string // uses cfg.eventbus as pattern name + pattern string } // EventBridgeProcessor implements the Processor interface type EventBridgeProcessor struct { session session.Session eventbridgeiface.EventBridgeAPI - patternMap *patternMap + matcher matcher // options - resyncInterval time.Duration - batchSize int logger.Logger mu sync.RWMutex @@ -76,12 +51,6 @@ type EventBridgeProcessor struct { // assert we implement Processor interface var _ processor.Processor = (*EventBridgeProcessor)(nil) -type eventPattern struct { - Detail struct { - Subject []string `json:"subject,omitempty"` - } `json:"detail,omitempty"` -} - // NewEventBridgeProcessor returns an AWS EventBridge processor for the given // configuration func NewEventBridgeProcessor(ctx context.Context, cfg *config.ProcessorConfigEventBridge, ms metrics.Receiver, log logger.Logger, opts ...Option) (*EventBridgeProcessor, error) { @@ -94,16 +63,13 @@ func NewEventBridgeProcessor(ctx context.Context, cfg *config.ProcessorConfigEve awsLog = zapSugared.Named(fmt.Sprintf("[%s]", proc)) } - eventBridge := EventBridgeProcessor{ - resyncInterval: defaultResyncInterval, - batchSize: defaultBatchSize, - Logger: awsLog, - patternMap: &patternMap{}, + proc := EventBridgeProcessor{ + Logger: awsLog, } // apply options for _, opt := range opts { - opt(&eventBridge) + opt(&proc) } if cfg == nil { @@ -122,87 +88,118 @@ func NewEventBridgeProcessor(ctx context.Context, cfg *config.ProcessorConfigEve return nil, errors.New("event bus must be specified") } - // Check the Auth Method to determine how the Session should be established - if cfg.Auth.Type == config.AWSAccessKeyAuth { - if cfg.Auth == nil || cfg.Auth.AWSAccessKeyAuth == nil { - return nil, fmt.Errorf("invalid %s credentials: accessKey and secretKey must be set", config.AWSAccessKeyAuth) + if proc.EventBridgeAPI == nil { + // Check the Auth Method to determine how the Session should be established + if cfg.Auth.Type == config.AWSAccessKeyAuth { + if cfg.Auth == nil || cfg.Auth.AWSAccessKeyAuth == nil { + return nil, fmt.Errorf("invalid %s credentials: accessKey and secretKey must be set", config.AWSAccessKeyAuth) + } + accessKey := cfg.Auth.AWSAccessKeyAuth.AccessKey + secretKey := cfg.Auth.AWSAccessKeyAuth.SecretKey + + awsSessionAccessKey, err := session.NewSession(&aws.Config{ + Region: aws.String(cfg.Region), + Credentials: credentials.NewStaticCredentials( + accessKey, + secretKey, + "", // a token will be created when the session is used. + ), + }) + if err != nil { + return nil, errors.Wrap(err, "create AWS session") + } + // Set the AWS Session to the IAM Role authenticated session + awsSession = awsSessionAccessKey } - accessKey := cfg.Auth.AWSAccessKeyAuth.AccessKey - secretKey := cfg.Auth.AWSAccessKeyAuth.SecretKey - - awsSessionAccessKey, err := session.NewSession(&aws.Config{ - Region: aws.String(cfg.Region), - Credentials: credentials.NewStaticCredentials( - accessKey, - secretKey, - "", // a token will be created when the session is used. - ), - }) - if err != nil { - return nil, errors.Wrap(err, "create AWS session") + if cfg.Auth.Type == config.AWSIAMRoleAuth { + // Create Session without additional options will load credentials region, + // and profile loaded from the environment and shared config automatically + awsSessionIam, err := session.NewSession(&aws.Config{ + Region: aws.String(cfg.Region), + }) + if err != nil { + return nil, errors.Wrap(err, "create AWS session") + } + // Set the AWS Session to the IAM Role authenticated session + awsSession = awsSessionIam } - // Set the AWS Session to the IAM Role authenticated session - awsSession = awsSessionAccessKey - } - if cfg.Auth.Type == config.AWSIAMRoleAuth { - // Create Session without additional options will load credentials region, - // and profile loaded from the environment and shared config automatically - awsSessionIam, err := session.NewSession(&aws.Config{ - Region: aws.String(cfg.Region), - }) - if err != nil { - return nil, errors.Wrap(err, "create AWS session") + + proc.session = *awsSession + ebSession := eventbridge.New(awsSession) + + if ebSession == nil { + return nil, errors.New("create AWS event bridge session") } - // Set the AWS Session to the IAM Role authenticated session - awsSession = awsSessionIam + + proc.EventBridgeAPI = ebSession } - eventBridge.session = *awsSession - ebSession := eventbridge.New(awsSession) + if err := configureRuleMatcher(ctx, &proc, cfg.EventBus, cfg.RuleARN); err != nil { + return nil, errors.Wrap(err, "configure rule matcher") + } - if ebSession == nil { - return nil, errors.Errorf("create AWS event bridge session") + // pre-populate the metrics stats + proc.stats = metrics.EventStats{ + Provider: string(config.ProcessorEventBridge), + Type: config.EventProcessor, + Address: cfg.RuleARN, // Using Rule ARN to uniquely identify and represent this processor + Started: time.Now().UTC(), + Invocations: make(map[string]*metrics.InvocationDetails), } - eventBridge.EventBridgeAPI = ebSession + go proc.PushMetrics(ctx, ms) + + return &proc, nil +} + +func configureRuleMatcher(ctx context.Context, proc *EventBridgeProcessor, bus string, ruleARN string) error { + q, err := quamina.New() + if err != nil { + return errors.Wrap(err, "create quamina pattern match instance") + } + + proc.matcher = matcher{ + Quamina: q, + bus: bus, + } var ( found bool nextToken *string ) - eventBridge.patternMap.init() for !found { - rules, err := eventBridge.ListRulesWithContext(ctx, &eventbridge.ListRulesInput{ - EventBusName: aws.String(cfg.EventBus), // explicitly passing eventbus name because list assumes "default" otherwise + rules, err := proc.ListRulesWithContext(ctx, &eventbridge.ListRulesInput{ + EventBusName: aws.String(bus), // explicitly passing eventbus name because list assumes "default" otherwise Limit: aws.Int64(defaultPageLimit), // up to n results per page for requests. NextToken: nextToken, }) if err != nil { - return nil, errors.Wrap(err, "list event bridge rules") + return errors.Wrap(err, "list event bridge rules") } arnLoop: for _, rule := range rules.Rules { switch { - case *rule.Arn == cfg.RuleARN: + case *rule.Arn == ruleARN: if rule.EventPattern == nil { - return nil, errors.Errorf("rule event pattern must not be empty") + return errors.New("rule event pattern must not be nil") } - var e eventPattern - err := json.Unmarshal([]byte(*rule.EventPattern), &e) - if err != nil { - return nil, errors.Wrap(err, "parse rule event pattern") + pattern := *rule.EventPattern + if err := proc.matcher.AddPattern(proc.matcher.bus, pattern); err != nil { + return errors.Wrap(err, "add rule event pattern to matcher") } - if len(e.Detail.Subject) == 0 { // might be a valid scenario, emit warning - eventBridge.Warn("rule event pattern does not contain any subjects") - } - for _, s := range e.Detail.Subject { - eventBridge.Infow("adding rule event forwarding pattern to processor", "subject", s) - eventBridge.patternMap.addSubject(s, *rule.EventBusName) - } + proc.Infow( + "adding rule event forwarding pattern to processor", + "bus", + proc.matcher.bus, + "pattern", + pattern, + ) + proc.matcher.pattern = pattern + found = true break arnLoop @@ -218,28 +215,15 @@ func NewEventBridgeProcessor(ctx context.Context, cfg *config.ProcessorConfigEve nextToken = rules.NextToken continue default: // nothing found - return nil, errors.Errorf("rule %s not found for configured AWS event bridge account", cfg.RuleARN) + return errors.Errorf("rule %q not found for configured AWS event bridge account", ruleARN) } } - - // pre-populate the metrics stats - eventBridge.stats = metrics.EventStats{ - Provider: string(config.ProcessorEventBridge), - Type: config.EventProcessor, - Address: cfg.RuleARN, // Using Rule ARN to uniquely identify and represent this processor - Started: time.Now().UTC(), - Invocations: make(map[string]*metrics.InvocationDetails), - } - - go eventBridge.PushMetrics(ctx, ms) - go eventBridge.syncPatternMap(ctx, cfg.EventBus, cfg.RuleARN) // periodically sync rules - - return &eventBridge, nil + return nil } // Process implements the stream processor interface func (eb *EventBridgeProcessor) Process(ctx context.Context, ce cloudevents.Event) error { - eb.Debugw("processing event", "eventID", ce.ID(), "event", ce) + eb.Debugw("processing event", "eventID", ce.ID(), "event", ce.String()) subject := ce.Subject() eb.mu.Lock() @@ -249,123 +233,70 @@ func (eb *EventBridgeProcessor) Process(ctx context.Context, ce cloudevents.Even } eb.mu.Unlock() - if bus, ok := eb.patternMap.matches(subject); ok { - jsonBytes, err := json.Marshal(ce) - if err != nil { - return processor.NewError(config.ProcessorEventBridge, errors.Wrapf(err, "marshal event %s", ce.ID())) - } - - jsonString := string(jsonBytes) - entry := eventbridge.PutEventsRequestEntry{ - Detail: aws.String(jsonString), - EventBusName: aws.String(bus), - Source: aws.String(ce.Source()), - DetailType: aws.String(subject), - } - - // TODO: add batching (metrics stats currently assume single item) - input := eventbridge.PutEventsInput{ - Entries: []*eventbridge.PutEventsRequestEntry{&entry}, - } - - eb.Infow("sending event", "eventID", ce.ID(), "subject", subject) - resp, err := eb.PutEventsWithContext(ctx, &input) - eb.Debugw("got response", "eventID", ce.ID(), "response", resp) - eb.mu.Lock() - defer eb.mu.Unlock() - if err != nil { - eb.stats.Invocations[subject].Failure() - return processor.NewError(config.ProcessorEventBridge, errors.Wrapf(err, "send event %s", ce.ID())) - } - - eb.Infow("successfully sent event", "eventID", ce.ID()) - eb.stats.Invocations[subject].Success() - return nil + // this is the format eventbridge uses (also for matching) so we need to wrap + // the cloudevent into the Detail field in order to correctly match + awsEvent := struct { + Detail cloudevents.Event `json:"detail,omitempty"` + }{ + Detail: ce, } - eb.Infow("skipping event: pattern rule does not match", "eventID", ce.ID(), "subject", subject) - return nil -} + e, err := json.Marshal(awsEvent) + if err != nil { + return processor.NewError(config.ProcessorEventBridge, errors.Wrapf(err, "convert cloudevent to aws eventbridge event: %s", ce.ID())) + } -func (eb *EventBridgeProcessor) syncPatternMap(ctx context.Context, eventbus, ruleARN string) { - for { - select { - case <-ctx.Done(): - return - case <-time.After(eb.resyncInterval): - eb.Debugw("syncing pattern map for rule ARN", "ruleARN", ruleARN) + matches, err := eb.matcher.MatchesForEvent(e) + if err != nil { + return processor.NewError(config.ProcessorEventBridge, errors.Wrapf(err, "match event: %s", ce.ID())) + } - err := eb.syncRules(ctx, eventbus, ruleARN) + for _, m := range matches { + if m == eb.matcher.bus { + jsonBytes, err := json.Marshal(ce) if err != nil { - eb.Errorw("could not sync pattern map for rule ARN", "ruleARN", ruleARN, "error", err) - eb.Infof("retrying pattern map sync after %v", eb.resyncInterval) + return processor.NewError(config.ProcessorEventBridge, errors.Wrapf(err, "marshal event: %s", ce.ID())) } - eb.Debugw("successfully synced pattern map for rule ARN", "ruleARN", ruleARN) - } - } -} - -func (eb *EventBridgeProcessor) syncRules(ctx context.Context, eventbus, ruleARN string) error { - // reset pattern map - eb.patternMap.init() - - var ( - found bool - nextToken *string - ) + jsonString := string(jsonBytes) + entry := eventbridge.PutEventsRequestEntry{ + Detail: aws.String(jsonString), + EventBusName: aws.String(eb.matcher.bus), + Source: aws.String(ce.Source()), + DetailType: aws.String(subject), + } - for !found { - rules, err := eb.ListRulesWithContext(ctx, &eventbridge.ListRulesInput{ - EventBusName: aws.String(eventbus), // explicitly passing eventbus name because list assumes "default" otherwise - Limit: aws.Int64(defaultPageLimit), - NextToken: nextToken, - }) - if err != nil { - return errors.Wrap(err, "list event bridge rules") - } + // TODO: add batching (metrics stats currently assume single item) + input := eventbridge.PutEventsInput{ + Entries: []*eventbridge.PutEventsRequestEntry{&entry}, + } - arnLoop: - for _, rule := range rules.Rules { - switch { - case *rule.Arn == ruleARN: - if rule.EventPattern == nil { - return errors.Errorf("rule event pattern must not be empty") - } + eb.Infow("sending event", "eventID", ce.ID(), "type", ce.Type(), "subject", subject) + resp, err := eb.PutEventsWithContext(ctx, &input) + eb.Debugw("got response", "eventID", ce.ID(), "response", resp) - var e eventPattern - err := json.Unmarshal([]byte(*rule.EventPattern), &e) + updateStats := func(err error) error { + eb.mu.Lock() + defer eb.mu.Unlock() if err != nil { - return errors.Wrap(err, "parse rule event pattern") - } - - if len(e.Detail.Subject) == 0 { // might be a valid scenario, emit warning - eb.Warn("rule event pattern does not contain any subjects") + eb.stats.Invocations[subject].Failure() + return processor.NewError(config.ProcessorEventBridge, errors.Wrapf(err, "send event: %s", ce.ID())) } - for _, s := range e.Detail.Subject { - eb.Infow("adding rule event forwarding pattern to processor", "subject", s) - eb.patternMap.addSubject(s, *rule.EventBusName) - } - - found = true - break arnLoop - - default: - continue + eb.Infow("successfully sent event", "eventID", ce.ID()) + eb.stats.Invocations[subject].Success() + return nil } - } - - switch { - case found: // return early - return nil - case rules.NextToken != nil: // try next batch of rules, if any - nextToken = rules.NextToken - continue - default: // nothing found - return errors.Errorf("rule %s not found for configured AWS event bridge account", ruleARN) + return updateStats(err) } } + + eb.Debugw( + "skipping event: pattern rule does not match", + "eventID", ce.ID(), + "event", ce.String(), + "pattern", eb.matcher.pattern, + ) return nil } diff --git a/vmware-event-router/internal/processor/aws/aws_event_bridge_test.go b/vmware-event-router/internal/processor/aws/aws_event_bridge_test.go new file mode 100644 index 00000000..655aae81 --- /dev/null +++ b/vmware-event-router/internal/processor/aws/aws_event_bridge_test.go @@ -0,0 +1,364 @@ +//go:build unit + +package aws + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + "testing" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/request" + "github.com/aws/aws-sdk-go/service/eventbridge" + "github.com/aws/aws-sdk-go/service/eventbridge/eventbridgeiface" + cloudevents "github.com/cloudevents/sdk-go/v2" + "github.com/google/uuid" + "github.com/vmware/govmomi/vim25/types" + "go.uber.org/zap/zaptest" + "gotest.tools/v3/assert" + + config "github.com/vmware-samples/vcenter-event-broker-appliance/vmware-event-router/internal/config/v1alpha1" + "github.com/vmware-samples/vcenter-event-broker-appliance/vmware-event-router/internal/metrics" +) + +const ( + region = "test-region" + eventBus = "test-bus" + ruleARN = "test-rule" +) + +type mockMetrics struct { + sync.Mutex + success int + failed int + + once sync.Once + received chan struct{} // signal Receive was called once +} + +func (m *mockMetrics) Receive(stats *metrics.EventStats) { + m.Lock() + defer m.Unlock() + + if m.success != 0 || m.failed != 0 { + m.once.Do(func() { + // close and signal if we received at least one real metric update + close(m.received) + }) + + return + } + + for _, v := range stats.Invocations { + m.success += v.SuccessCount + m.failed += v.FailureCount + } +} + +type mockClient struct { + eventbridgeiface.EventBridgeAPI + failPut bool // returns generic failure on PutEvents + failList bool // returns generic failure on ListRules + pattern string + sent int32 // track successful put calls +} + +func (m *mockClient) PutEventsWithContext(_ aws.Context, input *eventbridge.PutEventsInput, _ ...request.Option) (*eventbridge.PutEventsOutput, error) { + if m.failPut { + return nil, fmt.Errorf("could not put event: %v", input) + } + + atomic.AddInt32(&m.sent, 1) + return &eventbridge.PutEventsOutput{}, nil +} + +func (m *mockClient) ListRulesWithContext(_ aws.Context, input *eventbridge.ListRulesInput, _ ...request.Option) (*eventbridge.ListRulesOutput, error) { + if m.failList { + return nil, fmt.Errorf("could not list rules for input %v", input) + } + + return &eventbridge.ListRulesOutput{ + NextToken: nil, + Rules: []*eventbridge.Rule{ + { + Arn: aws.String(ruleARN), + EventBusName: aws.String(eventBus), + EventPattern: aws.String(m.pattern), + }, + }, + }, nil +} + +func TestEventBridgeProcessor_New(t *testing.T) { + tests := []struct { + name string + pattern string + failList bool // fail list rule with error + wantError string // expect send error + }{ + { + name: "fails to create when rules cannot be listed", + failList: true, + pattern: `{"detail": {"subject": [{"exists":true}]}}`, + wantError: "could not list", + }, + { + name: "fails to create when pattern rule is empty", + failList: false, + pattern: "", + wantError: "empty pattern", + }, + { + name: "successfully creates processor", + failList: false, + pattern: `{"detail": {"subject": [{"exists":true}]}}`, + wantError: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + logger := zaptest.NewLogger(t) + + cfg := config.ProcessorConfigEventBridge{ + Region: region, + EventBus: eventBus, + RuleARN: ruleARN, + } + + ebClient := mockClient{ + pattern: tt.pattern, + } + + ebClient.failList = tt.failList + + metricsClient := mockMetrics{ + received: make(chan struct{}), + } + + _, err := NewEventBridgeProcessor(ctx, &cfg, &metricsClient, logger.Sugar(), WithClient(&ebClient)) + if tt.wantError != "" { + assert.ErrorContains(t, err, tt.wantError) + } else { + assert.NilError(t, err) + } + }) + } +} + +func TestEventBridgeProcessor_Process(t *testing.T) { + tests := []struct { + name string + event cloudevents.Event + pattern string + wantSent int + wantFailed int + wantError string // expect send error + }{ + { + name: "fails to send when PutEvents returns error", + event: newTestEvent(t, "VmPoweredOnEvent", newVMPoweredOnEvent()), + pattern: `{"detail": {"subject": [{"exists":true}]}}`, // match any + wantSent: 0, + wantFailed: 1, + wantError: "could not put event", + }, + { + name: "successfully sends event", + event: newTestEvent(t, "VmPoweredOnEvent", newVMPoweredOnEvent()), + pattern: `{"detail": {"subject": [{"exists":true}]}}`, // match any + wantSent: 1, + wantFailed: 0, + wantError: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + logger := zaptest.NewLogger(t) + + cfg := config.ProcessorConfigEventBridge{ + Region: region, + EventBus: eventBus, + RuleARN: ruleARN, + } + + ebClient := mockClient{ + pattern: tt.pattern, + } + + if tt.wantError != "" { + ebClient.failPut = true + } + + metricsClient := mockMetrics{ + received: make(chan struct{}), + } + + proc, err := NewEventBridgeProcessor(ctx, &cfg, &metricsClient, logger.Sugar(), WithClient(&ebClient)) + assert.NilError(t, err) + defer func() { + err = proc.Shutdown(ctx) + assert.NilError(t, err) + }() + + err = proc.Process(ctx, tt.event) + if tt.wantError != "" { + assert.ErrorContains(t, err, tt.wantError) + } else { + assert.NilError(t, err) + } + + <-metricsClient.received + success := func() int { + metricsClient.Lock() + defer metricsClient.Unlock() + return metricsClient.success + } + + failed := func() int { + metricsClient.Lock() + defer metricsClient.Unlock() + return metricsClient.failed + } + + assert.Equal(t, tt.wantSent, success()) + assert.Equal(t, tt.wantFailed, failed()) + }) + } +} + +func TestEventBridgeProcessor_Process_PatternMatch(t *testing.T) { + tests := []struct { + name string + event cloudevents.Event + pattern string + wantSent int32 + }{ + { + name: "pattern match on subject \"VmPoweredOnEvent\" or \"VmPoweredOffEvent\"", + event: newTestEvent(t, "VmPoweredOnEvent", newVMPoweredOnEvent()), + pattern: `{"detail": {"subject": ["VmPoweredOnEvent","VmPoweredOffEvent"]}}`, + wantSent: 1, + }, + { + name: "pattern match on subject with prefix \"Vm\"", + event: newTestEvent(t, "VmReconfiguredEvent", newVMReconfiguredEvent()), + pattern: `{"detail": {"subject": [{"shellstyle":"Vm*"}]}}`, + wantSent: 1, + }, + { + name: "pattern match on VM name with prefix \"Linux\"", + event: newTestEvent(t, "VmPoweredOnEvent", newVMPoweredOnEvent()), + pattern: `{"detail": {"data": {"Vm": {"Name": [{"shellstyle": "Linux*"}]}}}}`, + wantSent: 1, + }, + { + name: "pattern match on extended attribute eventclass anything-but \"eventex\" and \"extendedevent\"", + event: newTestEvent(t, "VmPoweredOnEvent", newVMPoweredOnEvent()), + pattern: `{"detail": {"eventclass": [{"anything-but": ["extendedevent","eventex"]}]}}`, + wantSent: 1, + }, + { + name: "no pattern match on VM name with prefix \"Windows\"", + event: newTestEvent(t, "VmPoweredOnEvent", newVMPoweredOnEvent()), + pattern: `{"detail": {"data": {"Vm": {"Name": [{"shellstyle": "Windows*"}]}}}}`, + wantSent: 0, + }, + { + name: "no pattern match on NULL Host value", + event: newTestEvent(t, "VmPoweredOnEvent", newVMPoweredOnEvent()), + pattern: `{"detail": {"data": {"Host": [{"exists": false}]}}}`, + wantSent: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + logger := zaptest.NewLogger(t) + + cfg := config.ProcessorConfigEventBridge{ + Region: region, + EventBus: eventBus, + RuleARN: ruleARN, + } + + ebClient := mockClient{ + pattern: tt.pattern, + } + + proc, err := NewEventBridgeProcessor(ctx, &cfg, &mockMetrics{}, logger.Sugar(), WithClient(&ebClient)) + assert.NilError(t, err) + defer func() { + err = proc.Shutdown(ctx) + assert.NilError(t, err) + }() + + err = proc.Process(ctx, tt.event) + assert.NilError(t, err) + + sent := atomic.LoadInt32(&ebClient.sent) + assert.Equal(t, tt.wantSent, sent) + }) + } +} + +func newTestEvent(t *testing.T, subject string, data interface{}) cloudevents.Event { + t.Helper() + + e := cloudevents.NewEvent() + e.SetID(uuid.New().String()) + e.SetType("test.event.type.v0") + e.SetSubject(subject) + e.SetSource("test-source") + e.SetExtension("eventclass", "event") + + err := e.SetData(cloudevents.ApplicationJSON, data) + assert.NilError(t, err) + + return e +} + +func newVMPoweredOnEvent() types.BaseEvent { + return &types.VmPoweredOnEvent{ + VmEvent: types.VmEvent{ + Event: types.Event{ + Vm: &types.VmEventArgument{ + EntityEventArgument: types.EntityEventArgument{ + Name: "Linux-1234", + }, + Vm: types.ManagedObjectReference{ + Type: "VirtualMachine", + Value: "vm-1234", + }, + }, + }, + }, + } +} + +func newVMReconfiguredEvent() types.BaseEvent { + return &types.VmReconfiguredEvent{ + VmEvent: types.VmEvent{ + Event: types.Event{ + Vm: &types.VmEventArgument{ + EntityEventArgument: types.EntityEventArgument{ + Name: "Linux-1234", + }, + Vm: types.ManagedObjectReference{ + Type: "VirtualMachine", + Value: "vm-1234", + }, + }, + }, + }, + } +} diff --git a/vmware-event-router/internal/processor/aws/options.go b/vmware-event-router/internal/processor/aws/options.go index 244c0799..0876108e 100644 --- a/vmware-event-router/internal/processor/aws/options.go +++ b/vmware-event-router/internal/processor/aws/options.go @@ -1,24 +1,16 @@ package aws import ( - "time" + "github.com/aws/aws-sdk-go/service/eventbridge/eventbridgeiface" ) // Option configures the AWS processor // TODO: change signature to return errors type Option func(*EventBridgeProcessor) -// WithResyncInterval configures the interval to sync AWS EventBridge event -// pattern rules -func WithResyncInterval(interval time.Duration) Option { +// WithClient uses the specified EventBridge client, e.g. useful in testing +func WithClient(client eventbridgeiface.EventBridgeAPI) Option { return func(aws *EventBridgeProcessor) { - aws.resyncInterval = interval - } -} - -// WithBatchSize sets the batch size for PutEvents requests -func WithBatchSize(size int) Option { - return func(aws *EventBridgeProcessor) { - aws.batchSize = size + aws.EventBridgeAPI = client } } From 02c6c9e3e4095d7b4b010bb8b0fbac797d0ff3b5 Mon Sep 17 00:00:00 2001 From: Michael Gasch Date: Thu, 23 Jun 2022 17:37:49 +0200 Subject: [PATCH 48/54] feat: Add kn-go-harbor-webhook function Closes: #914 Signed-off-by: Michael Gasch --- .../knative/go/kn-go-harbor-webhook/.ko.yaml | 14 ++ .../knative/go/kn-go-harbor-webhook/README.md | 219 ++++++++++++++++ .../go/kn-go-harbor-webhook/function.yaml | 33 +++ .../knative/go/kn-go-harbor-webhook/go.mod | 28 +++ .../knative/go/kn-go-harbor-webhook/go.sum | 234 ++++++++++++++++++ .../knative/go/kn-go-harbor-webhook/main.go | 78 ++++++ .../knative/go/kn-go-harbor-webhook/server.go | 211 ++++++++++++++++ .../go/kn-go-harbor-webhook/server_test.go | 201 +++++++++++++++ .../go/kn-go-harbor-webhook/sinkbinding.yaml | 14 ++ .../static/screenshot-1.png | Bin 0 -> 145185 bytes 10 files changed, 1032 insertions(+) create mode 100644 examples/knative/go/kn-go-harbor-webhook/.ko.yaml create mode 100644 examples/knative/go/kn-go-harbor-webhook/README.md create mode 100644 examples/knative/go/kn-go-harbor-webhook/function.yaml create mode 100644 examples/knative/go/kn-go-harbor-webhook/go.mod create mode 100644 examples/knative/go/kn-go-harbor-webhook/go.sum create mode 100644 examples/knative/go/kn-go-harbor-webhook/main.go create mode 100644 examples/knative/go/kn-go-harbor-webhook/server.go create mode 100644 examples/knative/go/kn-go-harbor-webhook/server_test.go create mode 100644 examples/knative/go/kn-go-harbor-webhook/sinkbinding.yaml create mode 100644 examples/knative/go/kn-go-harbor-webhook/static/screenshot-1.png diff --git a/examples/knative/go/kn-go-harbor-webhook/.ko.yaml b/examples/knative/go/kn-go-harbor-webhook/.ko.yaml new file mode 100644 index 00000000..d6710a53 --- /dev/null +++ b/examples/knative/go/kn-go-harbor-webhook/.ko.yaml @@ -0,0 +1,14 @@ +builds: + - id: function + # dir: . + # main: . + env: + - GOPRIVATE=*.vmware.com + flags: + - -tags + - netgo + ldflags: + - -s -w + - -extldflags "-static" + - -X main.buildCommit={{.Env.KO_COMMIT}} + - -X main.buildTag={{.Env.KO_TAG}} diff --git a/examples/knative/go/kn-go-harbor-webhook/README.md b/examples/knative/go/kn-go-harbor-webhook/README.md new file mode 100644 index 00000000..a1df7b45 --- /dev/null +++ b/examples/knative/go/kn-go-harbor-webhook/README.md @@ -0,0 +1,219 @@ +# kn-go-harbor-webhook + +Example Knative Go function for receiving [Project Harbor +webhook](https://goharbor.io/docs/latest/working-with-projects/project-configuration/configure-webhooks/) +notifications (events). + +⚠️ This guide assumes that you have stood up a working Knative environment using +the vCenter Event Broker Appliance (VEBA). Since the function needs to be +exposed to the outside (so Harbor can send webhook notifications to it), **your +DNS server needs to be set up with wildcard DNS support for the VEBA host**. + +# How the function works + +The function starts an HTTP server, transforms Harbor webhook event +notifications to [CloudEvents](https://cloudevents.io/) and sends them to the +configured `K_SINK` (injected via a Knative +[`SinkBinding`](https://knative.dev/docs/eventing/custom-event-source/sinkbinding/)). +By default, `K_SINK` is set as the VEBA `default` broker in the +`vmware-functions` namespace. + +The function can be set up with +[`basic_auth`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication) +for the HTTP endpoint. This is highly recommended (and the default in this guide +using a Kubernetes secret). + +The webhook endpoint in the function, i.e. the HTTP `POST` target, by default is +`/webhook` (configurable.) + +See the [deployment](#step-3---deploy) section for configuration options and +details. + +The event transformation is done as follows: + +| CloudEvent Field | Harbor Event Field | Comment | Example | +|------------------|--------------------|-------------------------------------------------------------------------------------------------------------------------|----------------------------------------| +| `ID` | n/a | An `ID` is autogenerated using UUIDv4 | `35f42638-4627-4a8e-8862-360cae64dc44` | +| `Source` | n/a | The Source is generated using a fixed format (`/%s`) using the Knative `service` Name | `/kn-go-harbor-webhook` | +| `Type` | `type` | Harbor event `type` field is lower-cased and injected into a fixed CloudEvent `Type` format (`com.vmware.harbor.%s.v0`) | `com.vmware.harbor.pull_artifact.v0` | +| `Subject` | `operator` | This field might be empty | `admin` | +| `Time` | `occur_at` | Converted Harbor time to RFC3339 (UTC) | `2022-06-22T08:49:48Z` | +| `Data` | n/a | JSON-encoded full Harbor event | | + +A full example of a structured CloudEvent (JSON): + +```json +{ + "specversion": "1.0", + "id": "", + "source": "/kn-go-harbor-webhook", + "type": "com.vmware.harbor.pull_artifact.v0", + "subject": "admin", + "datacontenttype": "application/json", + "time": "2022-06-22T08:49:48Z", + "data": { + "type": "PULL_ARTIFACT", + "occur_at": 1655887788, + "operator": "admin", + "event_data": { + "resources": [{ + "digest": "sha256:3b465cbcadf7d437fc70c3b6aa2c93603a7eef0a3f5f1e861d91f303e4aabdee", + "tag": "sha256:3b465cbcadf7d437fc70c3b6aa2c93603a7eef0a3f5f1e861d91f303e4aabdee", + "resource_url": "harbor-app.jarvis.tanzu/veba-test/csi-test@sha256:3b465cbcadf7d437fc70c3b6aa2c93603a7eef0a3f5f1e861d91f303e4aabdee" + }], + "repository": { + "date_created": 1655887764, + "name": "csi-test", + "namespace": "veba-test", + "repo_full_name": "veba-test/csi-test", + "repo_type": "public" + } + } + } +} +``` + +# Step 1 - Build + +⚠️ This step is only required if you made code changes to any of the \*.go +files. To directly deploy the function jump to [Step 3](#step-3---deploy). + +Requirement: If you make changes to the Go code, the +[ko](https://github.com/google/ko) tool is required to create the artifacts. + +Set the destination to push the function container image with an environment +variable. + +```bash +export KO_DOCKER_REPO=docker.io/my-user +export KO_COMMIT=$(git rev-parse --short=8 HEAD) +export KO_TAG=1.0 +``` + +The following command will build and push the image to the specified +`KO_DOCKER_REPO` repository. + +```bash +# for docker.io +ko publish --bare -t $KO_TAG . + +# for GCR +ko publish -B -t $KO_TAG . +``` + +⚠️ Using the above example, the resulting image would be +`docker.io/myuser/kn-go-harbor-webhook:1.0`. + + +# Step 2 - Test + +Run unit tests using the following command: + +```bash +go test -v -race -count 1 ./... +``` + +# Step 3 - Deploy + +⚠️ The following steps assume a working Knative environment using the `default` + Rabbit `broker`. The Knative `service` and `sinkbinding` will be installed in + the `vmware-functions` Kubernetes namespace, assuming that the `broker` is also + available there. + +## Create Basic Auth Credentials Secrets + +Create a secret holding the username and password enforcing basic authentication +on the HTTP endpoint of the function which receives Harbor webhook +notifications. + +```bash +kubectl create secret generic webhook-auth \ +--type=kubernetes.io/basic-auth \ +--from-literal=username='webhookuser' \ +--from-literal=password='replaceme' +--namespace vmware-functions + +# update label for secret to show up in VEBA UI +kubectl -n vmware-functions label secret webhook-auth app=veba-ui +``` + +## Update Environment Settings + +The `function.yaml` comes with sane defaults, incl. basic auth for the HTTP +endpoint. Users may update environment specific settings under `env:` in the +`function.yaml` file. + +Please see the table below for a description of the available (and **required**) +settings. + + +| Configuration | Description | Example Values | Required | +|-----------------------|------------------------------------------------------------------------------|---------------------------|----------| +| `ADDRESS` | HTTP listen (bind) address of the function | `"0.0.0.0"` (default) | **Yes** | +| `WEBHOOK_PATH` | Endpoint where the function HTTP server accepts Harbor webhook notifications | `"/webhook"` (default) | **Yes** | +| `WEBHOOK_SECRET_PATH` | The path where the basic auth credentials secret will be mounted | `"/var/bindings/webhook"` | No | +| `DEBUG` | Enable debug logging | `"true"` | No | + +## Deploy the Function + +Create the `SinkBinding` which will automatically inject the VEBA `default` +broker into the function. + +```bash +kubectl create -f sinkbinding.yaml -n vmware-functions +``` + +⚠️ If you made changes to the Go code/container image in [Step +1](#step-1---build) edit the `function.yaml` file with the custom name of the +container image used to build and push. + +Deploy the function to the VMware Event Broker Appliance (VEBA): + +```bash +kubectl create -f function.yaml -n vmware-functions +``` + +For testing purposes, the [Knative manifest](function.yaml) contains the +following annotations, which will ensure the Knative Service Pod will always run +**exactly** one instance for debugging purposes. Functions deployed through the +VMware Event Broker Appliance UI defaults to scale to 0, which means the pods +will only run when it is triggered by a vCenter Event. + +```yaml +annotations: + autoscaling.knative.dev/maxScale: "1" + autoscaling.knative.dev/minScale: "1" +``` + +## Configure the Harbor Webhook + +Configure the webhook notifications in the Harbor UI. For example, the following +screenshot shows how to send all Harbor events to the `kn-go-harbor-webhook` +function with basic auth enabled. + +Change the "Endpoint URL" and "Auth Header" fields accordingly: + +The "Endpoint URL" is +`https://kn-go-harbor-webhook.vmware-functions./webhook` if you used +the default values provided in the `function.yaml`. + +The value in "Auth Header" needs to start with `Basic` followed by a whitespace +and the base64-encoded value of the `:` string combination +defined in the `webhook-auth` secret created earlier. You can use [this online +tool](https://www.debugbear.com/basic-auth-header-generator) to create the +required value. + +![Harbor Webhook Configuration](static/screenshot-1.png) + +# Step 4 - Undeploy + +```bash +# undeploy function +kubectl delete -f function.yaml -n vmware-functions + +# undeploy sinkbinding +kubectl delete -f sinkbinding.yaml -n vmware-functions + +# delete secret +kubectl delete secret webhook-auth -n vmware-functions +``` diff --git a/examples/knative/go/kn-go-harbor-webhook/function.yaml b/examples/knative/go/kn-go-harbor-webhook/function.yaml new file mode 100644 index 00000000..2f932c40 --- /dev/null +++ b/examples/knative/go/kn-go-harbor-webhook/function.yaml @@ -0,0 +1,33 @@ +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: kn-go-harbor-webhook + labels: + app: veba-ui +spec: + template: + metadata: + annotations: + autoscaling.knative.dev/maxScale: "1" + autoscaling.knative.dev/minScale: "1" + spec: + containers: + - image: us.gcr.io/daisy-284300/veba/kn-go-harbor-webhook:1.0 + imagePullPolicy: IfNotPresent + env: + - name: ADDRESS + value: "0.0.0.0" + - name: WEBHOOK_PATH + value: "/webhook" # default + - name: DEBUG + value: "true" + - name: WEBHOOK_SECRET_PATH # remove this env to disable basic auth + value: "/var/bindings/webhook" + volumeMounts: # remove this when not using basic auth + - name: webhook-auth + mountPath: "/var/bindings/webhook" + readOnly: true + volumes: # remove this when not using basic auth + - name: webhook-auth + secret: + secretName: webhook-auth diff --git a/examples/knative/go/kn-go-harbor-webhook/go.mod b/examples/knative/go/kn-go-harbor-webhook/go.mod new file mode 100644 index 00000000..ee96c0c4 --- /dev/null +++ b/examples/knative/go/kn-go-harbor-webhook/go.mod @@ -0,0 +1,28 @@ +module github.com/vmware-samples/vcenter-event-broker-appliance/examples/knative/go/kn-go-harbor-webhook + +go 1.18 + +require ( + github.com/cloudevents/sdk-go/v2 v2.10.1 + github.com/embano1/vsphere v0.2.2 + github.com/goharbor/harbor/src v0.0.0-20220622063440-0cf036e73a36 + github.com/google/uuid v1.3.0 + github.com/kelseyhightower/envconfig v1.4.0 + go.uber.org/zap v1.21.0 + golang.org/x/sync v0.0.0-20210220032951-036812b2e83c + gotest.tools/v3 v3.0.3 +) + +require ( + github.com/beego/beego v1.12.9 // indirect + github.com/benbjohnson/clock v1.1.0 // indirect + github.com/google/go-cmp v0.5.6 // indirect + github.com/hashicorp/golang-lru v0.5.4 // indirect + github.com/json-iterator/go v1.1.11 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.1 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.8.0 // indirect +) diff --git a/examples/knative/go/kn-go-harbor-webhook/go.sum b/examples/knative/go/kn-go-harbor-webhook/go.sum new file mode 100644 index 00000000..39bb1c51 --- /dev/null +++ b/examples/knative/go/kn-go-harbor-webhook/go.sum @@ -0,0 +1,234 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Knetic/govaluate v3.0.0+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alicebob/gopher-json v0.0.0-20180125190556-5a6b3ba71ee6/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc= +github.com/alicebob/miniredis v2.5.0+incompatible/go.mod h1:8HZjEj4yU0dwhYHky+DxYx+6BMjkBbe5ONFIF1MXffk= +github.com/beego/beego v1.12.9 h1:knN+7lL7BSVFm6McUVu58QVrh2UUPn0C9ioq83W5seo= +github.com/beego/beego v1.12.9/go.mod h1:QURFL1HldOcCZAxnc1cZ7wrplsYR5dKPHFjmk6WkLAs= +github.com/beego/goyaml2 v0.0.0-20130207012346-5545475820dd/go.mod h1:1b+Y/CofkYwXMUU0OhQqGvsY2Bvgr4j6jfT699wyZKQ= +github.com/beego/x2j v0.0.0-20131220205130-a0352aadc542/go.mod h1:kSeGC/p1AbBiEp5kat81+DSQrZenVBZXklMLaELspWU= +github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bradfitz/gomemcache v0.0.0-20180710155616-bc664df96737/go.mod h1:PmM6Mmwb0LSuEubjR8N7PtNe1KxZLtOUHtbeikc5h60= +github.com/casbin/casbin v1.7.0/go.mod h1:c67qKN6Oum3UF5Q1+BByfFxkwKvhwW57ITjqwtzR1KE= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cloudevents/sdk-go/v2 v2.10.1 h1:qNFovJ18fWOd8Q9ydWJPk1oiFudXyv1GxJIP7MwPjuM= +github.com/cloudevents/sdk-go/v2 v2.10.1/go.mod h1:GpCBmUj7DIRiDhVvsK5d6WCbgTWs8DxAWTRtAwQmIXs= +github.com/cloudflare/golz4 v0.0.0-20150217214814-ef862a3cdc58/go.mod h1:EOBUe0h4xcZ5GoxqC5SDxFQ8gwyZPKQoEzownBlhI80= +github.com/couchbase/go-couchbase v0.0.0-20201216133707-c04035124b17/go.mod h1:+/bddYDxXsf9qt0xpDUtRR47A2GjaXmGGAqQ/k3GJ8A= +github.com/couchbase/gomemcached v0.1.2-0.20201224031647-c432ccf49f32/go.mod h1:mxliKQxOv84gQ0bJWbI+w9Wxdpt9HjDvgW9MjCym5Vo= +github.com/couchbase/goutils v0.0.0-20210118111533-e33d3ffb5401/go.mod h1:BQwMFlJzDjFDG3DJUdU0KORxn88UlsOULuxLExMh3Hs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cupcake/rdb v0.0.0-20161107195141-43ba34106c76/go.mod h1:vYwsqCOLxGiisLwp9rITslkFNpZD5rz43tf41QFkTWY= +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/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/elastic/go-elasticsearch/v6 v6.8.5/go.mod h1:UwaDJsD3rWLM5rKNFzv9hgox93HoX8utj1kxD9aFUcI= +github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4= +github.com/embano1/vsphere v0.2.2 h1:QIsdBSL5BbwmkKqj8/MqAkGXvn8EJKo6eHBneUR3H/g= +github.com/embano1/vsphere v0.2.2/go.mod h1:R9spY3upDbJete+1Jp9r4xZ+AuI4GkS/KAHl9IwDQuw= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/glendc/gopher-json v0.0.0-20170414221815-dc4743023d0c/go.mod h1:Gja1A+xZ9BoviGJNA2E9vFkPjjsl+CoJxSXiQM1UXtw= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-redis/redis v6.14.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= +github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/goharbor/harbor/src v0.0.0-20220622063440-0cf036e73a36 h1:ydLuq3CRqlk8VGgwGNYejyyn4eikCd4vMncsZuOknfg= +github.com/goharbor/harbor/src v0.0.0-20220622063440-0cf036e73a36/go.mod h1:P75O0oYa4pNwMqprSvbl9TUyTqEfI6cn4jLnTPx5UFo= +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.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.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.0-20170215233205-553a64147049/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= +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.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc= +github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11 h1:uVUAXhF2To8cbw/3xN3pxj6kk7TYKs98NIrTqPlMWAQ= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +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/ledisdb/ledisdb v0.0.0-20200510135210-d35789ec47e6/go.mod h1:n931TsDuKuq+uX4v1fulaMbA/7ZLLhjc85h7chZGBCQ= +github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= +github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U= +github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +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 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.0/go.mod h1:oUhWkIvk5aDxtKvDDuw8gItl8pKl42LzjC9KZE0HfGg= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/pelletier/go-toml v1.0.1/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/peterh/liner v1.0.1-0.20171122030339-3681c2a91233/go.mod h1:xIteQHvHuaLYG9IFj6mSxM0fCKrs34IrEQUhOYuGPHc= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +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_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.0/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644 h1:X+yvsM2yrEktyI+b2qND5gpH8YhURn0k8OCaeRnkINo= +github.com/shiena/ansicolor v0.0.0-20151119151921-a422bbe96644/go.mod h1:nkxAfR/5quYxwPZhyDxgasBMnRtBZd0FCEpawpjMUFg= +github.com/siddontang/go v0.0.0-20170517070808-cb568a3e5cc0/go.mod h1:3yhqj7WBBfRhbBlzyOC3gUxftwsU0u8gqevxwIHQpMw= +github.com/siddontang/goredis v0.0.0-20150324035039-760763f78400/go.mod h1:DDcKzU3qCuvj/tPnimWSsZZzvk9qvkvrIL5naVBPh5s= +github.com/siddontang/rdb v0.0.0-20150307021120-fc89ed2e418d/go.mod h1:AMEsy7v5z92TR1JKMkLLoaOQk++LVnOKL3ScbJ8GNGA= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/ssdb/gossdb v0.0.0-20180723034631-88f6b59b84ec/go.mod h1:QBvMkMya+gXctz3kmljlUCu/yB3GZ6oee+dUozsezQE= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +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 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/syndtr/goleveldb v0.0.0-20160425020131-cfa635847112/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= +github.com/ugorji/go v0.0.0-20171122102828-84cb69a8af83/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= +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/wendal/errors v0.0.0-20181209125328-7f31f4b264ec/go.mod h1:Q12BUT7DqIlHRmgv3RskH+UCM/4eqVMgI0EMmlSpAXc= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/gopher-lua v0.0.0-20171031051903-609c9cd26973/go.mod h1:aEV29XrmTYFr3CiRxZeGHpkvbwq+prZduBqMaascyCU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= +go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= +go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.21.0 h1:WefMeulhovoZ2sYXz7st6K0sLj7bBhpiFaud4r4zST8= +go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/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-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +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-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-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac h1:7zkz7BUtwNFFqcowJ+RIgu2MaV/MapERkDIy+mwPyjs= +golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/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-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190624222133-a101b041ded4/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +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.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +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-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= diff --git a/examples/knative/go/kn-go-harbor-webhook/main.go b/examples/knative/go/kn-go-harbor-webhook/main.go new file mode 100644 index 00000000..892a0fa5 --- /dev/null +++ b/examples/knative/go/kn-go-harbor-webhook/main.go @@ -0,0 +1,78 @@ +package main + +import ( + "context" + "os" + "os/signal" + "syscall" + + "github.com/embano1/vsphere/logger" + "github.com/kelseyhightower/envconfig" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +var ( + buildCommit = "unknown" + buildTag = "unknown" +) + +type config struct { + // http settings + Address string `envconfig:"ADDRESS" default:"0.0.0.0" required:"true"` + Path string `envconfig:"WEBHOOK_PATH" default:"/webhook" required:"true"` + + // knative injected + Port int `envconfig:"PORT" default:"8080" required:"true"` + Service string `envconfig:"K_SERVICE" required:"true"` + Sink string `envconfig:"K_SINK" required:"true"` + + Debug bool `envconfig:"DEBUG" default:"false"` + + SecretPath string `envconfig:"WEBHOOK_SECRET_PATH"` +} + +func main() { + var cfg config + if err := envconfig.Process("", &cfg); err != nil { + panic("process environment variables: " + err.Error()) + } + + log, err := getLogger(cfg.Debug) + if err != nil { + panic("create logger: " + err.Error()) + } + log = log.Named("harbor-webhook") + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + ctx = logger.Set(ctx, log) + + if err = run(ctx, cfg); err != nil { + log.Panic("could not run server", zap.Error(err)) + } + + log.Info("graceful shutdown complete") +} + +func getLogger(debug bool) (*zap.Logger, error) { + fields := []zap.Field{ + zap.String("commit", buildCommit), + zap.String("tag", buildTag), + } + + var cfg zap.Config + if debug { + cfg = zap.NewDevelopmentConfig() + } else { + cfg = zap.NewProductionConfig() + cfg.EncoderConfig.EncodeTime = zapcore.RFC3339NanoTimeEncoder + } + + log, err := cfg.Build(zap.Fields(fields...)) + if err != nil { + return nil, err + } + + return log, nil +} diff --git a/examples/knative/go/kn-go-harbor-webhook/server.go b/examples/knative/go/kn-go-harbor-webhook/server.go new file mode 100644 index 00000000..a1a31526 --- /dev/null +++ b/examples/knative/go/kn-go-harbor-webhook/server.go @@ -0,0 +1,211 @@ +package main + +import ( + "context" + "crypto/sha256" + "crypto/subtle" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + ce "github.com/cloudevents/sdk-go/v2" + cectx "github.com/cloudevents/sdk-go/v2/context" + "github.com/embano1/vsphere/logger" + "github.com/goharbor/harbor/src/pkg/notifier/model" + "github.com/google/uuid" + "go.uber.org/zap" + "golang.org/x/sync/errgroup" +) + +const ( + httpTimeout = time.Second * 5 + sourceFormat = "/%s" // /K_SERVICE + eventTypeFormat = "com.vmware.harbor.%s.v0" // com.vmware.harbor.pull_artifact.v0 + + // secrets + userFileKey = "username" + passwordFileKey = "password" +) + +var ( + retries = 3 + retryDelay = time.Millisecond * 200 +) + +func run(ctx context.Context, cfg config) error { + log := logger.Get(ctx) + + client, err := ce.NewClientHTTP(ce.WithTarget(cfg.Sink)) + if err != nil { + return fmt.Errorf("create cloudevent client: %w", err) + } + + var auth bool + if path := os.Getenv("WEBHOOK_SECRET_PATH"); path != "" { + auth = true + } + + handler := eventHandler(ctx, client) + if auth { + user, err := readKey(userFileKey, cfg.SecretPath) + if err != nil { + return fmt.Errorf("read secret key %q: %w", userFileKey, err) + } + + pass, err := readKey(passwordFileKey, cfg.SecretPath) + if err != nil { + return fmt.Errorf("read secret key %q: %w", passwordFileKey, err) + } + + handler = withBasicAuth(ctx, eventHandler(ctx, client), user, pass) + } + + mux := http.NewServeMux() + mux.Handle(cfg.Path, handler) + + address := fmt.Sprintf("%s:%d", cfg.Address, cfg.Port) + srv := http.Server{ + Addr: address, + Handler: mux, + ReadTimeout: httpTimeout, + WriteTimeout: httpTimeout, + } + + eg, egCtx := errgroup.WithContext(ctx) + + eg.Go(func() error { + <-egCtx.Done() + log.Info("shutting down http server") + shutdownCtx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + if err := srv.Shutdown(shutdownCtx); err != nil { + log.Warn("could not gracefully shutdown http server") + } + return nil + }) + + log.Info("starting http server", + zap.String("address", address), + zap.String("path", cfg.Path), + zap.String("sink", cfg.Sink), + zap.Bool("basic_auth", auth), + ) + + eg.Go(func() error { + if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + return fmt.Errorf("run http server: %w", err) + } + return nil + }) + + return eg.Wait() +} + +// harbor webhook event handler +func eventHandler(ctx context.Context, client ce.Client) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } + + // TODO (@mgasch): support inbound rate limiting + + log := logger.Get(ctx) + b, err := io.ReadAll(r.Body) + if err != nil { + log.Error("read body", zap.Error(err)) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + var event model.Payload + if err = json.Unmarshal(b, &event); err != nil { + log.Error("could not decode harbor notification event", zap.Error(err)) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + id := uuid.New().String() + log = log.With(zap.String("eventID", id)) + + log.Debug("received request", zap.String("request", string(b))) + + e := ce.NewEvent() + e.SetID(id) + e.SetSource(fmt.Sprintf(sourceFormat, os.Getenv("K_SERVICE"))) + e.SetSubject(event.Operator) // might be empty + + // sanity check + if event.Type == "" { + log.Error("harbor event type must not be empty", zap.String("type", event.Type)) + http.Error(w, http.StatusText(http.StatusBadRequest), http.StatusBadRequest) + return + } + + t := strings.ToLower(event.Type) + e.SetType(fmt.Sprintf(eventTypeFormat, t)) + + ts := time.Unix(event.OccurAt, 0) + e.SetTime(ts) + + if err = e.SetData(ce.ApplicationJSON, event); err != nil { + log.Error("could not set cloudevent data", zap.Error(err)) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + ctx = cectx.WithRetriesExponentialBackoff(ctx, retryDelay, retries) + if err = client.Send(ctx, e); ce.IsUndelivered(err) || ce.IsNACK(err) { + log.Error("could not send cloudevent", zap.Error(err), zap.String("event", e.String())) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + log.Debug("successfully sent cloudevent", zap.Any("event", e)) + }) +} + +// withBasicAuth enforces basic auth as a middleware for the given username and +// password +func withBasicAuth(ctx context.Context, next http.Handler, u, p string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + username, password, ok := r.BasicAuth() + if ok { + // reduce brute-force guessing attacks with constant-time comparisons + usernameHash := sha256.Sum256([]byte(username)) + passwordHash := sha256.Sum256([]byte(password)) + expectedUsernameHash := sha256.Sum256([]byte(u)) + expectedPasswordHash := sha256.Sum256([]byte(p)) + + usernameMatch := subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1 + passwordMatch := subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1 + + if usernameMatch && passwordMatch { + next.ServeHTTP(w, r) + return + } + } + + logger.Get(ctx).Debug("rejecting incoming request: user not authenticated") + w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`) + http.Error(w, "Unauthorized", http.StatusUnauthorized) + }) +} + +// readKey reads the file from the secret path +func readKey(key string, path string) (string, error) { + data, err := ioutil.ReadFile(filepath.Join(path, key)) + if err != nil { + return "", err + } + return string(data), nil +} diff --git a/examples/knative/go/kn-go-harbor-webhook/server_test.go b/examples/knative/go/kn-go-harbor-webhook/server_test.go new file mode 100644 index 00000000..1365faf7 --- /dev/null +++ b/examples/knative/go/kn-go-harbor-webhook/server_test.go @@ -0,0 +1,201 @@ +package main + +import ( + "context" + "encoding/json" + "io/fs" + "io/ioutil" + nethttp "net/http" + "net/http/httptest" + "os" + "path/filepath" + "strings" + "sync/atomic" + "testing" + "time" + + ce "github.com/cloudevents/sdk-go/v2" + "github.com/cloudevents/sdk-go/v2/protocol" + "github.com/cloudevents/sdk-go/v2/protocol/http" + "github.com/embano1/vsphere/logger" + "go.uber.org/zap" + "go.uber.org/zap/zaptest" + "gotest.tools/v3/assert" +) + +type mockClient struct { + ce.Client + + t *testing.T + requests int32 + code int +} + +func (m *mockClient) Send(_ context.Context, event ce.Event) protocol.Result { + assert.NilError(m.t, event.Validate()) + + var inputEvent map[string]interface{} + err := json.Unmarshal([]byte(harborEvent), &inputEvent) + assert.NilError(m.t, err) + + assert.Equal(m.t, inputEvent["occur_at"], float64(event.Time().Unix())) + + eventType := strings.ToLower(inputEvent["type"].(string)) + assert.Assert(m.t, strings.Contains(event.Type(), eventType)) + + // send mock response + atomic.AddInt32(&m.requests, 1) + if m.code != 200 { + return http.NewResult(m.code, "") + } + return nil // ACK +} + +func Test_run(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + ctx = logger.Set(ctx, zaptest.NewLogger(t)) + dir, err := ioutil.TempDir("", "secret") + assert.NilError(t, err) + + err = ioutil.WriteFile(filepath.Join(dir, userFileKey), []byte("user"), fs.ModePerm) + assert.NilError(t, err) + + err = ioutil.WriteFile(filepath.Join(dir, passwordFileKey), []byte("pass"), fs.ModePerm) + assert.NilError(t, err) + + t.Cleanup(func() { + if err := os.RemoveAll(dir); err != nil { + logger.Get(ctx).Error("could not clean up temp directory", zap.Error(err)) + } + }) + + t.Setenv("WEBHOOK_SECRET_PATH", dir) + + cfg := config{ + Address: "127.0.0.1", + Path: "/webhook", + Port: 8080, + Service: "testservice", + Sink: "somesink", + Debug: true, + SecretPath: dir, + } + + err = run(ctx, cfg) + assert.NilError(t, err) +} + +func Test_eventhandler(t *testing.T) { + type basicAuth struct { + username string + password string + } + + basicAuthCredentials := basicAuth{ + username: "user", + password: "pass", + } + + tests := []struct { + name string + auth *basicAuth + code int + method string + wantCount int32 + wantCode int + }{ + { + name: "successfully sends event (no auth)", + code: 200, + method: nethttp.MethodPost, + wantCount: 1, + wantCode: 200, + }, + { + name: "fails with 405 if method is not POST", + code: 0, + method: nethttp.MethodGet, + wantCount: 0, + wantCode: 405, + }, + { + name: "fails with 401 not authorized", + auth: &basicAuth{ + username: "userrrrr", + password: "passssssss", + }, + code: 0, + method: nethttp.MethodPost, + wantCount: 0, + wantCode: 401, + }, + { + name: "successfully sends event (basic auth)", + auth: &basicAuth{ + username: "user", + password: "pass", + }, + code: 200, + method: nethttp.MethodPost, + wantCount: 1, + wantCode: 200, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + ctx := logger.Set(context.Background(), zaptest.NewLogger(t)) + t.Setenv("K_SERVICE", "testservice") + + // overwrite retry config + retries = 3 + retryDelay = time.Millisecond + + mc := mockClient{ + t: t, + code: tc.code, + } + + testHandler := eventHandler(ctx, &mc) + req := httptest.NewRequest(tc.method, "/webhook", strings.NewReader(harborEvent)) + + if tc.auth != nil { + t.Setenv("WEBHOOK_SECRET_PATH", "/somesecret") + testHandler = withBasicAuth(ctx, eventHandler(ctx, &mc), basicAuthCredentials.username, basicAuthCredentials.password) + req.SetBasicAuth(tc.auth.username, tc.auth.password) + } + + rec := httptest.NewRecorder() + testHandler.ServeHTTP(rec, req) + + count := atomic.LoadInt32(&mc.requests) + assert.Equal(t, tc.wantCount, count) + assert.Equal(t, tc.wantCode, rec.Code) + }) + } +} + +const harborEvent = ` +{ + "type": "PULL_ARTIFACT", + "occur_at": 1655887788, + "operator": "admin", + "event_data": { + "resources": [ + { + "digest": "sha256:3b465cbcadf7d437fc70c3b6aa2c93603a7eef0a3f5f1e861d91f303e4aabdee", + "tag": "sha256:3b465cbcadf7d437fc70c3b6aa2c93603a7eef0a3f5f1e861d91f303e4aabdee", + "resource_url": "harbor-app.jarvis.tanzu/veba-test/csi-test@sha256:3b465cbcadf7d437fc70c3b6aa2c93603a7eef0a3f5f1e861d91f303e4aabdee" + } + ], + "repository": { + "date_created": 1655887764, + "name": "csi-test", + "namespace": "veba-test", + "repo_full_name": "veba-test/csi-test", + "repo_type": "public" + } + } +}` diff --git a/examples/knative/go/kn-go-harbor-webhook/sinkbinding.yaml b/examples/knative/go/kn-go-harbor-webhook/sinkbinding.yaml new file mode 100644 index 00000000..350ff842 --- /dev/null +++ b/examples/knative/go/kn-go-harbor-webhook/sinkbinding.yaml @@ -0,0 +1,14 @@ +apiVersion: sources.knative.dev/v1 +kind: SinkBinding +metadata: + name: kn-go-harbor-webhook-binding +spec: + subject: + apiVersion: serving.knative.dev/v1 + kind: Service + name: kn-go-harbor-webhook + sink: + ref: + apiVersion: eventing.knative.dev/v1 + kind: Broker + name: default \ No newline at end of file diff --git a/examples/knative/go/kn-go-harbor-webhook/static/screenshot-1.png b/examples/knative/go/kn-go-harbor-webhook/static/screenshot-1.png new file mode 100644 index 0000000000000000000000000000000000000000..08085cd55d95876532c6bbf5f4383394354005f6 GIT binary patch literal 145185 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 c?4&HMN76Vi8q~(I{04q5Y8z Date: Mon, 27 Jun 2022 20:44:46 +0200 Subject: [PATCH 49/54] feat: created kn-ps-harbor-slack function example Closes: #916 Signed-off-by: Robert Guske --- docs/site/examples.md | 34 +++- .../powershell/kn-ps-harbor-slack/Dockerfile | 5 + .../powershell/kn-ps-harbor-slack/README.md | 157 ++++++++++++++++++ .../kn-ps-harbor-slack/function.yaml | 39 +++++ .../powershell/kn-ps-harbor-slack/handler.ps1 | 112 +++++++++++++ .../kn-ps-harbor-slack/slack_secret.json | 4 + .../test/docker-test-env-variable | 1 + .../test/send-cloudevent-test.ps1 | 17 ++ .../test/send-cloudevent-test.sh | 14 ++ .../kn-ps-harbor-slack/test/test-payload.json | 21 +++ 10 files changed, 396 insertions(+), 8 deletions(-) create mode 100644 examples/knative/powershell/kn-ps-harbor-slack/Dockerfile create mode 100644 examples/knative/powershell/kn-ps-harbor-slack/README.md create mode 100644 examples/knative/powershell/kn-ps-harbor-slack/function.yaml create mode 100644 examples/knative/powershell/kn-ps-harbor-slack/handler.ps1 create mode 100644 examples/knative/powershell/kn-ps-harbor-slack/slack_secret.json create mode 100644 examples/knative/powershell/kn-ps-harbor-slack/test/docker-test-env-variable create mode 100644 examples/knative/powershell/kn-ps-harbor-slack/test/send-cloudevent-test.ps1 create mode 100755 examples/knative/powershell/kn-ps-harbor-slack/test/send-cloudevent-test.sh create mode 100644 examples/knative/powershell/kn-ps-harbor-slack/test/test-payload.json diff --git a/docs/site/examples.md b/docs/site/examples.md index dc637dbd..1a188e82 100644 --- a/docs/site/examples.md +++ b/docs/site/examples.md @@ -196,7 +196,7 @@ examples: usecases: - item: integration id: kn-ps-webhook-function - description: Function to ingest a non-CloudEvent using a custom incoming webhook + description: Function to ingest a non-CloudEvent using a custom incoming webhook. links: - language: powershell url: "/tree/master/examples/knative/powershell/kn-ps-webhook" @@ -205,7 +205,7 @@ examples: usecases: - item: notification id: kn-ps-vrni-databus-function - description: Function that accepts an incoming webhook from the vRealize Network Insight Databus, constructs a CloudEvent and sends it to the VMware Event Router + description: Function that accepts an incoming webhook from the vRealize Network Insight Databus, constructs a CloudEvent and sends it to the VMware Event Router. links: - language: powershell url: "/tree/master/examples/knative/powershell/kn-ps-vrni-databus" @@ -214,7 +214,7 @@ examples: usecases: - item: notification id: kn-ps-vsphere-inv-slack-function - description: Function to send a Slack notification when a specific vSphere inventory resource has been deleted + description: Function to send a Slack notification when a specific vSphere inventory resource has been deleted. links: - language: powershell url: "/tree/master/examples/knative/powershell/kn-ps-vsphere-inv-slack" @@ -224,7 +224,7 @@ examples: - item: automation - item: remediation id: kn-pcli-snapshot-cron-function - description: Function to manage VM snapshots on a scheduled job (cron) + description: Function to manage VM snapshots on a scheduled job (cron). links: - language: powercli url: "/tree/master/examples/knative/powercli/kn-pcli-snapshot-cron" @@ -233,18 +233,18 @@ examples: usecases: - item: integration id: kn-go-preemption-function - description: Function for triggering vSphere virtual machine preemption (power off) using a workflow engine and the vsphere-preemption prototype + description: Function for triggering vSphere virtual machine preemption (power off) using a workflow engine and the vsphere-preemption prototype. links: - language: go url: "/tree/master/examples/knative/go/kn-go-preemption" - title: vRealize Orchestrator - usecases: + usecases: - item: integration - item: remediation id: kn-py-vro-function description: Trigger a vRealize Orchestrator workflow, passing all CloudEvent data as native vRO datatypes, using the vRO REST API. - links: + links: - language: python url: "/tree/master/examples/knative/python/kn-py-vro" @@ -252,10 +252,28 @@ examples: usecases: - item: integration id: kn-ps-zapier-function - description: Trigger a Zapier workflow, passing select CloudEvent data to a Zapier webhook + description: Trigger a Zapier workflow, passing select CloudEvent data to a Zapier webhook. links: - language: powershell url: "/tree/master/examples/knative/powershell/kn-ps-zapier" + + - title: Transform Harbor webhook event notifications to CloudEvents + usecases: + - item: integration + id: kn-go-harbor-webhook-function + description: Function for receiving Project Harbor webhook notifications (events). + links: + - language: go + url: "/tree/master/examples/knative/go/kn-go-harbor-webhook" + + - title: Creates a Slack notification when a Harbor webhook notification event got triggered + usecases: + - item: notification + id: kn-ps-harbor-slack-function + description: Function to send a Slack notification triggered by a Harbor webhook notification. + links: + - language: powershell + url: "/tree/master/examples/knative/powershell/kn-ps-harbor-slack" --- A complete and updated list of ready to use functions curated by the VMware Event Broker community is listed below. diff --git a/examples/knative/powershell/kn-ps-harbor-slack/Dockerfile b/examples/knative/powershell/kn-ps-harbor-slack/Dockerfile new file mode 100644 index 00000000..1b16e1ef --- /dev/null +++ b/examples/knative/powershell/kn-ps-harbor-slack/Dockerfile @@ -0,0 +1,5 @@ +FROM us.gcr.io/daisy-284300/veba/ce-ps-base:1.4 + +COPY handler.ps1 handler.ps1 + +CMD ["pwsh","./server.ps1"] diff --git a/examples/knative/powershell/kn-ps-harbor-slack/README.md b/examples/knative/powershell/kn-ps-harbor-slack/README.md new file mode 100644 index 00000000..7fcaac9b --- /dev/null +++ b/examples/knative/powershell/kn-ps-harbor-slack/README.md @@ -0,0 +1,157 @@ +# kn-ps-harbor-slack + +Example Knative PowerShell function for sending Harbor CloudEvents to a Slack webhook. This function relies on the Harbor webhook [function example](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/development/examples/knative/go/kn-go-harbor-webhook) which is a requirement for this example. + +# Step 1 - Build + +Create the container image locally to test your function logic. Change the IMAGE name accordingly, example below for Docker. + +```console +export IMAGE=/kn-ps-#REPLACE-FN-NAME#:1.0 +docker build -t ${IMAGE} +``` + +# Step 2 - Test + +Verify the container image works by executing it locally. + +Change into the `test` directory + +```console +cd test +``` + +Update the following variable names within the `docker-test-env-variable` file + +* SLACK_WEBHOOK_URL - Slack webhook URL +* SLACK_MESSAGE_PRETEXT - Text displayed for Slack notification + +Start the container image by running the following command: + +```console +docker run -e FUNCTION_DEBUG=true -e PORT=8080 --env-file docker-test-env-variable -it --rm -p 8080:8080 ${IMAGE} +``` + +In a separate terminal, run either `send-cloudevent-test.ps1` (PowerShell Script) or `send-cloudevent-test.sh` (Bash Script) to simulate a CloudEvent payload being sent to the local container image + +```console +Testing Function ... +See docker container console for output + +# Output from docker container console +06/27/2022 09:47:31 - DEBUG: K8s Secrets: +{"SLACK_WEBHOOK_URL":"**********","SLACK_MESSAGE_PRETEXT":":harbor: Harbor Slack Function :veba_official:"} + +06/27/2022 09:47:31 - DEBUG: CloudEventData + +Name Value +---- ----- +event_data {resources, repository} +occur_at 1656076946 +type PUSH_ARTIFACT +operator admin + + + +06/27/2022 09:47:31 - DEBUG: "{ + "attachments": [ + { + "footer_icon": "https://raw.githubusercontent.com/vmware-samples/vcenter-event-broker-appliance/development/logo/veba_icon_only.png", + "footer": "Powered by VEBA", + "pretext": ":harbor: Harbor Slack Function :veba_official:", + "fields": [ + { + "short": "false", + "value": "PUSH_ARTIFACT", + "title": "Event Type" + }, + { + "short": "false", + "value": "2022-06-25T11:42:42+00:00", + "title": "DateTime in UTC" + }, + { + "short": "false", + "value": "admin", + "title": "Username" + }, + { + "short": "false", + "value": "veba-webhook/bitnami-nginx", + "title": "Repository Name" + }, + { + "short": "false", + "value": "public", + "title": "Repository Type" + }, + { + "short": "false", + "value": "1.21.6-debian-10-r117", + "title": "Image Tag" + }, + { + "short": "false", + "value": "harbor.jarvis.tanzu/veba-webhook/bitnami-nginx:1.21.6-debian-10-r117", + "title": "Image Resource Data" + }, + { + "short": "false", + "value": "sha256:d3890814cc5a7cfc02403435281cdf51adfb6b67e223934d9d6137a4ad364286", + "title": "Image Digest" + } + ] + } + ] +}" +06/27/2022 09:47:31 - Sending Webhook payload to Slack ... +06/27/2022 09:47:31 - Successfully sent Webhook ... +``` + +# Step 3 - Deploy + +> **Note:** The following steps assume a working Knative environment using the +`default` Rabbit `broker`. The Knative `service` and `trigger` will be installed in the +`vmware-functions` Kubernetes namespace, assuming that the `broker` is also available there. +> +> **Note:** Also, in order to receive incoming Harbor events and to ultimately invoke the Harbor-Slack-Function, it's necessary to have the [kn-go-harbor-webhook function example](https://github.com/vmware-samples/vcenter-event-broker-appliance/tree/development/examples/knative/go/kn-go-harbor-webhook) running properly first. + +Update the `slack_secret.json` file with your Slack webhook configurations and then create the kubernetes secret which can then be accessed from within the function by using the environment variable named called `SLACK_SECRET`. + +```console +# create secret + +kubectl -n vmware-functions create secret generic harbor-slack-secret --from-file=SLACK_SECRET=slack_secret.json + +# update label for secret to show up in VEBA UI +kubectl -n vmware-functions label secret harbor-slack-secret app=veba-ui +``` + +Edit the `function.yaml` file with the name of the container image from Step 1 if you made any changes. If not, the default VMware container image will suffice. By default, the function deployment will filter on the `com.vmware.harbor.push_artifact.v0` Harbor Event. If you wish to change this, update the `type` field within `function.yaml` to the desired event type. A list of supported notification events is available on the official Harbor documentation under [Configure Webhook Notifications](https://goharbor.io/docs/2.5.0/working-with-projects/project-configuration/configure-webhooks/). Furthermore, use the VEBA Event viewer endpoint (`https:///events`) to display all incoming events. + +Deploy the function to the VMware Event Broker Appliance (VEBA). + +```console +# deploy function + +kubectl -n vmware-functions apply -f function.yaml +``` + +For testing purposes, the `function.yaml` contains the following annotations, which will ensure the Knative Service Pod will always run **exactly** one instance for debugging purposes. Functions deployed through through the VMware Event Broker Appliance UI defaults to scale to 0, which means the pods will only run when it is triggered by an vCenter Event. + +```yaml +annotations: + autoscaling.knative.dev/maxScale: "1" + autoscaling.knative.dev/minScale: "1" +``` + +# Step 4 - Undeploy + +```console +# undeploy function + +kubectl -n vmware-functions delete -f function.yaml + +# delete secret +kubectl -n vmware-functions delete secret harbor-slack-secret +``` \ No newline at end of file diff --git a/examples/knative/powershell/kn-ps-harbor-slack/function.yaml b/examples/knative/powershell/kn-ps-harbor-slack/function.yaml new file mode 100644 index 00000000..6fff81a3 --- /dev/null +++ b/examples/knative/powershell/kn-ps-harbor-slack/function.yaml @@ -0,0 +1,39 @@ +apiVersion: serving.knative.dev/v1 +kind: Service +metadata: + name: kn-ps-harbor-slack + labels: + app: veba-ui +spec: + template: + metadata: + annotations: + autoscaling.knative.dev/maxScale: "1" + autoscaling.knative.dev/minScale: "1" + spec: + containers: + - image: us.gcr.io/daisy-284300/veba/kn-ps-harbor-slack:1.0 + envFrom: + - secretRef: + name: harbor-slack-secret + env: + - name: FUNCTION_DEBUG + value: "false" +--- +apiVersion: eventing.knative.dev/v1 +kind: Trigger +metadata: + name: kn-ps-harbor-slack-trigger + labels: + app: veba-ui +spec: + broker: default + filter: + attributes: + type: com.vmware.harbor.push_artifact.v0 + subject: admin + subscriber: + ref: + apiVersion: serving.knative.dev/v1 + kind: Service + name: kn-ps-harbor-slack diff --git a/examples/knative/powershell/kn-ps-harbor-slack/handler.ps1 b/examples/knative/powershell/kn-ps-harbor-slack/handler.ps1 new file mode 100644 index 00000000..60b2e989 --- /dev/null +++ b/examples/knative/powershell/kn-ps-harbor-slack/handler.ps1 @@ -0,0 +1,112 @@ +Function Process-Init { + [CmdletBinding()] + param() + Write-Host "$(Get-Date) - Processing Init`n" + + Write-Host "$(Get-Date) - Init Processing Completed`n" +} + +Function Process-Shutdown { + [CmdletBinding()] + param() + Write-Host "$(Get-Date) - Processing Shutdown`n" + + Write-Host "$(Get-Date) - Shutdown Processing Completed`n" +} + +Function Process-Handler { + [CmdletBinding()] + param( + [Parameter(Position=0,Mandatory=$true)][CloudNative.CloudEvents.CloudEvent]$CloudEvent + ) + + # Decode CloudEvent + try { + $cloudEventData = $cloudEvent | Read-CloudEventJsonData -Depth 10 + } catch { + throw "`nPayload must be JSON encoded" + } + + try { + $jsonSecrets = ${env:SLACK_SECRET} | ConvertFrom-Json + } catch { + throw "`nK8s secrets `$env:SLACK_SECRET does not look to be defined" + } + + if(${env:FUNCTION_DEBUG} -eq "true") { + Write-Host "$(Get-Date) - DEBUG: K8s Secrets:`n${env:SLACK_SECRET}`n" + + Write-Host "$(Get-Date) - DEBUG: CloudEventData`n $(${cloudEventData} | Out-String)`n" + } + + # Construct Slack message object + $payload = @{ + attachments = @( + @{ + pretext = $(${jsonSecrets}.SLACK_MESSAGE_PRETEXT); + fields = @( + @{ + title = "Event Type"; + value = $cloudEventData.type; + short = "false"; + } + @{ + title = "DateTime in UTC"; + value = $cloudEvent.time; + short = "false"; + } + @{ + title = "Username"; + value = $cloudEventData.operator; + short = "false"; + } + @{ + title = "Repository Name"; + value = $cloudEventData.event_data.repository.repo_full_name; + short = "false"; + } + @{ + title = "Repository Type"; + value = $cloudEventData.event_data.repository.repo_type; + short = "false"; + } + @{ + title = "Image Tag"; + value = $cloudEventData.event_data.resources[0].tag; + short = "false"; + } + @{ + title = "Image Resource Data"; + value = $cloudEventData.event_data.resources[0].resource_url; + short = "false"; + } + @{ + title = "Image Digest"; + value = $cloudEventData.event_data.resources[0].digest; + short = "false"; + } + ) + footer = "Powered by VEBA"; + footer_icon = "https://raw.githubusercontent.com/vmware-samples/vcenter-event-broker-appliance/development/logo/veba_icon_only.png"; + } + ) + } + + # Convert Slack message object into JSON + $body = $payload | ConvertTo-Json -Depth 5 + + if(${env:FUNCTION_DEBUG} -eq "true") { + Write-Host "$(Get-Date) - DEBUG: `"$body`"" + } + + Write-Host "$(Get-Date) - Sending Webhook payload to Slack ..." + $ProgressPreference = "SilentlyContinue" + + try { + Invoke-WebRequest -Uri $(${jsonSecrets}.SLACK_WEBHOOK_URL) -Method POST -ContentType "application/json" -Body $body + } catch { + throw "$(Get-Date) - Failed to send Slack Message: $($_)" + } + + Write-Host "$(Get-Date) - Successfully sent Webhook ..." +} diff --git a/examples/knative/powershell/kn-ps-harbor-slack/slack_secret.json b/examples/knative/powershell/kn-ps-harbor-slack/slack_secret.json new file mode 100644 index 00000000..92afa898 --- /dev/null +++ b/examples/knative/powershell/kn-ps-harbor-slack/slack_secret.json @@ -0,0 +1,4 @@ +{ + "SLACK_WEBHOOK_URL": "YOUR-WEBHOOK-URL", + "SLACK_MESSAGE_PRETEXT": ":harbor: Harbor Slack Function :veba_official:" +} \ No newline at end of file diff --git a/examples/knative/powershell/kn-ps-harbor-slack/test/docker-test-env-variable b/examples/knative/powershell/kn-ps-harbor-slack/test/docker-test-env-variable new file mode 100644 index 00000000..d24a6e37 --- /dev/null +++ b/examples/knative/powershell/kn-ps-harbor-slack/test/docker-test-env-variable @@ -0,0 +1 @@ +SLACK_SECRET={"SLACK_WEBHOOK_URL":"YOUR-WEBHOOK-URL","SLACK_MESSAGE_PRETEXT":":harbor: Harbor Slack Function :veba_official:"} \ No newline at end of file diff --git a/examples/knative/powershell/kn-ps-harbor-slack/test/send-cloudevent-test.ps1 b/examples/knative/powershell/kn-ps-harbor-slack/test/send-cloudevent-test.ps1 new file mode 100644 index 00000000..4d1c4abf --- /dev/null +++ b/examples/knative/powershell/kn-ps-harbor-slack/test/send-cloudevent-test.ps1 @@ -0,0 +1,17 @@ + +$headers = @{ + "Content-Type" = "application/json"; + "ce-specversion" = "1.0"; + "ce-id" = "d70079f9-fddd-4b7f-aa76-1193f28b0611"; + "ce-source" = "/kn-go-harbor-webhook"; + "ce-type" = "com.vmware.harbor.push_artifact.v0"; + "ce-subject" = "admin"; + "ce-time" = "2022-06-25T11:42:42Z"; +} + +$body = Get-Content -Raw -Path "./test-payload.json" + +Write-Host "Testing Function ..." +Invoke-WebRequest -Uri http://localhost:8080 -Method POST -Headers $headers -Body $body + +Write-host "See docker container console for output" \ No newline at end of file diff --git a/examples/knative/powershell/kn-ps-harbor-slack/test/send-cloudevent-test.sh b/examples/knative/powershell/kn-ps-harbor-slack/test/send-cloudevent-test.sh new file mode 100755 index 00000000..0197ed8e --- /dev/null +++ b/examples/knative/powershell/kn-ps-harbor-slack/test/send-cloudevent-test.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +echo "Testing Function ..." +curl -d@test-payload.json \ + -H "Content-Type: application/json" \ + -H 'ce-specversion: 1.0' \ + -H 'ce-id: d70079f9-fddd-4b7f-aa76-1193f28b0611' \ + -H 'ce-source: /kn-go-harbor-webhook' \ + -H 'ce-type: com.vmware.harbor.push_artifact.v0' \ + -H 'ce-subject: admin' \ + -H 'ce-time: 2022-06-25T11:42:42Z' \ + -X POST localhost:8080 + +echo "See docker container console for output" diff --git a/examples/knative/powershell/kn-ps-harbor-slack/test/test-payload.json b/examples/knative/powershell/kn-ps-harbor-slack/test/test-payload.json new file mode 100644 index 00000000..4c4898e5 --- /dev/null +++ b/examples/knative/powershell/kn-ps-harbor-slack/test/test-payload.json @@ -0,0 +1,21 @@ +{ + "type": "PUSH_ARTIFACT", + "occur_at": 1656076946, + "operator": "admin", + "event_data": { + "resources": [ + { + "digest": "sha256:d3890814cc5a7cfc02403435281cdf51adfb6b67e223934d9d6137a4ad364286", + "tag": "1.21.6-debian-10-r117", + "resource_url": "harbor.jarvis.tanzu/veba-webhook/bitnami-nginx:1.21.6-debian-10-r117" + } + ], + "repository": { + "date_created": 1656076946, + "name": "bitnami-nginx", + "namespace": "veba-webhook", + "repo_full_name": "veba-webhook/bitnami-nginx", + "repo_type": "public" + } + } + } \ No newline at end of file From 3d8b39592ac900b6b962c429e1373e76a7b0e26b Mon Sep 17 00:00:00 2001 From: Michael Gasch Date: Wed, 29 Jun 2022 17:58:04 +0200 Subject: [PATCH 50/54] chore: Update VEBA UI version Git repo tag is not used, changing to dummy. Closes: #918 Signed-off-by: Michael Gasch --- veba-bom.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/veba-bom.json b/veba-bom.json index cbde0513..eff754dc 100644 --- a/veba-bom.json +++ b/veba-bom.json @@ -10,10 +10,10 @@ }] }, "veba-ui": { - "gitRepoTag": "v0.6.0", + "gitRepoTag": "v0.0.0", "containers": [{ "name": "projects.registry.vmware.com/veba/veba-ui", - "version": "3e1f828b" + "version": "27b4e224" }] }, "antrea": { From 14040c39262abcdc1b31ef1589c6e8c1407dabf5 Mon Sep 17 00:00:00 2001 From: Michael Gasch Date: Mon, 4 Jul 2022 17:06:57 +0200 Subject: [PATCH 51/54] docs: Fix Harbor example docs Closes: #920 Signed-off-by: Michael Gasch --- examples/knative/go/kn-go-harbor-webhook/README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/knative/go/kn-go-harbor-webhook/README.md b/examples/knative/go/kn-go-harbor-webhook/README.md index a1df7b45..7179b9d6 100644 --- a/examples/knative/go/kn-go-harbor-webhook/README.md +++ b/examples/knative/go/kn-go-harbor-webhook/README.md @@ -104,7 +104,6 @@ ko publish -B -t $KO_TAG . ⚠️ Using the above example, the resulting image would be `docker.io/myuser/kn-go-harbor-webhook:1.0`. - # Step 2 - Test Run unit tests using the following command: @@ -130,7 +129,7 @@ notifications. kubectl create secret generic webhook-auth \ --type=kubernetes.io/basic-auth \ --from-literal=username='webhookuser' \ ---from-literal=password='replaceme' +--from-literal=password='replaceme' \ --namespace vmware-functions # update label for secret to show up in VEBA UI @@ -146,7 +145,6 @@ endpoint. Users may update environment specific settings under `env:` in the Please see the table below for a description of the available (and **required**) settings. - | Configuration | Description | Example Values | Required | |-----------------------|------------------------------------------------------------------------------|---------------------------|----------| | `ADDRESS` | HTTP listen (bind) address of the function | `"0.0.0.0"` (default) | **Yes** | From 5d1952d47e020925c672fee117ed7605a22de629 Mon Sep 17 00:00:00 2001 From: William Lam Date: Tue, 5 Jul 2022 06:29:46 -0700 Subject: [PATCH 52/54] Bump version to release-0.7.3 Signed-off-by: William Lam --- veba-bom.json | 6 +++--- vmware-event-router/chart/Chart.yaml | 4 ++-- .../event-router-v0.7.3-pre-release.tgz | Bin 3488 -> 3487 bytes 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/veba-bom.json b/veba-bom.json index eff754dc..c88f7dfb 100644 --- a/veba-bom.json +++ b/veba-bom.json @@ -1,12 +1,12 @@ { "veba": { - "version": "v0.7.2" + "version": "release-0.7.3" }, "vmware-event-router": { - "gitRepoTag": "v0.7.2", + "gitRepoTag": "release-0.7.3", "containers": [{ "name": "us.gcr.io/daisy-284300/veba/router", - "version": "v0.7.2" + "version": "release-0.7.3" }] }, "veba-ui": { diff --git a/vmware-event-router/chart/Chart.yaml b/vmware-event-router/chart/Chart.yaml index 25e7056d..e84b81c1 100644 --- a/vmware-event-router/chart/Chart.yaml +++ b/vmware-event-router/chart/Chart.yaml @@ -3,8 +3,8 @@ name: event-router description: The VMware Event Router is used to connect to various VMware event providers and forwards received events to supported event processors. home: https://vmweventbroker.io/ type: application -version: v0.7.3 -appVersion: v0.7.2 +version: v0.7.3-pre-release +appVersion: release-0.7.3 maintainers: - name: Michael Gasch email: mgasch@vmware.com diff --git a/vmware-event-router/chart/releases/event-router-v0.7.3-pre-release.tgz b/vmware-event-router/chart/releases/event-router-v0.7.3-pre-release.tgz index 41961e74e540248531a0892862e410c186511259..ebe2c8a759ace2ba24e330328546ac860039c61c 100644 GIT binary patch delta 2802 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 F0091oiiZFI From 9d8a6e043074f31f04f09c52b2f84514acb06835 Mon Sep 17 00:00:00 2001 From: William Lam Date: Wed, 6 Jul 2022 12:44:13 -0700 Subject: [PATCH 53/54] docs: Add v0.7.3 release to website Signed-off-by: William Lam --- docs/site/evolutions.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/docs/site/evolutions.md b/docs/site/evolutions.md index 8d40a80e..856e7474 100644 --- a/docs/site/evolutions.md +++ b/docs/site/evolutions.md @@ -6,6 +6,18 @@ permalink: /evolution limit: 3 entry: +- title: VEBA [v0.7.3](https://github.com/vmware-samples/vcenter-event-broker-appliance/releases/tag/v0.7.3) + type: feature + date: Jul 2022 + id: vzeroseventhree + detail: + subtitle: Features + text: + - New VMware Harbor, Zapier, NSX Tag Sync & Unapproved Portgroup Usage Functions + - Ported Datastore Usage Email, vSphere HA Restart & Host Maint. Alarm functions to Knative + - Enhanced pattern matching for EventBridge processor + - Various Documentation Updates & Bug Fixes + - title: VEBA [v0.7.2](https://williamlam.com/2022/03/vmware-event-broker-appliance-veba-v0-7-2.html) type: feature date: Mar 2022 From d23524efebdeec45aa7343c16f749c193f390117 Mon Sep 17 00:00:00 2001 From: William Lam Date: Thu, 14 Jul 2022 06:28:26 -0700 Subject: [PATCH 54/54] Bump version to v0.7.3 for release Signed-off-by: William Lam --- veba-bom.json | 6 +++--- vmware-event-router/chart/Chart.yaml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/veba-bom.json b/veba-bom.json index c88f7dfb..a0a10438 100644 --- a/veba-bom.json +++ b/veba-bom.json @@ -1,12 +1,12 @@ { "veba": { - "version": "release-0.7.3" + "version": "v0.7.3" }, "vmware-event-router": { - "gitRepoTag": "release-0.7.3", + "gitRepoTag": "v0.7.3", "containers": [{ "name": "us.gcr.io/daisy-284300/veba/router", - "version": "release-0.7.3" + "version": "v0.7.3" }] }, "veba-ui": { diff --git a/vmware-event-router/chart/Chart.yaml b/vmware-event-router/chart/Chart.yaml index e84b81c1..bc23e0c2 100644 --- a/vmware-event-router/chart/Chart.yaml +++ b/vmware-event-router/chart/Chart.yaml @@ -3,8 +3,8 @@ name: event-router description: The VMware Event Router is used to connect to various VMware event providers and forwards received events to supported event processors. home: https://vmweventbroker.io/ type: application -version: v0.7.3-pre-release -appVersion: release-0.7.3 +version: v0.7.4 +appVersion: v0.7.3 maintainers: - name: Michael Gasch email: mgasch@vmware.com